| @@ -18,9 +18,69 @@ struct cntrlAssociate: RouteCollection { | |||||
| rts.get(":x", use: read) | rts.get(":x", use: read) | ||||
| rts.post(use: create) | rts.post(use: create) | ||||
| rts.delete(":x", use: delete) | 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 | //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 | @Sendable | ||||
| func index(req: Request) async throws -> Response { | func index(req: Request) async throws -> Response { | ||||
| let obj = try await mdlAssociate.ObjQuery(req: req).all() | let obj = try await mdlAssociate.ObjQuery(req: req).all() | ||||
| @@ -66,6 +126,8 @@ struct cntrlAssociate: RouteCollection { | |||||
| } | } | ||||
| @Sendable | @Sendable | ||||
| func delete(req: Request) async throws -> Response { | func delete(req: Request) async throws -> Response { | ||||
| guard let obj = try? await mdlAssociate.ObjQuery(req: req).first() else { | 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(mdlXAssocDept.Create()) | ||||
| app.migrations.add(mdlXDocDept.Create()) | app.migrations.add(mdlXDocDept.Create()) | ||||
| app.migrations.add(mdlDocument.tmpField()) | app.migrations.add(mdlDocument.tmpField()) | ||||
| app.migrations.add(mdlAssociate.Mod1()) | |||||
| try await app.autoMigrate() | try await app.autoMigrate() | ||||
| } | } | ||||
| @@ -10,15 +10,16 @@ import Fluent | |||||
| import Vapor | import Vapor | ||||
| import Foundation | import Foundation | ||||
| import FluentKit | import FluentKit | ||||
| import JWT | |||||
| final class mdlAssociate: Model, Content { | final class mdlAssociate: Model, Content { | ||||
| static let schema = "associate" | static let schema = "associate" | ||||
| @ID(custom: "id", generatedBy: .database) var id: Int? | @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] | @Siblings(through: mdlXAssocDept.self, from: \.$associate, to: \.$department) var departments: [mdlDepartment] | ||||
| @@ -30,34 +31,31 @@ final class mdlAssociate: Model, Content { | |||||
| init() { } | init() { } | ||||
| init(id: Int? = nil, | init(id: Int? = nil, | ||||
| firstname: String? = nil, | |||||
| lastname: String? = nil, | |||||
| firstname: String, | |||||
| lastname: String, | |||||
| username: String, | |||||
| password: String , | |||||
| createuserid: Int? = nil, | createuserid: Int? = nil, | ||||
| updateuserid: Int? = nil | updateuserid: Int? = nil | ||||
| ) { | ) { | ||||
| self.id = id | self.id = id | ||||
| self.firstname = firstname | self.firstname = firstname | ||||
| self.lastname = lastname | self.lastname = lastname | ||||
| self.username = username | |||||
| self.password = password | |||||
| self.createuserid = createuserid | self.createuserid = createuserid | ||||
| self.updateuserid = updateuserid | self.updateuserid = updateuserid | ||||
| } | } | ||||
| } | } | ||||
| extension mdlAssociate{ | extension mdlAssociate{ | ||||
| struct User: JWTPayload{ | |||||
| var id: Int | |||||
| var email: String | |||||
| var password: String | |||||
| func verify(using signer: JWTSigner) throws{ | |||||
| } | |||||
| } | |||||
| struct Create: AsyncMigration { | struct Create: AsyncMigration { | ||||
| func prepare(on database: Database) async throws { | func prepare(on database: Database) async throws { | ||||
| try await database.schema("associate") | try await database.schema("associate") | ||||
| .field("id", .int, .identifier(auto: true)) | .field("id", .int, .identifier(auto: true)) | ||||
| .field("firstName", .string) | .field("firstName", .string) | ||||
| .field("lastName", .string) | .field("lastName", .string) | ||||
| // .field("userName", .string) | |||||
| // .field("password", .string) | |||||
| .field("createDate", .datetime) | .field("createDate", .datetime) | ||||
| .field("createUserId", .int) | .field("createUserId", .int) | ||||
| .field("updateDate", .datetime) | .field("updateDate", .datetime) | ||||
| @@ -69,6 +67,21 @@ extension mdlAssociate{ | |||||
| try await database.schema("associate").delete() | 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>{ | public static func ObjQuery(req: Request) async throws -> QueryBuilder<mdlAssociate>{ | ||||
| let query = mdlAssociate.query(on: req.db) | 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. | // 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. | // 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 Fluent | ||||
| import FluentPostgresDriver | import FluentPostgresDriver | ||||
| import Vapor | import Vapor | ||||
| //import JWT | |||||
| extension String { | extension String { | ||||
| var bytes: [UInt8] { .init(self.utf8) } | var bytes: [UInt8] { .init(self.utf8) } | ||||
| } | } | ||||
| //extension JWKIdentifier{ | |||||
| // static let `public` = JWKIdentifier(string: "public") | |||||
| // static let `private` = JWKIdentifier(string: "private") | |||||
| //} | |||||
| // configures your application | // configures your application | ||||
| public func configure(_ app: Application) async throws { | 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 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)) | // let publicSigner = try JWTSigner.rs256(key: .public(pem: publicKey.bytes)) | ||||
| // | // | ||||
| // app.jwt.signers.use(privateSigner, kid: .private) | // app.jwt.signers.use(privateSigner, kid: .private) | ||||
| @@ -2,6 +2,13 @@ import Foundation | |||||
| import Fluent | import Fluent | ||||
| import Vapor | import Vapor | ||||
| import Crypto | import Crypto | ||||
| //import JWT | |||||
| //struct Example: JWTPayload, Content{ | |||||
| // | |||||
| // var test: String | |||||
| // func verify(using signer: JWTSigner) throws{} | |||||
| //} | |||||
| func routes(_ app: Application) throws { | func routes(_ app: Application) throws { | ||||
| app.routes.caseInsensitive = true | app.routes.caseInsensitive = true | ||||
| @@ -11,6 +18,7 @@ func routes(_ app: Application) throws { | |||||
| baseApp.get { req async in | baseApp.get { req async in | ||||
| "Training Services Version 1.0" | "Training Services Version 1.0" | ||||
| } | } | ||||
| try baseApp.register(collection: cntrlAssociate()) | try baseApp.register(collection: cntrlAssociate()) | ||||
| try baseApp.register(collection: cntrlDepartment()) | try baseApp.register(collection: cntrlDepartment()) | ||||
| try baseApp.register(collection: cntrlDocument()) | 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----- | |||||