Leaf check if user is authenticated

Hi,

I finally got the authentication for my app set up and the next task is now to check inside Leaf templates whether the user is authenticated or not. I figured that, in the templates, I could do something like

#if(user):
  <p>Hello, #(user.username)</p>
#endif

but to do this I would need to pass the user object in every... single... view... which I am not a fan of. I therefor went through the source code and found ViewRenderer. I'm now wondering if a solution could be to extend this protocol

struct Context: Encodable {
    var user: User?
}

extension ViewRenderer {
    func myRender(req: Request, name: String) async throws -> View {
        var context = Context()

        if let user = req.auth.get(User.self) {
            context.user = user
        }

        return try await self.render(name, context).get()
    }

    // func myRender<E>(req: Request, _ name: String, _ context: E) async throws -> View where E: Encodable
}

or is there a better way of achieving this?

Edit:

I came up with this solution

import Vapor

struct ViewContext: Encodable {
    let valueEncoder: (Encoder) throws -> Void

    init<T: Encodable>(user: User.Read, ctx: T) {
        self.valueEncoder = {
            var container = $0.container(keyedBy: CodingKeys.self)
            try container.encode(user, forKey: .user)
            try container.encode(ctx, forKey: .ctx)
        }
    }

    enum CodingKeys: String, CodingKey {
        case user, ctx
    }

    func encode(to encoder: Encoder) throws {
        try valueEncoder(encoder)
    }
}

extension User {
    struct Read: Content {
        var id: UUID
        var username: String
        var email: String
    }
}

extension ViewRenderer {
    func customRender<E>(_ req: Request, _ name: String, _ context: E) async throws -> View where E: Encodable {
        guard let user = req.auth.get(User.self) else {
            return try await self.render(name, context).get()
        }
        
        let userDTO = User.Read(
            id: user.id!,
            username: user.username,
            email: user.email
        )
        
        return try await self.render(name, ViewContext(user: userDTO, ctx: context)).get()
    }

    func customRender(_ req: Request, _ name: String) async throws -> View {
        guard let user = req.auth.get(User.self) else {
            return try await self.render(name).get()
        }
        
        return try await self.render(name, ["user": user]).get()
    }
}

I vaguely remember, its what the userInfo (req.leaf.userInfo) is for, but I could be wrong too. It's been a while, since I used Leaf.

Not sure if that is what I was looking for because when I try debugPrint(req.leaf.userInfo) I get

[AnyHashable("application"): Vapor.Application,
AnyHashable("request"): GET / HTTP/1.1
Host: localhost:8000
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
...
Accept-Language: en-GB,en;q=0.9
Accept-Encoding: gzip, deflate
Connection: keep-alive
]

and I do not seem to be able to access it in the leaf template - I could be doing it wrong though.

Ah no, but you can use it together with a custom leaf tag, for example:

/// #username() 
struct UsernameTag: LeafTag {

   func render(_ context: LeafContext) throws -> LeafData {

      let name = context.userInfo["username"] as? String ?? ""

      return .string(name)
   }
}

At least what I can see, is been doing in the test files.

But also there is nothing wrong with your approach.

1 Like

Ah, I see :slight_smile: This looks like it could also have been a solution - and a lot less code! :slight_smile: