| @@ -216,6 +216,15 @@ | |||||
| "version" : "1.0.2" | "version" : "1.0.2" | ||||
| } | } | ||||
| }, | }, | ||||
| { | |||||
| "identity" : "swift-syntax", | |||||
| "kind" : "remoteSourceControl", | |||||
| "location" : "https://github.com/apple/swift-syntax.git", | |||||
| "state" : { | |||||
| "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", | |||||
| "version" : "509.1.1" | |||||
| } | |||||
| }, | |||||
| { | { | ||||
| "identity" : "swift-system", | "identity" : "swift-system", | ||||
| "kind" : "remoteSourceControl", | "kind" : "remoteSourceControl", | ||||
| @@ -225,6 +234,15 @@ | |||||
| "version" : "1.2.1" | "version" : "1.2.1" | ||||
| } | } | ||||
| }, | }, | ||||
| { | |||||
| "identity" : "swiftopenapi", | |||||
| "kind" : "remoteSourceControl", | |||||
| "location" : "https://github.com/dankinsoid/SwiftOpenAPI.git", | |||||
| "state" : { | |||||
| "revision" : "d4ac9379e1f7071844deda554c247c8cb13823ce", | |||||
| "version" : "2.20.0" | |||||
| } | |||||
| }, | |||||
| { | { | ||||
| "identity" : "vapor", | "identity" : "vapor", | ||||
| "kind" : "remoteSourceControl", | "kind" : "remoteSourceControl", | ||||
| @@ -234,6 +252,15 @@ | |||||
| "version" : "4.92.1" | "version" : "4.92.1" | ||||
| } | } | ||||
| }, | }, | ||||
| { | |||||
| "identity" : "vaportoopenapi", | |||||
| "kind" : "remoteSourceControl", | |||||
| "location" : "https://github.com/dankinsoid/VaporToOpenAPI.git", | |||||
| "state" : { | |||||
| "revision" : "5bf8c3fc73a42f0dd8c1ae2928fb5817026fa69e", | |||||
| "version" : "4.5.0" | |||||
| } | |||||
| }, | |||||
| { | { | ||||
| "identity" : "websocket-kit", | "identity" : "websocket-kit", | ||||
| "kind" : "remoteSourceControl", | "kind" : "remoteSourceControl", | ||||
| @@ -13,6 +13,7 @@ let package = Package( | |||||
| .package(url: "https://github.com/vapor/fluent.git", from: "4.8.0"), | .package(url: "https://github.com/vapor/fluent.git", from: "4.8.0"), | ||||
| // 🐘 Fluent driver for Postgres. | // 🐘 Fluent driver for Postgres. | ||||
| .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"), | .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"), | ||||
| .package(url: "https://github.com/dankinsoid/VaporToOpenAPI.git", from: "4.5.0"), | |||||
| ], | ], | ||||
| targets: [ | targets: [ | ||||
| .executableTarget( | .executableTarget( | ||||
| @@ -21,6 +22,7 @@ let package = Package( | |||||
| .product(name: "Fluent", package: "fluent"), | .product(name: "Fluent", package: "fluent"), | ||||
| .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), | .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), | ||||
| .product(name: "Vapor", package: "vapor"), | .product(name: "Vapor", package: "vapor"), | ||||
| .product(name: "VaporToOpenAPI", package: "vaportoopenapi"), | |||||
| ] | ] | ||||
| ), | ), | ||||
| .testTarget(name: "AppTests", dependencies: [ | .testTarget(name: "AppTests", dependencies: [ | ||||
| @@ -0,0 +1,16 @@ | |||||
| html { | |||||
| box-sizing: border-box; | |||||
| overflow: -moz-scrollbars-vertical; | |||||
| overflow-y: scroll; | |||||
| } | |||||
| *, | |||||
| *:before, | |||||
| *:after { | |||||
| box-sizing: inherit; | |||||
| } | |||||
| body { | |||||
| margin: 0; | |||||
| background: #fafafa; | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| <!-- HTML for static distribution bundle build --> | |||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Swagger UI</title> | |||||
| <link rel="stylesheet" type="text/css" href="./swagger-ui.css" /> | |||||
| <link rel="stylesheet" type="text/css" href="index.css" /> | |||||
| <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" /> | |||||
| <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" /> | |||||
| </head> | |||||
| <body> | |||||
| <div id="swagger-ui"></div> | |||||
| <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script> | |||||
| <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script> | |||||
| <script src="./swagger-initializer.js" charset="UTF-8"> </script> | |||||
| </body> | |||||
| </html> | |||||
| @@ -0,0 +1,79 @@ | |||||
| <!doctype html> | |||||
| <html lang="en-US"> | |||||
| <head> | |||||
| <title>Swagger UI: OAuth2 Redirect</title> | |||||
| </head> | |||||
| <body> | |||||
| <script> | |||||
| 'use strict'; | |||||
| function run () { | |||||
| var oauth2 = window.opener.swaggerUIRedirectOauth2; | |||||
| var sentState = oauth2.state; | |||||
| var redirectUrl = oauth2.redirectUrl; | |||||
| var isValid, qp, arr; | |||||
| if (/code|token|error/.test(window.location.hash)) { | |||||
| qp = window.location.hash.substring(1).replace('?', '&'); | |||||
| } else { | |||||
| qp = location.search.substring(1); | |||||
| } | |||||
| arr = qp.split("&"); | |||||
| arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';}); | |||||
| qp = qp ? JSON.parse('{' + arr.join() + '}', | |||||
| function (key, value) { | |||||
| return key === "" ? value : decodeURIComponent(value); | |||||
| } | |||||
| ) : {}; | |||||
| isValid = qp.state === sentState; | |||||
| if (( | |||||
| oauth2.auth.schema.get("flow") === "accessCode" || | |||||
| oauth2.auth.schema.get("flow") === "authorizationCode" || | |||||
| oauth2.auth.schema.get("flow") === "authorization_code" | |||||
| ) && !oauth2.auth.code) { | |||||
| if (!isValid) { | |||||
| oauth2.errCb({ | |||||
| authId: oauth2.auth.name, | |||||
| source: "auth", | |||||
| level: "warning", | |||||
| message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server." | |||||
| }); | |||||
| } | |||||
| if (qp.code) { | |||||
| delete oauth2.state; | |||||
| oauth2.auth.code = qp.code; | |||||
| oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl}); | |||||
| } else { | |||||
| let oauthErrorMsg; | |||||
| if (qp.error) { | |||||
| oauthErrorMsg = "["+qp.error+"]: " + | |||||
| (qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") + | |||||
| (qp.error_uri ? "More info: "+qp.error_uri : ""); | |||||
| } | |||||
| oauth2.errCb({ | |||||
| authId: oauth2.auth.name, | |||||
| source: "auth", | |||||
| level: "error", | |||||
| message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server." | |||||
| }); | |||||
| } | |||||
| } else { | |||||
| oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl}); | |||||
| } | |||||
| window.close(); | |||||
| } | |||||
| if (document.readyState !== 'loading') { | |||||
| run(); | |||||
| } else { | |||||
| document.addEventListener('DOMContentLoaded', function () { | |||||
| run(); | |||||
| }); | |||||
| } | |||||
| </script> | |||||
| </body> | |||||
| </html> | |||||
| @@ -0,0 +1,20 @@ | |||||
| window.onload = function() { | |||||
| //<editor-fold desc="Changeable Configuration Block"> | |||||
| // the following lines will be replaced by docker/configurator, when it runs in a docker-container | |||||
| window.ui = SwaggerUIBundle({ | |||||
| url: "https://petstore.swagger.io/v2/swagger.json", | |||||
| dom_id: '#swagger-ui', | |||||
| deepLinking: true, | |||||
| presets: [ | |||||
| SwaggerUIBundle.presets.apis, | |||||
| SwaggerUIStandalonePreset | |||||
| ], | |||||
| plugins: [ | |||||
| SwaggerUIBundle.plugins.DownloadUrl | |||||
| ], | |||||
| layout: "StandaloneLayout" | |||||
| }); | |||||
| //</editor-fold> | |||||
| }; | |||||
| @@ -84,4 +84,19 @@ struct cntrlUser: RouteCollection { | |||||
| } | } | ||||
| return response | return response | ||||
| } | } | ||||
| func createToken(req: Request) async throws -> Response{ | |||||
| guard let obj = try? await mdlUser.ObjQuery(req: req).field(\.$id).field(\.$username).first() else{ | |||||
| throw Abort(.notFound, reason: "User was not found.") | |||||
| } | |||||
| guard let exp = try? await mdlLicense.query(on: req.db).first() else { | |||||
| throw Abort(.notFound) | |||||
| } | |||||
| let token = Token(userid: obj.id!, token: [UInt8].random(count: 16).base64, source: .signup, expiresat: exp.expires) | |||||
| 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 has been deleted.") | |||||
| } | |||||
| return response | |||||
| } | |||||
| } | } | ||||
| @@ -0,0 +1,55 @@ | |||||
| // | |||||
| // Token.swift | |||||
| // | |||||
| // | |||||
| // Created by Michiel Carman on 4/5/24. | |||||
| // | |||||
| import Vapor | |||||
| import Fluent | |||||
| enum SessionSource: Int, Content { | |||||
| case signup | |||||
| case login | |||||
| } | |||||
| final class Token: Model, Content{ | |||||
| static let schema = "token" | |||||
| @ID(custom: "id", generatedBy: .database) var id: Int? | |||||
| @Parent(key: "userId") var user: mdlUser | |||||
| @Field(key: "value") var value: String | |||||
| @Field(key: "source") var source: SessionSource | |||||
| @Field(key: "expiresAt") var expiresat: Date? | |||||
| @Timestamp(key: "createdAt", on: .create) var createdat: Date? | |||||
| init(){} | |||||
| init(id: Int? = nil, userid: mdlUser.IDValue, token: String, | |||||
| source: SessionSource, expiresat: Date?) { | |||||
| self.id = id | |||||
| self.$user.id = userid | |||||
| self.value = token | |||||
| self.source = source | |||||
| self.expiresat = expiresat | |||||
| } | |||||
| } | |||||
| extension Token{ | |||||
| struct Create: AsyncMigration { | |||||
| func prepare(on database: Database) async throws { | |||||
| try await database.schema(Token.schema) | |||||
| .field("id", .int, .identifier(auto: true)) | |||||
| .field("userId", .int, .references("user", "id")) | |||||
| .field("value", .string, .required).unique(on: "value") | |||||
| .field("source", .int, .required) | |||||
| .field("createdAt", .datetime, .required) | |||||
| .field("expiresAt", .datetime) | |||||
| .create() | |||||
| } | |||||
| // 5 | |||||
| func revert(on database: Database) async throws { | |||||
| try await database.schema(Token.schema).delete() | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| // | |||||
| // mdlLicense.swift | |||||
| // | |||||
| // | |||||
| // Created by Michiel Carman on 4/5/24. | |||||
| // | |||||
| import Vapor | |||||
| import Fluent | |||||
| final class mdlLicense: Model{ | |||||
| static let schema = "license" | |||||
| @ID(custom: "id", generatedBy: .database) var id: Int? | |||||
| @Field(key: "expires") var expires: Date | |||||
| init(){} | |||||
| init(id: Int?, expires: Date){ | |||||
| self.id = id! | |||||
| self.expires = expires | |||||
| } | |||||
| } | |||||
| extension mdlLicense{ | |||||
| struct Create: AsyncMigration{ | |||||
| func prepare(on database: FluentKit.Database) async throws { | |||||
| try await database.schema(mdlLicense.schema) | |||||
| .field("id", .int, .identifier(auto: true)) | |||||
| .field("expires", .datetime) | |||||
| .create() | |||||
| } | |||||
| func revert(on database: FluentKit.Database) async throws { | |||||
| try await database.schema(mdlLicense.schema) | |||||
| .delete() | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -138,6 +138,8 @@ public func configure(_ app: Application) async throws { | |||||
| app.migrations.add(mdlTemplate.Mod1()) | app.migrations.add(mdlTemplate.Mod1()) | ||||
| app.migrations.add(mdlTemplateCategory.Create()) | app.migrations.add(mdlTemplateCategory.Create()) | ||||
| app.migrations.add(mdlTemplateItemSpec.Create()) | app.migrations.add(mdlTemplateItemSpec.Create()) | ||||
| app.migrations.add(Token.Create()) | |||||
| app.migrations.add(mdlLicense.Create()) | |||||
| app.logger.logLevel = .debug | app.logger.logLevel = .debug | ||||
| @@ -4,10 +4,7 @@ import Crypto | |||||
| let | let | ||||
| encryptionKey = | |||||
| SymmetricKey(data: | |||||
| Environment.get("ENCRYPTION_KEY")!.data(using: | |||||
| .utf8)!) | |||||
| encryptionKey = SymmetricKey(data: Environment.get("ENCRYPTION_KEY")!.data(using: .utf8)!) | |||||
| func routes(_ app: Application) throws { | func routes(_ app: Application) throws { | ||||
| app.routes.caseInsensitive = true | app.routes.caseInsensitive = true | ||||
| @@ -1,8 +1,66 @@ | |||||
| // | |||||
| // File.swift | |||||
| // | |||||
| // | |||||
| // Created by Michiel Carman on 4/5/24. | |||||
| // | |||||
| @testable import App | |||||
| import Fluent | |||||
| import XCTVapor | |||||
| import Foundation | |||||
| final class RoleTests: XCTestCase { | |||||
| func test1() async throws{ | |||||
| let app = Application(.testing) | |||||
| defer{app.shutdown()} | |||||
| try await configure(app) | |||||
| let basepath = "/api/v2.0/roles/" | |||||
| let name = "testRole" | |||||
| let description = "Testing Role methods" | |||||
| let cuserid = -1 | |||||
| let uuserid = -1 | |||||
| //Insert object | |||||
| try app.test(.POST, basepath, beforeRequest: { | |||||
| req in | |||||
| let input = mdlRole(id: nil, name: name, description: description, createuserid: cuserid, updateuserid: uuserid) | |||||
| try req.content.encode(input) | |||||
| }, afterResponse: { | |||||
| res in | |||||
| XCTAssertEqual(res.status, .created) | |||||
| let obj = try res.content.decode(mdlRole.self) | |||||
| XCTAssertEqual(obj.name, name) | |||||
| let id = obj.id | |||||
| //Update object just created | |||||
| try app.test(.POST, basepath, beforeRequest:{ | |||||
| req in | |||||
| let input = mdlRole(id: nil, name: name, description: "Test Update", createuserid: cuserid, updateuserid: uuserid) | |||||
| }, afterResponse: { | |||||
| res in | |||||
| XCTAssertEqual(res.status, .accepted) | |||||
| let obj = try res.content.decode(mdlRole.self) | |||||
| XCTAssertEqual(obj.name, name) | |||||
| }) | |||||
| //Read all objects in the table | |||||
| try app.test(.GET, basepath, afterResponse: { res in | |||||
| XCTAssertEqual(res.status, .ok) | |||||
| XCTAssertEqual(res.headers.contentType, .json) | |||||
| _ = try res.content.decode([mdlRole].self) | |||||
| }) | |||||
| //Read the object just inserted | |||||
| var parameter = try CustomCrypto.EncryptString(input: String(id!)) | |||||
| var path = "\(basepath)\(parameter)" | |||||
| try app.test(.GET, path, afterResponse: { res in | |||||
| XCTAssertEqual(res.status, .ok) | |||||
| XCTAssertEqual(res.headers.contentType, .json) | |||||
| _ = try res.content.decode(mdlRole.self) | |||||
| }) | |||||
| //Delete object just inserted | |||||
| let encryptedid = try CustomCrypto.EncryptString(input: String(id!)) | |||||
| path = "api/v2.0/role/" | |||||
| let url = "\(path)\(encryptedid)" | |||||
| try app.test(.DELETE, url, afterResponse: { | |||||
| res in | |||||
| XCTAssertEqual(res.status, .ok) | |||||
| _ = try res.content.decode(mdlRole.self) | |||||
| }) | |||||
| }) | |||||
| } | |||||
| } | |||||
| @@ -101,14 +101,14 @@ final class UserTests: XCTestCase { | |||||
| }) | }) | ||||
| //delete user | //delete user | ||||
| // let encryptedid = try CustomCrypto.EncryptString(input: String(id!)) | |||||
| // path = "api/v2.0/users/" | |||||
| // let url = "\(path)\(encryptedid)" | |||||
| // try app.test(.DELETE, url, afterResponse: { | |||||
| // res in | |||||
| // XCTAssertEqual(res.status, .ok) | |||||
| // _ = try res.content.decode(mdlUser.self) | |||||
| // }) | |||||
| let encryptedid = try CustomCrypto.EncryptString(input: String(id!)) | |||||
| path = "api/v2.0/users/" | |||||
| let url = "\(path)\(encryptedid)" | |||||
| try app.test(.DELETE, url, afterResponse: { | |||||
| res in | |||||
| XCTAssertEqual(res.status, .ok) | |||||
| _ = try res.content.decode(mdlUser.self) | |||||
| }) | |||||
| }) | }) | ||||
| } | } | ||||