| @@ -18,9 +18,69 @@ struct cntrlAssociate: RouteCollection { | |||
| rts.get(":x", use: read) | |||
| rts.post(use: create) | |||
| rts.delete(":x", use: delete) | |||
| rts.post("create", use: createUser) | |||
| let passwordProtected = rts.grouped(mdlAssociate.authenticator(), mdlAssociate.guardMiddleware()) | |||
| passwordProtected.post("login", use: login) | |||
| passwordProtected.post("tlogin", use: jwtauth) | |||
| let tokenProtected = rts.grouped(mdlToken.authenticator()) | |||
| tokenProtected.get("tauth", use: auth) | |||
| //Additional routs added here | |||
| } | |||
| @Sendable | |||
| func jwtauth(req: Request) async throws -> ClientTokenResponse{ | |||
| let user = try req.auth.require(mdlAssociate.self) | |||
| let payload = SessionToken(user: user) | |||
| return ClientTokenResponse(token: try req.jwt.sign(payload)) | |||
| } | |||
| @Sendable | |||
| func auth(req: Request) async throws -> Response{ | |||
| let result = try req.auth.require(mdlAssociate.self) | |||
| let assoc = mdlAssociate.Output(id: result.id, firstname: result.firstname, lastname: result.lastname) | |||
| guard let response = try? await assoc.encodeResponse(status: .ok, for: req) else{ | |||
| throw Abort(.noContent, reason: "Something went wrong decoding data. Associate could not be shown.") | |||
| } | |||
| return response | |||
| } | |||
| @Sendable | |||
| func login(req: Request) async throws -> Response{ | |||
| guard let result = try? req.auth.require(mdlAssociate.self) else{ | |||
| throw Abort(.unauthorized) | |||
| } | |||
| let token = try await result.generateToken() | |||
| try await token.save(on: req.db) | |||
| guard let response = try? await token.encodeResponse(status: .ok, for: req) else{ | |||
| throw Abort(.noContent, reason: "Something went wrong decoding data. Token could not be shown.") | |||
| } | |||
| return response | |||
| } | |||
| @Sendable | |||
| func createUser(req: Request) async throws -> Response{ | |||
| try mdlAssociate.CreateUser.validate(content: req) | |||
| let create = try req.content.decode(mdlAssociate.CreateUser.self) | |||
| guard create.password == create.confirmpassword else{ | |||
| throw Abort(.badRequest, reason: "Passwords did not match") | |||
| } | |||
| let user = try mdlAssociate( | |||
| firstname: create.firstname, | |||
| lastname: create.lastname, | |||
| username: create.username, | |||
| password: Bcrypt.hash(create.password) | |||
| ) | |||
| try await user.save(on: req.db) | |||
| let assoc = mdlAssociate.Output(id: user.id, firstname: user.firstname, lastname: user.lastname) | |||
| guard let response = try? await assoc.encodeResponse(status: .ok, for: req) else{ | |||
| throw Abort(.noContent, reason: "Something went wrong decoding data. Associate could not be shown.") | |||
| } | |||
| return response | |||
| } | |||
| @Sendable | |||
| func index(req: Request) async throws -> Response { | |||
| let obj = try await mdlAssociate.ObjQuery(req: req).all() | |||
| @@ -66,6 +126,8 @@ struct cntrlAssociate: RouteCollection { | |||
| } | |||
| @Sendable | |||
| func delete(req: Request) async throws -> Response { | |||
| guard let obj = try? await mdlAssociate.ObjQuery(req: req).first() else { | |||
| @@ -20,6 +20,7 @@ public func doMigrations(_ app: Application) async throws{ | |||
| app.migrations.add(mdlXAssocDept.Create()) | |||
| app.migrations.add(mdlXDocDept.Create()) | |||
| app.migrations.add(mdlDocument.tmpField()) | |||
| app.migrations.add(mdlAssociate.Mod1()) | |||
| try await app.autoMigrate() | |||
| } | |||
| @@ -10,15 +10,16 @@ import Fluent | |||
| import Vapor | |||
| import Foundation | |||
| import FluentKit | |||
| import JWT | |||
| final class mdlAssociate: Model, Content { | |||
| static let schema = "associate" | |||
| @ID(custom: "id", generatedBy: .database) var id: Int? | |||
| @Field(key: "firstName") var firstname: String? | |||
| @Field(key: "lastName") var lastname: String? | |||
| @Field(key: "firstName") var firstname: String | |||
| @Field(key: "lastName") var lastname: String | |||
| @Field(key: "userName") var username: String | |||
| @Field(key: "password") var password: String | |||
| @Siblings(through: mdlXAssocDept.self, from: \.$associate, to: \.$department) var departments: [mdlDepartment] | |||
| @@ -30,34 +31,31 @@ final class mdlAssociate: Model, Content { | |||
| init() { } | |||
| init(id: Int? = nil, | |||
| firstname: String? = nil, | |||
| lastname: String? = nil, | |||
| firstname: String, | |||
| lastname: String, | |||
| username: String, | |||
| password: String , | |||
| createuserid: Int? = nil, | |||
| updateuserid: Int? = nil | |||
| ) { | |||
| self.id = id | |||
| self.firstname = firstname | |||
| self.lastname = lastname | |||
| self.username = username | |||
| self.password = password | |||
| self.createuserid = createuserid | |||
| self.updateuserid = updateuserid | |||
| } | |||
| } | |||
| extension mdlAssociate{ | |||
| struct User: JWTPayload{ | |||
| var id: Int | |||
| var email: String | |||
| var password: String | |||
| func verify(using signer: JWTSigner) throws{ | |||
| } | |||
| } | |||
| struct Create: AsyncMigration { | |||
| func prepare(on database: Database) async throws { | |||
| try await database.schema("associate") | |||
| .field("id", .int, .identifier(auto: true)) | |||
| .field("firstName", .string) | |||
| .field("lastName", .string) | |||
| // .field("userName", .string) | |||
| // .field("password", .string) | |||
| .field("createDate", .datetime) | |||
| .field("createUserId", .int) | |||
| .field("updateDate", .datetime) | |||
| @@ -69,6 +67,21 @@ extension mdlAssociate{ | |||
| try await database.schema("associate").delete() | |||
| } | |||
| } | |||
| struct Mod1: AsyncMigration{ | |||
| func prepare(on database: Database) async throws { | |||
| try await database.schema("associate") | |||
| .field("userName", .string).unique(on: "userName") | |||
| .field("password", .string) | |||
| .update() | |||
| } | |||
| func revert(on database: Database) async throws { | |||
| try await database.schema("associate") | |||
| .deleteField("userName") | |||
| .deleteField("password") | |||
| .update() | |||
| } | |||
| } | |||
| public static func ObjQuery(req: Request) async throws -> QueryBuilder<mdlAssociate>{ | |||
| let query = mdlAssociate.query(on: req.db) | |||
| @@ -107,4 +120,44 @@ extension mdlAssociate{ | |||
| } | |||
| } | |||
| } | |||
| extension mdlAssociate{ | |||
| struct CreateUser: Content{ | |||
| var username: String | |||
| var password: String | |||
| var confirmpassword: String | |||
| var firstname: String | |||
| var lastname: String | |||
| } | |||
| struct Output: Content{ | |||
| var id: Int? | |||
| var firstname: String | |||
| var lastname: String | |||
| } | |||
| } | |||
| extension mdlAssociate.CreateUser: Validatable{ | |||
| static func validations(_ validations: inout Validations){ | |||
| validations.add("firstname", as: String.self, is: !.empty) | |||
| validations.add("lastname", as: String.self, is: !.empty) | |||
| validations.add("username", as: String.self, is: !.empty) | |||
| validations.add("password", as: String.self, is: .count(8...)) | |||
| } | |||
| } | |||
| extension mdlAssociate: ModelAuthenticatable{ | |||
| static let usernameKey = \mdlAssociate.$username | |||
| static var passwordHashKey = \mdlAssociate.$password | |||
| func verify(password: String) throws -> Bool { | |||
| try Bcrypt.verify(password, created: self.password) | |||
| } | |||
| } | |||
| extension mdlAssociate{ | |||
| func generateToken() async throws -> mdlToken { | |||
| try .init( | |||
| value: [UInt8].random(count: 16).base64, | |||
| userid: self.requireID() | |||
| ) | |||
| } | |||
| } | |||
| @@ -5,4 +5,32 @@ | |||
| // Created by Michiel Carman on 4/16/24. | |||
| // | |||
| import Foundation | |||
| import Vapor | |||
| import Fluent | |||
| import JWT | |||
| struct SessionToken: Content, Authenticatable, JWTPayload{ | |||
| var expirationtime: TimeInterval = 60 * 15 | |||
| var expiration: ExpirationClaim | |||
| var userid: Int | |||
| init(userid: Int){ | |||
| self.userid = userid | |||
| self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationtime)) | |||
| } | |||
| init(user: mdlAssociate){ | |||
| self.userid = try! user.requireID() | |||
| self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationtime)) | |||
| } | |||
| func verify(using signer: JWTKit.JWTSigner) throws { | |||
| try expiration.verifyNotExpired() | |||
| } | |||
| } | |||
| struct ClientTokenResponse: Content{ | |||
| var token: String | |||
| } | |||
| @@ -5,4 +5,43 @@ | |||
| // Created by Michiel Carman on 4/16/24. | |||
| // | |||
| import Foundation | |||
| import Fluent | |||
| import Vapor | |||
| final class mdlToken: Model, Content{ | |||
| static let schema = "user_token" | |||
| @ID(custom: "id", generatedBy: .database) var id: Int? | |||
| @Field(key: "value") var value: String | |||
| @Parent(key: "associateId") var user: mdlAssociate | |||
| init(){} | |||
| init(id: Int? = nil, value: String, userid: mdlAssociate.IDValue){ | |||
| self.id = id | |||
| self.value = value | |||
| self.$user.id = userid | |||
| } | |||
| } | |||
| extension mdlToken{ | |||
| struct Create: AsyncMigration { | |||
| var name: String { "CreateUserToken"} | |||
| func prepare(on database: Database) async throws { | |||
| try await database.schema("user_token") | |||
| .field("id", .int, .identifier(auto: true)) | |||
| .field("value", .string) | |||
| .field("associateId", .int, .required, .references("associate", "id")) | |||
| .unique(on: "value") | |||
| .create() | |||
| } | |||
| func revert(on database: Database) async throws { | |||
| try await database.schema("user_token").delete() | |||
| } | |||
| } | |||
| } | |||
| extension mdlToken: ModelTokenAuthenticatable{ | |||
| static let valueKey = \mdlToken.$value | |||
| static let userKey = \mdlToken.$user | |||
| var isValid: Bool{true} | |||
| } | |||
| @@ -2,19 +2,23 @@ import NIOSSL | |||
| import Fluent | |||
| import FluentPostgresDriver | |||
| import Vapor | |||
| //import JWT | |||
| extension String { | |||
| var bytes: [UInt8] { .init(self.utf8) } | |||
| } | |||
| //extension JWKIdentifier{ | |||
| // static let `public` = JWKIdentifier(string: "public") | |||
| // static let `private` = JWKIdentifier(string: "private") | |||
| //} | |||
| // configures your application | |||
| public func configure(_ app: Application) async throws { | |||
| // let privateKey = try String(contentsOfFile: app.directory.workingDirectory + "jwtRS256.key") | |||
| // let privateKey = try String(contentsOfFile: app.directory.workingDirectory + "jwtRS256.key").replacingOccurrences(of: "\\n", with: "\n") | |||
| // let privateSigner = try JWTSigner.rs256(key: .private(pem: privateKey.bytes)) | |||
| // | |||
| // let publicKey = try String(contentsOfFile: app.directory.workingDirectory + "jwtRS256.key.pub") | |||
| // let publicKey = try String(contentsOfFile: app.directory.workingDirectory + "jwtRS256.key.pub").replacingOccurrences(of: "\\n", with: "\n") | |||
| // let publicSigner = try JWTSigner.rs256(key: .public(pem: publicKey.bytes)) | |||
| // | |||
| // app.jwt.signers.use(privateSigner, kid: .private) | |||
| @@ -2,6 +2,13 @@ import Foundation | |||
| import Fluent | |||
| import Vapor | |||
| import Crypto | |||
| //import JWT | |||
| //struct Example: JWTPayload, Content{ | |||
| // | |||
| // var test: String | |||
| // func verify(using signer: JWTSigner) throws{} | |||
| //} | |||
| func routes(_ app: Application) throws { | |||
| app.routes.caseInsensitive = true | |||
| @@ -11,6 +18,7 @@ func routes(_ app: Application) throws { | |||
| baseApp.get { req async in | |||
| "Training Services Version 1.0" | |||
| } | |||
| try baseApp.register(collection: cntrlAssociate()) | |||
| try baseApp.register(collection: cntrlDepartment()) | |||
| try baseApp.register(collection: cntrlDocument()) | |||
| @@ -0,0 +1,51 @@ | |||
| -----BEGIN RSA PRIVATE KEY----- | |||
| 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 | |||
| -----END RSA PRIVATE KEY----- | |||
| @@ -0,0 +1,14 @@ | |||
| -----BEGIN PUBLIC KEY----- | |||
| MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx0IhjUkXs2RSz7bhcrIS | |||
| g6u+So5EdyLN/3gQK8N5CFshS9n2Div8QYBWTsJU/vJ8qn1g8jrZ0PCpEbvfTZVH | |||
| s3Lb5N4USSUHe+JpyFup1EnOzTAxAu4OoOSnnoiBIHv0gwunFE9lH1ey/p/ro+uB | |||
| z9iIBmwtn+4xGPkcZ4QPeKOUudULZEQ3DK7MNF9rcCAyts/xyrQ4mHcpHM4r8SeW | |||
| t4aiblWBkdQqhQdKbUQbyrfpv3AZGVRoUjYkIfvp5BiCkZADj/knhxxvdgKNv9D8 | |||
| hOhkQ69H6uFk4jT1QsRa0GuKPCWfZQOxdCCpk65PlrrAXryWWPMfywry/vuMCLUJ | |||
| JiUVjXoYIKOzz8GNf214RhmHuW1baNDgkLV8EQ5GfFcWd92Tb/04eH9TI3M7iSGa | |||
| IShRCbRJ8szFSzGCGOh2DNGXfj1BPixlFP1s/Vdin9UQMXMZpQnw1IgNdMksJ8hG | |||
| MH2qxo6PrJvxIgjlW1F0/m9CT9mj2GIkYA5CwPej0VidkUgqFOeY8fGXw82YP44j | |||
| AHpOO3cmC2zPNxqUN09caJj/BaNQD9h0mxyW/t6xVuvZzN/TmUFeZ8UOgxnqCsu9 | |||
| QFrah4S9JN/7ZssYI+PrWaeYYdM9+8QJdNzG89G+Krcfez4ZXD8RmarpMpu9Ibxn | |||
| 4tLwQKJUg3+i15IQYem2pEUCAwEAAQ== | |||
| -----END PUBLIC KEY----- | |||