| @@ -3,4 +3,22 @@ | |||
| uuid = "D3C706B5-9CBD-4F91-8A68-CF08562866B5" | |||
| type = "1" | |||
| version = "2.0"> | |||
| <Breakpoints> | |||
| <BreakpointProxy | |||
| BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint"> | |||
| <BreakpointContent | |||
| uuid = "65DB5B80-779D-462A-A47F-A0A75B1C75F8" | |||
| shouldBeEnabled = "Yes" | |||
| ignoreCount = "0" | |||
| continueAfterRunningActions = "No" | |||
| filePath = "Sources/App/Controllers/UserController.swift" | |||
| startingColumnNumber = "9223372036854775807" | |||
| endingColumnNumber = "9223372036854775807" | |||
| startingLineNumber = "35" | |||
| endingLineNumber = "35" | |||
| landmarkName = "create(req:)" | |||
| landmarkType = "7"> | |||
| </BreakpointContent> | |||
| </BreakpointProxy> | |||
| </Breakpoints> | |||
| </Bucket> | |||
| @@ -11,14 +11,18 @@ import FluentKit | |||
| struct FuelEntryV2Controller: RouteCollection { | |||
| func boot(routes: RoutesBuilder) throws { | |||
| let fuelEntries = routes.grouped("fuel_entries") | |||
| fuelEntries.get(use: index) | |||
| fuelEntries.get("test", use: filtered) | |||
| let tokenAuthMiddleware = Token.authenticator() | |||
| let guardMiddleware = User.guardMiddleware() | |||
| let tokenAuthGroup = fuelEntries.grouped(tokenAuthMiddleware, guardMiddleware) | |||
| tokenAuthGroup.get(use: index) | |||
| tokenAuthGroup.get("test", use: filtered) | |||
| // fuelEntries.get("with-images", use: getAllEntriesWithImages) | |||
| fuelEntries.post(use: create) | |||
| tokenAuthGroup.post(use: create) | |||
| // fuelEntries.post("upload-image", use: uploadImage) | |||
| fuelEntries.group(":fuelEntryID") { fuelEntry in | |||
| tokenAuthGroup.group(":fuelEntryID") { fuelEntry in | |||
| fuelEntry.delete(use: delete) | |||
| } | |||
| } | |||
| func index(req: Request) async throws -> [FuelEntry] { | |||
| @@ -26,57 +26,17 @@ extension UserSignup: Validatable { | |||
| } | |||
| struct UserController: RouteCollection { | |||
| func boot(routes: RoutesBuilder) throws { | |||
| let usersRoute = routes.grouped("users") | |||
| usersRoute.post("signup", use: create) | |||
| let tokenProtected = usersRoute.grouped(Token.authenticator()) | |||
| tokenProtected.get("me", use: getMyOwnUser) | |||
| let passwordProtected = usersRoute.grouped(User.authenticator()) | |||
| passwordProtected.post("login", use: login) | |||
| } | |||
| fileprivate func create(req: Request) throws -> EventLoopFuture<NewSession> { | |||
| try UserSignup.validate(content: req) | |||
| let userSignup = try req.content.decode(UserSignup.self) | |||
| let user = try User.create(from: userSignup) | |||
| var token: Token! | |||
| return checkIfUserExists(userSignup.username, req: req).flatMap { exists in | |||
| guard !exists else { | |||
| return req.eventLoop.future(error: UserError.usernameTaken) | |||
| } | |||
| return user.save(on: req.db) | |||
| }.flatMap { | |||
| guard let newToken = try? user.createToken(source: .signup) else { | |||
| return req.eventLoop.future(error: Abort(.internalServerError)) | |||
| } | |||
| token = newToken | |||
| return token.save(on: req.db) | |||
| }.flatMapThrowing { | |||
| NewSession(token: token.value, user: try user.asPublic()) | |||
| func boot(routes: any RoutesBuilder) throws { | |||
| let users = routes.grouped("users") | |||
| users.post(use: create) | |||
| } | |||
| } | |||
| fileprivate func login(req: Request) throws -> EventLoopFuture<NewSession> { | |||
| let user = try req.auth.require(User.self) | |||
| let token = try user.createToken(source: .login) | |||
| return token.save(on: req.db).flatMapThrowing { | |||
| NewSession(token: token.value, user: try user.asPublic()) | |||
| @Sendable | |||
| func create(req: Request) async throws -> Response{ | |||
| let user = try req.content.decode(User.self) | |||
| user.passwordHash = try Bcrypt.hash(user.passwordHash) | |||
| try await user.save(on: req.db) | |||
| let token = try Token.generate(for: user) | |||
| try await token.save(on: req.db) | |||
| return try await token.encodeResponse(for: req) | |||
| } | |||
| } | |||
| func getMyOwnUser(req: Request) throws -> User.Public { | |||
| try req.auth.require(User.self).asPublic() | |||
| } | |||
| private func checkIfUserExists(_ username: String, req: Request) -> EventLoopFuture<Bool> { | |||
| User.query(on: req.db) | |||
| .filter(\.$username == username) | |||
| .first() | |||
| .map { $0 != nil } | |||
| } | |||
| } | |||
| @@ -29,3 +29,23 @@ struct CreateTokens: Migration { | |||
| database.schema(Token.schema).delete() | |||
| } | |||
| } | |||
| struct UpdateTokens1: Migration { | |||
| func prepare(on database: Database) -> EventLoopFuture<Void> { | |||
| // 2 | |||
| database.schema(Token.schema) | |||
| // 3 | |||
| .deleteField("source") | |||
| .deleteField("created_at") | |||
| .deleteField("expires_at") | |||
| .update() | |||
| } | |||
| // 5 | |||
| func revert(on database: Database) -> EventLoopFuture<Void> { | |||
| database.schema(Token.schema) | |||
| .field("source", .int, .required) | |||
| .field("created_at", .datetime, .required) | |||
| .field("expires_at", .datetime) | |||
| .update() | |||
| } | |||
| } | |||
| @@ -25,3 +25,45 @@ struct CreateUser: AsyncMigration { | |||
| try await database.schema(User.schema).delete() | |||
| } | |||
| } | |||
| struct UpdateUser1: AsyncMigration{ | |||
| func prepare(on database: Database) async throws { | |||
| try await database.schema(User.schema) | |||
| .deleteField("created_at") | |||
| .deleteField("updated_at") | |||
| .update() | |||
| } | |||
| func revert(on database: Database) async throws { | |||
| try await database.schema(User.schema) | |||
| .field("created_at", .datetime, .required) | |||
| .field("updated_at", .datetime, .required) | |||
| .update() | |||
| } | |||
| } | |||
| struct UpdateUser2_AddTokenfield: AsyncMigration { | |||
| func prepare(on database: Database) async throws { | |||
| try await database.schema(User.schema) | |||
| .field("token", .string) | |||
| .update() | |||
| } | |||
| func revert(on database: Database) async throws { | |||
| try await database.schema(User.schema) | |||
| .deleteField("token") | |||
| .update() | |||
| } | |||
| } | |||
| struct UpdateUser3_RemoveTokenfield: AsyncMigration { | |||
| func prepare(on database: Database) async throws { | |||
| try await database.schema(User.schema) | |||
| .deleteField("token") | |||
| .update() | |||
| } | |||
| func revert(on database: Database) async throws { | |||
| try await database.schema(User.schema) | |||
| .field("token", .string) | |||
| .update() | |||
| } | |||
| } | |||
| @@ -8,12 +8,9 @@ | |||
| import Vapor | |||
| import Fluent | |||
| enum SessionSource: Int, Content { | |||
| case signup | |||
| case login | |||
| } | |||
| final class Token: Model { | |||
| final class Token: Model, Content, @unchecked Sendable { | |||
| static let schema = "token" | |||
| @ID() | |||
| @@ -25,36 +22,29 @@ final class Token: Model { | |||
| @Field(key: "value") | |||
| var value: String | |||
| @Field(key: "source") | |||
| var source: SessionSource | |||
| @Field(key: "expires_at") | |||
| var expiresAt: Date? | |||
| @Timestamp(key: "created_at", on: .create) | |||
| var createdAt: Date? | |||
| init() {} | |||
| init(id: UUID? = nil, userId: User.IDValue, token: String, | |||
| source: SessionSource, expiresAt: Date?) { | |||
| init(id: UUID? = nil, userId: User.IDValue, token: String) { | |||
| self.id = id | |||
| self.$user.id = userId | |||
| self.value = token | |||
| self.source = source | |||
| self.expiresAt = expiresAt | |||
| } | |||
| } | |||
| extension Token: ModelTokenAuthenticatable { | |||
| static let valueKey = \Token.$value | |||
| static let userKey = \Token.$user | |||
| var isValid: Bool { | |||
| guard let expiryDate = expiresAt else { | |||
| return true | |||
| extension Token { | |||
| static func generate(for user: User) throws -> Token { | |||
| let random = [UInt8].random(count: 32).base64 | |||
| return try Token( userId: user.requireID(), token: random) | |||
| } | |||
| } | |||
| extension Token: ModelTokenAuthenticatable { | |||
| typealias User = App.User | |||
| static let valueKey = \Token.$value | |||
| static let userKey = \Token.$user | |||
| return expiryDate > Date() | |||
| } | |||
| var isValid: Bool { | |||
| true | |||
| } | |||
| } | |||
| @@ -8,16 +8,17 @@ | |||
| import Fluent | |||
| import Vapor | |||
| final class User: Model { | |||
| struct Public: Content { | |||
| let username: String | |||
| let id: UUID | |||
| let createdAt: Date? | |||
| let updatedAt: Date? | |||
| } | |||
| final class User: Model, Content { | |||
| static let schema = "user" | |||
| final class Public: Content { | |||
| var id: UUID? | |||
| var username: String | |||
| init(id: UUID?, username: String) { | |||
| self.id = id | |||
| self.username = username | |||
| } | |||
| } | |||
| @ID() | |||
| var id: UUID? | |||
| @@ -26,12 +27,13 @@ final class User: Model { | |||
| @Field(key: "password_hash") | |||
| var passwordHash: String | |||
| @Timestamp(key: "created_at", on: .create) | |||
| var createdAt: Date? | |||
| @Timestamp(key: "updated_at", on: .update) | |||
| var updatedAt: Date? | |||
| // @Timestamp(key: "created_at", on: .create) | |||
| // var createdAt: Date? | |||
| // | |||
| // @Timestamp(key: "updated_at", on: .update) | |||
| // var updatedAt: Date? | |||
| init() {} | |||
| @@ -41,27 +43,11 @@ final class User: Model { | |||
| self.passwordHash = passwordHash | |||
| } | |||
| } | |||
| extension User { | |||
| static func create(from userSignup: UserSignup) throws -> User { | |||
| User(username: userSignup.username, passwordHash: try Bcrypt.hash(userSignup.password)) | |||
| } | |||
| func createToken(source: SessionSource) throws -> Token { | |||
| let calendar = Calendar(identifier: .gregorian) | |||
| let expiryDate = calendar.date(byAdding: .year, value: 1, to: Date()) | |||
| return try Token(userId: requireID(), | |||
| token: [UInt8].random(count: 16).base64, source: source, expiresAt: expiryDate) | |||
| } | |||
| func asPublic() throws -> Public { | |||
| Public(username: username, | |||
| id: try requireID(), | |||
| createdAt: createdAt, | |||
| updatedAt: updatedAt) | |||
| } | |||
| func convertToPublic() -> User.Public { | |||
| return User.Public(id: id, username: username) | |||
| } | |||
| } | |||
| extension User: ModelAuthenticatable { | |||
| static let usernameKey = \User.$username | |||
| static let passwordHashKey = \User.$passwordHash | |||
| @@ -70,3 +56,8 @@ extension User: ModelAuthenticatable { | |||
| try Bcrypt.verify(password, created: self.passwordHash) | |||
| } | |||
| } | |||
| @@ -19,6 +19,10 @@ public func configure(_ app: Application) async throws { | |||
| app.migrations.add(AddDownloadedBool()) | |||
| app.migrations.add(CreateUser()) | |||
| app.migrations.add(CreateTokens()) | |||
| app.migrations.add(UpdateUser1()) | |||
| app.migrations.add(UpdateTokens1()) | |||
| app.migrations.add(UpdateUser2_AddTokenfield()) | |||
| app.migrations.add(UpdateUser3_RemoveTokenfield()) | |||
| try await app.autoMigrate().get() | |||
| // register routes | |||