├── .gitignore ├── Dockerfile ├── Jenkinsfile ├── LICENSE ├── Makefile ├── Other ├── default-logo-512.png ├── default-logo.png └── logo.png ├── Package.resolved ├── Package.swift ├── README.md ├── Resources ├── Templates │ ├── email.invitation.html.leaf │ ├── email.invitation.plain.leaf │ ├── email.password-recovery.html.leaf │ ├── email.password-recovery.plain.leaf │ ├── email.registration.html.leaf │ └── email.registration.plain.leaf └── commit.txt ├── Sources ├── ApiCore │ ├── ApiCoreBase.swift │ ├── Auth │ │ ├── Generic │ │ │ ├── Auth.swift │ │ │ ├── Extensions │ │ │ │ └── URL+Authenticated.swift │ │ │ ├── Libs │ │ │ │ └── AuthenticableJWTService.swift │ │ │ ├── Model │ │ │ │ └── Authenticated.swift │ │ │ └── Protocols │ │ │ │ └── Authenticable.swift │ │ ├── Github │ │ │ ├── Configuration │ │ │ │ └── GithubConfig.swift │ │ │ ├── Controllers │ │ │ │ └── GithubLoginController.swift │ │ │ ├── Extensions │ │ │ │ └── URL+Parameters.swift │ │ │ ├── GithubLoginManager.swift │ │ │ └── Model │ │ │ │ ├── GithubEmail.swift │ │ │ │ ├── GithubUser.swift │ │ │ │ └── GithubUserInfo.swift │ │ └── Gitlab │ │ │ ├── Configuration │ │ │ └── GitlabConfig.swift │ │ │ ├── Controllers │ │ │ └── GitlabLoginController.swift │ │ │ ├── Extensions │ │ │ └── Gitlab+URLParameters.swift │ │ │ ├── GitlabLoginManager.swift │ │ │ └── Model │ │ │ ├── GitlabUser.swift │ │ │ └── GitlabUserInfo.swift │ ├── Config │ │ ├── Base setup │ │ │ ├── ApiCoreBase+CORS.swift │ │ │ ├── ApiCoreBase+Configuration.swift │ │ │ ├── ApiCoreBase+Controllers.swift │ │ │ ├── ApiCoreBase+Database.swift │ │ │ ├── ApiCoreBase+Email.swift │ │ │ ├── ApiCoreBase+Middlewares.swift │ │ │ └── ApiCoreBase+Storage.swift │ │ ├── Configurable.swift │ │ └── Configuration.swift │ ├── Controllers │ │ ├── AuthController.swift │ │ ├── GenericController.swift │ │ ├── InstallController.swift │ │ ├── LogsController.swift │ │ ├── ServerController.swift │ │ ├── SettingsController.swift │ │ ├── TeamsController.swift │ │ └── UsersController.swift │ ├── Database │ │ ├── ApiCoreDb.swift │ │ ├── DbCoreModel.swift │ │ └── DbError.swift │ ├── Extensions │ │ ├── Content+Response.swift │ │ ├── Data+Response.swift │ │ ├── Data+Tools.swift │ │ ├── DatabaseIdentifier+Db.swift │ │ ├── Date+Tools.swift │ │ ├── Encodable+Tools.swift │ │ ├── EventLoop+Future.swift │ │ ├── FileCore │ │ │ └── CoreManager+Location.swift │ │ ├── Future+Response.swift │ │ ├── Future+Tools.swift │ │ ├── HTTPHeaders+Tools.swift │ │ ├── Model+Helpers.swift │ │ ├── Request+Auth.swift │ │ ├── Request+Files.swift │ │ ├── Request+URL.swift │ │ ├── Response+Tools.swift │ │ ├── Router+Extended.swift │ │ ├── String+Crypto.swift │ │ ├── String+Manipulation.swift │ │ └── String+Security.swift │ ├── Libs │ │ ├── Audit │ │ │ ├── Audit.swift │ │ │ ├── ConfigurationAudit.swift │ │ │ └── SecurityAudit.swift │ │ ├── AuthError.swift │ │ ├── AuthenticationCache.swift │ │ ├── Coding │ │ │ └── Decodable+Helpers.swift │ │ ├── Env.swift │ │ ├── Filesize.swift │ │ ├── Gravatar.swift │ │ ├── Images │ │ │ ├── Icon.swift │ │ │ └── Logo.swift │ │ ├── Me.swift │ │ ├── RequestIdService.swift │ │ ├── Result.swift │ │ ├── ServerIcon.swift │ │ └── Templates.swift │ ├── Managers │ │ ├── AuthManager.swift │ │ ├── SystemManager.swift │ │ └── UsersManager.swift │ ├── Middleware │ │ ├── ApiAuthMiddleware.swift │ │ ├── DebugCheckMiddleware.swift │ │ ├── ErrorLoggingMiddleware.swift │ │ └── UrlPrinterMiddleware.swift │ ├── Migrations │ │ └── BaseMigration.swift │ ├── Model │ │ ├── Authenticator.swift │ │ ├── ErrorLog.swift │ │ ├── FluentDesign.swift │ │ ├── FrontendSystemData.swift │ │ ├── Info.swift │ │ ├── Query │ │ │ └── BasicQuery.swift │ │ ├── ServerSecurity.swift │ │ ├── Setting.swift │ │ ├── System.swift │ │ ├── Team.swift │ │ ├── TeamUser.swift │ │ ├── Token.swift │ │ ├── User.swift │ │ └── UserSource.swift │ └── Protocols │ │ ├── Controller.swift │ │ └── EmailTemplateData.swift ├── ApiCoreApp │ └── configure.swift ├── ApiCoreRun │ └── main.swift ├── ApiCoreTestTools │ ├── ApiCoreTestTools.swift │ ├── Extensions │ │ ├── Application+Testable.swift │ │ ├── HTTPRequest+Make.swift │ │ ├── Team+Testable.swift │ │ └── User+Testable.swift │ └── Helpers │ │ ├── LinuxTests.swift │ │ ├── TeamsTestCase.swift │ │ └── UsersTestCase.swift ├── FileCore │ ├── Clients │ │ ├── Local │ │ │ ├── LocalClient.swift │ │ │ └── LocalConfig.swift │ │ └── S3 │ │ │ └── S3LibClient.swift │ ├── Extensions │ │ ├── Request+Service.swift │ │ └── Services+Registration.swift │ ├── FileCoreManager.swift │ ├── Libs │ │ ├── Async.swift │ │ └── Configuration │ │ │ └── Configuration.swift │ └── Protocols │ │ ├── CoreManager.swift │ │ └── FileManagement.swift ├── ImageCore │ ├── Extensions │ │ ├── Data+MediaType.swift │ │ ├── MediaType+GD.swift │ │ ├── RequestResponse+ImageCore.swift │ │ └── Size+Tools.swift │ └── Libs │ │ ├── Color.swift │ │ └── ImageError.swift └── ResourceCache │ └── Cache.swift ├── Tests ├── ApiCoreTests │ ├── ApiCoreTests.swift │ ├── Controllers │ │ ├── AuthControllerTests.swift │ │ ├── GenericControllerTests.swift │ │ ├── TeamsControllerTests.swift │ │ └── UsersControllerTests.swift │ ├── Libs │ │ └── StringCryptoTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift ├── apiary.apib ├── docker-compose.override.dist.yaml ├── docker-compose.yaml ├── run.sh ├── scripts ├── docker-shortcuts │ └── kill-all.sh ├── generate-linuxmain.sh └── wait-for-it.sh └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/vapor 3 | 4 | ### Vapor ### 5 | Config/secrets 6 | 7 | ### Vapor Patch ### 8 | Packages 9 | .build 10 | xcuserdata 11 | DerivedData/ 12 | .DS_Store 13 | *_Info.plist 14 | 15 | *.xcodeproj 16 | !empty 17 | .swiftpm 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM einstore/einstore-base:2.0 as builder 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | 6 | ARG CONFIGURATION="release" 7 | 8 | RUN swift build --configuration ${CONFIGURATION} --product ApiCoreRun 9 | 10 | # ------------------------------------------------------------------------------ 11 | 12 | FROM einstore/einstore-base:2.0 13 | 14 | ARG CONFIGURATION="release" 15 | 16 | WORKDIR /app 17 | COPY --from=builder /app/.build/${CONFIGURATION}/ApiCoreRun /app 18 | 19 | ENTRYPOINT ["/app/ApiCoreRun"] 20 | CMD ["serve", "--hostname", "0.0.0.0", "--port", "8080"] 21 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent none 3 | options { 4 | timeout(time: 15, unit: 'MINUTES') 5 | } 6 | 7 | stages { 8 | stage('Builds') { 9 | parallel { 10 | stage('Test Linux') { 11 | agent { 12 | label 'master' 13 | } 14 | when { 15 | anyOf { 16 | branch 'master' 17 | } 18 | } 19 | steps { 20 | script { 21 | sh './test.sh' 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: ## Display this help 2 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-13s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) 3 | 4 | run: ## Run docker compose 5 | docker-compose up 6 | 7 | build: ## Build docker 8 | docker build -t liveui/api-core:local-dev . 9 | 10 | build-debug: ## Build docker image in debug mode 11 | docker build --build-arg CONFIGURATION="debug" -t liveui/api-core:local-dev-debug . 12 | 13 | clean: ## Clean docker compose and .build folder 14 | docker-compose stop -t 2 15 | docker-compose down --volumes 16 | docker-compose --project-name apicore-test stop -t 2 17 | docker-compose --project-name apicore-test down --volumes 18 | rm -rf .build 19 | 20 | test: ## Run tests in docker 21 | docker-compose --project-name apicore-test down 22 | docker-compose --project-name apicore-test run --rm api swift test 23 | docker-compose --project-name apicore-test down 24 | 25 | xcode: ## Generate Xcode project 26 | cp ./ApiCore.xcodeproj/xcshareddata/xcschemes/ApiCoreRun.xcscheme ./ApiCoreRun.xcscheme 27 | vapor xcode -n --verbose 28 | mv ./ApiCoreRun.xcscheme ./ApiCore.xcodeproj/xcshareddata/xcschemes/ApiCoreRun.xcscheme 29 | 30 | update: ## Update all dependencies but keep same versions 31 | cp ./ApiCore.xcodeproj/xcshareddata/xcschemes/ApiCoreRun.xcscheme ./ApiCoreRun.xcscheme 32 | rm -rf .build 33 | vapor clean -y --verbose 34 | vapor xcode -n --verbose 35 | mv ./ApiCoreRun.xcscheme ./ApiCore.xcodeproj/xcshareddata/xcschemes/ApiCoreRun.xcscheme 36 | 37 | upgrade: ## Upgrade all dependencies to the latest versions 38 | cp ./ApiCore.xcodeproj/xcshareddata/xcschemes/ApiCoreRun.xcscheme ./ApiCoreRun.xcscheme 39 | rm -rf .build 40 | vapor clean -y --verbose 41 | rm -f Package.resolved 42 | vapor xcode -n --verbose 43 | mv ./ApiCoreRun.xcscheme ./ApiCore.xcodeproj/xcshareddata/xcschemes/ApiCoreRun.xcscheme 44 | 45 | linuxmain: ## Generate linuxmain file 46 | swift test --generate-linuxmain -------------------------------------------------------------------------------- /Other/default-logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiveUI/ApiCore/c3893b19ba86a1dea2f1a29e88a2388781cb5277/Other/default-logo-512.png -------------------------------------------------------------------------------- /Other/default-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiveUI/ApiCore/c3893b19ba86a1dea2f1a29e88a2388781cb5277/Other/default-logo.png -------------------------------------------------------------------------------- /Other/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiveUI/ApiCore/c3893b19ba86a1dea2f1a29e88a2388781cb5277/Other/logo.png -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ApiCore", 6 | products: [ 7 | .library(name: "ApiCore", targets: ["ApiCore"]), 8 | .library(name: "FileCore", targets: ["FileCore"]), 9 | .library(name: "ImageCore", targets: ["ImageCore"]), 10 | .library(name: "ResourceCache", targets: ["ResourceCache"]), 11 | .library(name: "ApiCoreTestTools", targets: ["ApiCoreTestTools"]) 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), 15 | .package(url: "https://github.com/vapor/core.git", from: "3.4.1"), 16 | .package(url: "https://github.com/vapor/crypto.git", from: "3.2.0"), 17 | .package(url: "https://github.com/vapor/fluent.git", from: "3.0.0"), 18 | .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"), 19 | .package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"), 20 | .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"), 21 | .package(url: "https://github.com/twostraws/SwiftGD.git", .upToNextMinor(from: "2.3.0")), 22 | .package(url: "https://github.com/LiveUI/S3.git", from: "3.0.0"), 23 | .package(url: "https://github.com/LiveUI/MailCore.git", .upToNextMinor(from: "3.1.2")), 24 | .package(url: "https://github.com/LiveUI/ErrorsCore.git", from: "0.1.0"), 25 | .package(url: "https://github.com/LiveUI/VaporTestTools.git", from: "0.1.5"), 26 | .package(url: "https://github.com/LiveUI/FluentTestTools.git", from: "0.1.0"), 27 | .package(url: "https://github.com/vapor-community/Imperial.git", from: "0.12.0") 28 | ], 29 | targets: [ 30 | .target( 31 | name: "ApiCoreApp", 32 | dependencies: [ 33 | "Vapor", 34 | "ApiCore" 35 | ] 36 | ), 37 | .target( 38 | name: "ResourceCache", 39 | dependencies: [ 40 | "Vapor" 41 | ] 42 | ), 43 | .target(name: "ApiCoreRun", dependencies: [ 44 | "ApiCoreApp" 45 | ] 46 | ), 47 | .target(name: "ApiCore", dependencies: [ 48 | "Vapor", 49 | "Fluent", 50 | "Crypto", 51 | "Random", 52 | "FluentPostgreSQL", 53 | "ErrorsCore", 54 | "JWT", 55 | "MailCore", 56 | "Leaf", 57 | "FileCore", 58 | "ImageCore", 59 | "Imperial", 60 | "ResourceCache" 61 | ] 62 | ), 63 | .target(name: "FileCore", dependencies: [ 64 | "Vapor", 65 | "ErrorsCore", 66 | "S3" 67 | ] 68 | ), 69 | .target(name: "ImageCore", dependencies: [ 70 | "Vapor", 71 | "ErrorsCore", 72 | "SwiftGD", 73 | "COperatingSystem" 74 | ] 75 | ), 76 | .target( 77 | name: "ApiCoreTestTools", 78 | dependencies: [ 79 | "Vapor", 80 | "ApiCore", 81 | "VaporTestTools", 82 | "FluentTestTools", 83 | "MailCoreTestTools" 84 | ] 85 | ), 86 | .testTarget(name: "ApiCoreTests", dependencies: [ 87 | "Vapor", 88 | "ErrorsCore", 89 | "ApiCore", 90 | "MailCore", 91 | "VaporTestTools", 92 | "FluentTestTools", 93 | "ApiCoreTestTools", 94 | "MailCoreTestTools" 95 | ] 96 | ) 97 | ] 98 | ) 99 | -------------------------------------------------------------------------------- /Resources/Templates/email.invitation.html.leaf: -------------------------------------------------------------------------------- 1 |

Hi #(user.firstname) #(user.lastname)

2 |

 

3 |

4 | You have been invited to one of our teams by #(sender.firstname) #(sender.lastname) (#(sender.email)).
5 | You can confirm your registration now by clicking on this link 6 |

7 |

 

8 |

Verification code is: #(verification)

9 |

 

10 |

ApiCore

11 | -------------------------------------------------------------------------------- /Resources/Templates/email.invitation.plain.leaf: -------------------------------------------------------------------------------- 1 | Hi #(user.firstname) #(user.lastname) 2 | 3 | You have been invited to one of our teams by #(sender.firstname) #(sender.lastname) (#(sender.email)). 4 | You can confirm your registration now by clicking on this link #(link) 5 | 6 | Verification code is: |#(verification)| 7 | 8 | ApiCore -------------------------------------------------------------------------------- /Resources/Templates/email.password-recovery.html.leaf: -------------------------------------------------------------------------------- 1 |

Hi #(user.firstname) #(user.lastname)

2 |

 

3 |

Please confirm your email #(user.email) by clicking on this link

4 |

 

5 |

Recovery code is: #(verification)

6 |

 

7 |

Boost team

-------------------------------------------------------------------------------- /Resources/Templates/email.password-recovery.plain.leaf: -------------------------------------------------------------------------------- 1 | Hi #(user.firstname) #(user.lastname) 2 | 3 | Please confirm your email #(user.email) by clicking on this link #(link) 4 | 5 | Recovery code is: |#(verification)| 6 | 7 | Boost team -------------------------------------------------------------------------------- /Resources/Templates/email.registration.html.leaf: -------------------------------------------------------------------------------- 1 |

Hi #(user.firstname) #(user.lastname)

2 |

 

3 |

To finish your registration, please confirm your email #(user.email) by clicking on this link

4 |

 

5 |

Verification code is: #(verification)

6 |

 

7 |

ApiCore

-------------------------------------------------------------------------------- /Resources/Templates/email.registration.plain.leaf: -------------------------------------------------------------------------------- 1 | Hi #(user.firstname) #(user.lastname) 2 | 3 | To finish your registration, please confirm your email #(user.email) by clicking on this link #(link) 4 | 5 | Verification code is: |#(verification)| 6 | 7 | ApiCore -------------------------------------------------------------------------------- /Resources/commit.txt: -------------------------------------------------------------------------------- 1 | unknown -------------------------------------------------------------------------------- /Sources/ApiCore/ApiCoreBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreBase.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 09/12/2017. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | import FluentPostgreSQL 12 | import ErrorsCore 13 | import Leaf 14 | import FileCore 15 | 16 | 17 | /// Default database typealias 18 | public typealias ApiCoreDatabase = PostgreSQLDatabase 19 | 20 | /// Default database connection type typealias 21 | public typealias ApiCoreConnection = PostgreSQLConnection 22 | 23 | /// Default database ID column type typealias 24 | public typealias DbIdentifier = UUID 25 | 26 | /// Default database port 27 | public let DbDefaultPort: Int = 5432 28 | 29 | 30 | /// Main ApiCore class 31 | public class ApiCoreBase { 32 | 33 | /// Register models here 34 | public internal(set) static var models: [AnyModel.Type] = [] 35 | 36 | /// Migration config 37 | public static var migrationConfig = MigrationConfig() 38 | 39 | /// Databse config 40 | public static var databaseConfig: DatabasesConfig? 41 | 42 | /// Blocks of code executed when new user registers 43 | public static var userDidRegister: [(User) -> ()] = [] 44 | 45 | /// Blocks of code executed when new user tries to register 46 | public static var userShouldRegister: [(User) -> (Bool)] = [] 47 | 48 | /// Configuration cache 49 | static var _configuration: Configuration? 50 | 51 | /// Enable detailed request debugging 52 | public static var debugRequests: Bool = false 53 | 54 | public typealias DeleteTeamWarning = (_ team: Team) -> Future 55 | public typealias DeleteUserWarning = (_ user: User) -> Future 56 | 57 | /// Fire a warning before team get's deleted (to cascade in submodules, etc ...) 58 | public static var deleteTeamWarning: DeleteTeamWarning? 59 | 60 | /// Fire a warning before user get's deleted (to cascade in submodules, etc ...) 61 | public static var deleteUserWarning: DeleteUserWarning? 62 | 63 | /// Shared middleware config 64 | public internal(set) static var middlewareConfig = MiddlewareConfig() 65 | 66 | /// Add futures to be executed during an installation process 67 | public typealias InstallFutureClosure = (_ worker: BasicWorker) throws -> Future 68 | public static var installFutures: [InstallFutureClosure] = [] 69 | 70 | /// Registered Controllers with the API, these need to have a boot method to setup their routing 71 | public static var controllers: [Controller.Type] = [ 72 | GenericController.self, 73 | InstallController.self, 74 | AuthController.self, 75 | UsersController.self, 76 | TeamsController.self, 77 | LogsController.self, 78 | ServerController.self, 79 | Auth.self, 80 | SettingsController.self 81 | ] 82 | 83 | /// Main configure method for ApiCore 84 | public static func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { 85 | // Set max upload filesize 86 | if configuration.server.maxUploadFilesize ?? 0 < 50 { 87 | configuration.server.maxUploadFilesize = 50 88 | } 89 | let mb = Double(configuration.server.maxUploadFilesize ?? 50) 90 | let maxBodySize = Int(Filesize.megabyte(mb).value) 91 | let serverConfig = NIOServerConfig.default(maxBodySize: maxBodySize) 92 | services.register(serverConfig) 93 | 94 | try setupDatabase(&services) 95 | 96 | try setupEmails(&services) 97 | 98 | // Check JWT secret's security 99 | if env.isRelease && configuration.jwtSecret == "secret" { 100 | fatalError("You shouldn't be running around in a production mode with Configuration.jwt_secret set to \"secret\" as it is not very ... well, secret") 101 | } 102 | 103 | // CORS 104 | setupCORS() 105 | 106 | // Github login 107 | if ApiCoreBase.configuration.auth.github.enabled { 108 | print("Enabling Github login for \(configuration.auth.github.host)") 109 | let githubLogin = try GithubLoginManager( 110 | GithubConfig( 111 | server: ApiCoreBase.configuration.auth.github.host, 112 | api: ApiCoreBase.configuration.auth.github.api 113 | ), 114 | services: &services, 115 | jwtSecret: ApiCoreBase.configuration.jwtSecret 116 | ) 117 | services.register { _ in 118 | githubLogin 119 | } 120 | } else { 121 | print("Github login disabled") 122 | } 123 | 124 | // Gitlab login 125 | if ApiCoreBase.configuration.auth.gitlab.enabled { 126 | print("Enabling Gitlab login for \(configuration.auth.gitlab.host)") 127 | let githubLogin = try GitlabLoginManager( 128 | GitlabConfig( 129 | server: ApiCoreBase.configuration.auth.gitlab.host, 130 | api: ApiCoreBase.configuration.auth.gitlab.api 131 | ), 132 | services: &services, 133 | jwtSecret: ApiCoreBase.configuration.jwtSecret 134 | ) 135 | services.register { _ in 136 | githubLogin 137 | } 138 | } else { 139 | print("Gitlab login disabled") 140 | } 141 | 142 | try Auth.configure(&config, &env, &services) 143 | 144 | // Filesystem 145 | try setupStorage(&services) 146 | 147 | // Templates 148 | try services.register(LeafProvider()) 149 | 150 | let templator = try Templator(packageUrl: ApiCoreBase.configuration.mail.templates) 151 | services.register(templator) 152 | 153 | // UUID service 154 | services.register(RequestIdService.self) 155 | 156 | try setupMiddlewares(&services, &env, &config) 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Generic/Auth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Auth.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 25/04/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import ErrorsCore 11 | 12 | 13 | public final class Auth: Controller { 14 | 15 | public enum Error: FrontendError where A: Authenticable { 16 | 17 | case missingRedirectLink 18 | case unableToProcessUserData 19 | case unableToGenerateRedirectLink 20 | 21 | public var status: HTTPStatus { 22 | switch self { 23 | case .missingRedirectLink: 24 | return .badRequest 25 | case .unableToProcessUserData, .unableToGenerateRedirectLink: 26 | return .internalServerError 27 | } 28 | } 29 | 30 | public var identifier: String { 31 | switch self { 32 | case .missingRedirectLink: 33 | return "\(A.name.lowercased()).missing_redirect_link" 34 | case .unableToProcessUserData: 35 | return "\(A.name.lowercased()).bad_user_data" 36 | case .unableToGenerateRedirectLink: 37 | return "\(A.name.lowercased()).callback_link_error" 38 | } 39 | } 40 | 41 | public var reason: String { 42 | switch self { 43 | case .missingRedirectLink: 44 | return "Missing redirect link" 45 | case .unableToProcessUserData: 46 | return "Unable to process user data" 47 | case .unableToGenerateRedirectLink: 48 | return "Unable to generate the redirect link" 49 | } 50 | } 51 | 52 | } 53 | 54 | static var authenticators: [Authenticable.Type] = [] 55 | 56 | static func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { 57 | for auth in authenticators { 58 | try auth.configure(&config, &env, &services) 59 | } 60 | } 61 | 62 | /// Boot routes for all available authenticators 63 | public static func boot(router: Router, secure: Router, debug: Router) throws { 64 | for auth in authenticators { 65 | try auth.boot(router: router, secure: secure, debug: debug) 66 | } 67 | } 68 | 69 | /// Return authenticated user details back to the system for authentication 70 | /// 71 | /// - Parameters: 72 | /// - user: Information about the user from the service 73 | /// - linkUrl: Redirect URL (usually kept in a session when user is redirected from a website) 74 | /// - auth: Original authenticator service type 75 | /// - req: Request 76 | /// - Returns: Redirect to the desired frontend url with JWT signed data 77 | /// - Throws: FrontendError 78 | public static func authenticate(_ user: Authenticated, redirectUrl: URL, with auth: T, on req: Request) throws -> EventLoopFuture where T: Authenticable { 79 | return try UsersManager.userFromExternalAuthenticationService(user, on: req).flatMap(to: ResponseEncodable.self) { apiCoreUser in 80 | return try AuthManager.authData(request: req, user: apiCoreUser).map(to: ResponseEncodable.self) { authData in 81 | var user = user 82 | user.token = authData.0.token 83 | guard let url = try? redirectUrl.append(userInfo: user, on: req), let unwrappedUrl = url else { 84 | throw Error.unableToGenerateRedirectLink 85 | } 86 | 87 | return req.redirect(to: unwrappedUrl.absoluteString) 88 | } 89 | } 90 | } 91 | 92 | /// Register new authenticator with the system 93 | public static func add(authenticator: Authenticable.Type) throws { 94 | authenticators.append(authenticator) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Generic/Extensions/URL+Authenticated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Authenticated.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 25/04/2019. 6 | // 7 | 8 | import Foundation 9 | import JWT 10 | 11 | extension URL { 12 | 13 | @discardableResult func append(userInfo: Authenticated, on req: Request) throws -> URL? { 14 | guard var urlComponents = URLComponents(string: absoluteString) else { return absoluteURL } 15 | var queryItems: [URLQueryItem] = urlComponents.queryItems ?? [] 16 | 17 | // Add user info as a JWT token 18 | let jwtService: AuthenticableJWTService = try req.make() 19 | let token = try jwtService.signAuthenticatedUserInfoToToken(userInfo) 20 | 21 | let infoValue = URLQueryItem(name: "info", value: token) 22 | queryItems.append(infoValue) 23 | 24 | urlComponents.queryItems = queryItems 25 | return urlComponents.url 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Generic/Libs/AuthenticableJWTService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticableJWTService.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 25/04/2019. 6 | // 7 | 8 | import Foundation 9 | import JWT 10 | 11 | 12 | /// JWT service 13 | final class AuthenticableJWTService: Service { 14 | 15 | /// Signer 16 | var signer: JWTSigner 17 | 18 | /// Initializer 19 | init(secret: String) { 20 | signer = JWTSigner.hs512(key: Data(secret.utf8)) 21 | } 22 | 23 | /// Sign user info to token 24 | func signAuthenticatedUserInfoToToken(_ info: Authenticated) throws -> String { 25 | var jwt = JWT(payload: info) 26 | 27 | jwt.header.typ = nil // set to nil to avoid dictionary re-ordering causing probs 28 | let data = try signer.sign(jwt) 29 | 30 | guard let jwtToken: String = String(data: data, encoding: .utf8) else { 31 | fatalError() 32 | } 33 | return jwtToken 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Generic/Model/Authenticated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticated.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 25/04/2019. 6 | // 7 | 8 | import Foundation 9 | import JWT 10 | 11 | 12 | /// When user is authenticated through the service (usually through a callback) they should return an Authenticated object back to Auth for finish the process 13 | public struct Authenticated: JWTPayload, UserSource { 14 | 15 | public let username: String 16 | 17 | public let firstname: String 18 | 19 | public let lastname: String 20 | 21 | public let email: String 22 | 23 | public var info: [String : String]? 24 | 25 | public var token: String? 26 | 27 | 28 | /// Expiration claim (for signing JWT) 29 | let expires: ExpirationClaim 30 | 31 | /// Initializer 32 | init(username: String, firstname: String, lastname: String, email: String, info: [String : String]? = nil, token: String? = nil) { 33 | self.username = username 34 | self.firstname = firstname 35 | self.lastname = lastname 36 | self.email = email 37 | self.info = info 38 | self.token = token 39 | expires = ExpirationClaim(value: Date().addingTimeInterval(120)) 40 | } 41 | 42 | } 43 | 44 | 45 | extension Authenticated { 46 | 47 | public func verify(using signer: JWTSigner) throws { 48 | try expires.verifyNotExpired() 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Generic/Protocols/Authenticable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticable.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 25/04/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | public enum AuthType: String, Codable { 12 | case basic = "BASIC" 13 | case ldap = "LDAP" 14 | case oauth = "OAUTH" 15 | } 16 | 17 | 18 | /// Main authentication protocol 19 | public protocol Authenticable: Controller { 20 | 21 | /// Name of the service 22 | static var name: String { get } 23 | 24 | /// FontAwesone icon name (Ex. folder, github, apple) 25 | static var icon: String { get } 26 | 27 | /// Hex color for the service (no #, FF0000, 000000) 28 | static var color: String { get } 29 | 30 | /// Relative link to the service, (Ex. auth/github/login) 31 | static var link: String { get } 32 | 33 | /// Authentication type 34 | static var type: AuthType { get } 35 | 36 | /// Allow registration if user email doesn't exist 37 | static var allowRegistrations: Bool { get } 38 | 39 | /// Configure services 40 | static func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Github/Configuration/GithubConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubConfig.swift 3 | // GithubLogin 4 | // 5 | // Created by Ondrej Rafaj on 05/03/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | public struct GithubConfig { 13 | 14 | public let server: String 15 | 16 | public let api: String 17 | 18 | public var scopes: [String] = ["read:user", "user:email"] 19 | 20 | public init(server: String = "https://github.com/", api: String = "https://api.github.com/") { 21 | self.server = server 22 | self.api = api 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Github/Extensions/URL+Parameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Parameters.swift 3 | // GithubLogin 4 | // 5 | // Created by Ondrej Rafaj on 27/03/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import JWT 11 | 12 | 13 | /// JWT service 14 | final class GithubJWTService: Service { 15 | 16 | /// Signer 17 | var signer: JWTSigner 18 | 19 | /// Initializer 20 | init(secret: String) { 21 | signer = JWTSigner.hs512(key: Data(secret.utf8)) 22 | } 23 | 24 | /// Sign user info to token 25 | func signUserInfoToToken(info: GithubUserInfo) throws -> String { 26 | var jwt = JWT(payload: info) 27 | 28 | jwt.header.typ = nil // set to nil to avoid dictionary re-ordering causing probs 29 | let data = try signer.sign(jwt) 30 | 31 | guard let jwtToken: String = String(data: data, encoding: .utf8) else { 32 | fatalError() 33 | } 34 | return jwtToken 35 | } 36 | 37 | } 38 | 39 | 40 | 41 | extension URL { 42 | 43 | @discardableResult func append(userInfo: GithubUserInfo, on req: Request) throws -> URL? { 44 | guard var urlComponents = URLComponents(string: absoluteString) else { return absoluteURL } 45 | var queryItems: [URLQueryItem] = urlComponents.queryItems ?? [] 46 | 47 | // Add user info as a JWT token 48 | let jwtService: GithubJWTService = try req.make() 49 | let token = try jwtService.signUserInfoToToken(info: userInfo) 50 | 51 | let infoValue = URLQueryItem(name: "info", value: token) 52 | queryItems.append(infoValue) 53 | 54 | urlComponents.queryItems = queryItems 55 | return urlComponents.url 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Github/GithubLoginManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubLoginManager.swift 3 | // GithubLogin 4 | // 5 | // Created by Ondrej Rafaj on 03/03/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Imperial 11 | 12 | 13 | /// GitHub login manager 14 | public class GithubLoginManager: Service { 15 | 16 | public let config: GithubConfig 17 | 18 | public init(_ config: GithubConfig, services: inout Services, jwtSecret: String) throws { 19 | self.config = config 20 | 21 | Imperial.GitHubRouter.baseURL = ApiCoreBase.configuration.auth.github.host.finished(with: "/") 22 | Imperial.GitHubAuth.idEnvKey = "APICORE_AUTH_GITHUB_CLIENT" 23 | Imperial.GitHubAuth.secretEnvKey = "APICORE_AUTH_GITHUB_SECRET" 24 | 25 | services.register { _ in 26 | GithubJWTService(secret: jwtSecret) 27 | } 28 | 29 | GithubLoginController.config = config 30 | 31 | ApiCoreBase.controllers.append(GithubLoginController.self) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Github/Model/GithubEmail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitlabEmail.swift 3 | // GithubLogin 4 | // 5 | // Created by Ondrej Rafaj on 27/03/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias GithubEmails = [GithubEmail] 11 | 12 | public struct GithubEmail: Codable { 13 | 14 | public let email: String 15 | public let primary: Bool? 16 | public let verified: Bool? 17 | public let visibility: String? 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Github/Model/GithubUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubUser.swift 3 | // GithubLogin 4 | // 5 | // Created by Ondrej Rafaj on 27/03/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | public struct GithubUser: Codable { 12 | 13 | public struct Plan: Codable { 14 | 15 | public let name: String? 16 | public let space: Int? 17 | public let collaborators: Int? 18 | public let privateRepos: Int? 19 | 20 | enum CodingKeys: String, CodingKey { 21 | case name = "name" 22 | case space = "space" 23 | case collaborators = "collaborators" 24 | case privateRepos = "private_repos" 25 | } 26 | 27 | } 28 | 29 | public let id: Int 30 | public let login: String 31 | public let nodeID: String? 32 | public let avatarURL: String? 33 | public let gravatarID: String? 34 | public let url: String? 35 | public let htmlURL: String? 36 | public let followersURL: String? 37 | public let followingURL: String? 38 | public let gistsURL: String? 39 | public let starredURL: String? 40 | public let subscriptionsURL: String? 41 | public let organizationsURL: String? 42 | public let reposURL: String? 43 | public let eventsURL: String? 44 | public let receivedEventsURL: String? 45 | public let type: String? 46 | public let siteAdmin: Bool? 47 | public let name: String? 48 | public let company: String? 49 | public let blog: String? 50 | public let location: String? 51 | public let email: String? 52 | public let hireable: Bool? 53 | public let bio: String? 54 | public let publicRepos: Int? 55 | public let publicGists: Int? 56 | public let followers: Int? 57 | public let following: Int? 58 | public let createdAt: String? 59 | public let updatedAt: String? 60 | public let privateGists: Int? 61 | public let totalPrivateRepos: Int? 62 | public let ownedPrivateRepos: Int? 63 | public let diskUsage: Int? 64 | public let collaborators: Int? 65 | public let twoFactorAuthentication: Bool? 66 | public let plan: Plan? 67 | 68 | enum CodingKeys: String, CodingKey { 69 | case login = "login" 70 | case id = "id" 71 | case nodeID = "node_id" 72 | case avatarURL = "avatar_url" 73 | case gravatarID = "gravatar_id" 74 | case url = "url" 75 | case htmlURL = "html_url" 76 | case followersURL = "followers_url" 77 | case followingURL = "following_url" 78 | case gistsURL = "gists_url" 79 | case starredURL = "starred_url" 80 | case subscriptionsURL = "subscriptions_url" 81 | case organizationsURL = "organizations_url" 82 | case reposURL = "repos_url" 83 | case eventsURL = "events_url" 84 | case receivedEventsURL = "received_events_url" 85 | case type = "type" 86 | case siteAdmin = "site_admin" 87 | case name = "name" 88 | case company = "company" 89 | case blog = "blog" 90 | case location = "location" 91 | case email = "email" 92 | case hireable = "hireable" 93 | case bio = "bio" 94 | case publicRepos = "public_repos" 95 | case publicGists = "public_gists" 96 | case followers = "followers" 97 | case following = "following" 98 | case createdAt = "created_at" 99 | case updatedAt = "updated_at" 100 | case privateGists = "private_gists" 101 | case totalPrivateRepos = "total_private_repos" 102 | case ownedPrivateRepos = "owned_private_repos" 103 | case diskUsage = "disk_usage" 104 | case collaborators = "collaborators" 105 | case twoFactorAuthentication = "two_factor_authentication" 106 | case plan = "plan" 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Github/Model/GithubUserInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInfo.swift 3 | // GithubLogin 4 | // 5 | // Created by Ondrej Rafaj on 27/03/2019. 6 | // 7 | 8 | import Foundation 9 | import JWT 10 | 11 | 12 | public struct GithubUserInfo: Codable, JWTPayload, UserSource { 13 | 14 | public enum Error: Swift.Error { 15 | case missingEmail 16 | } 17 | 18 | /// Expiration 19 | var exp: ExpirationClaim 20 | 21 | public let username: String 22 | public let firstname: String 23 | public let lastname: String 24 | public let email: String 25 | public let avatar: String? 26 | public let companies: [String] 27 | 28 | public var token: String? 29 | public let githubToken: String 30 | 31 | public var info: [String : String]? 32 | 33 | /// Initializer 34 | init(user: GithubUser, emails: GithubEmails, githubToken: String, token: String? = nil) throws { 35 | username = user.login 36 | 37 | let name = user.name ?? "" 38 | if name.isEmpty { 39 | firstname = user.login 40 | lastname = "" 41 | } else { 42 | let parts = name.split(separator: " ") 43 | firstname = String(parts[0]) 44 | if parts.count > 1 { 45 | lastname = String(parts.last ?? "") 46 | } else { 47 | lastname = "" 48 | } 49 | } 50 | 51 | guard let email = emails.filter({ $0.primary ?? false }).first?.email else { 52 | throw Error.missingEmail 53 | } 54 | self.email = email 55 | avatar = user.avatarURL 56 | companies = user.company?.split(separator: ",").map({ String($0).trimmingCharacters(in: .whitespacesAndNewlines) }) ?? [] 57 | 58 | exp = ExpirationClaim(value: Date().addingTimeInterval(120)) 59 | 60 | self.githubToken = githubToken 61 | self.token = token 62 | } 63 | 64 | /// Verify 65 | public func verify(using signer: JWTSigner) throws { 66 | try exp.verifyNotExpired() 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Gitlab/Configuration/GitlabConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitlabConfig.swift 3 | // GitlabLogin 4 | // 5 | // Created by Ondrej Rafaj on 05/03/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | public struct GitlabConfig { 13 | 14 | public let server: String 15 | 16 | public let api: String 17 | 18 | public var scopes: [String] = ["read_user", "email"] 19 | 20 | public init(server: String = "https://gitlab.com/", api: String = "https://gitlab.com/api/v4/") { 21 | self.server = server 22 | self.api = api 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Gitlab/Extensions/Gitlab+URLParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Parameters.swift 3 | // GitlabLogin 4 | // 5 | // Created by Ondrej Rafaj on 27/03/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import JWT 11 | 12 | 13 | /// JWT service 14 | final class GitlabJWTService: Service { 15 | 16 | /// Signer 17 | var signer: JWTSigner 18 | 19 | /// Initializer 20 | init(secret: String) { 21 | signer = JWTSigner.hs512(key: Data(secret.utf8)) 22 | } 23 | 24 | /// Sign user info to token 25 | func signUserInfoToToken(info: GitlabUserInfo) throws -> String { 26 | var jwt = JWT(payload: info) 27 | 28 | jwt.header.typ = nil // set to nil to avoid dictionary re-ordering causing probs 29 | let data = try signer.sign(jwt) 30 | 31 | guard let jwtToken: String = String(data: data, encoding: .utf8) else { 32 | fatalError() 33 | } 34 | return jwtToken 35 | } 36 | 37 | } 38 | 39 | 40 | 41 | extension URL { 42 | 43 | @discardableResult func append(userInfo: GitlabUserInfo, on req: Request) throws -> URL? { 44 | guard var urlComponents = URLComponents(string: absoluteString) else { return absoluteURL } 45 | var queryItems: [URLQueryItem] = urlComponents.queryItems ?? [] 46 | 47 | // Add user info as a JWT token 48 | let jwtService: GitlabJWTService = try req.make() 49 | let token = try jwtService.signUserInfoToToken(info: userInfo) 50 | 51 | let infoValue = URLQueryItem(name: "info", value: token) 52 | queryItems.append(infoValue) 53 | 54 | urlComponents.queryItems = queryItems 55 | return urlComponents.url 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Gitlab/GitlabLoginManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitlabLoginManager.swift 3 | // GitlabLogin 4 | // 5 | // Created by Ondrej Rafaj on 03/03/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Imperial 11 | 12 | 13 | /// Gitlab login manager 14 | public class GitlabLoginManager: Service { 15 | 16 | public let config: GitlabConfig 17 | 18 | public init(_ config: GitlabConfig, services: inout Services, jwtSecret: String) throws { 19 | self.config = config 20 | 21 | Imperial.GitlabRouter.baseURL = ApiCoreBase.configuration.auth.gitlab.host.finished(with: "/") 22 | Imperial.GitlabAuth.idEnvKey = "APICORE_AUTH_GITLAB_APPLICATION" 23 | Imperial.GitlabAuth.secretEnvKey = "APICORE_AUTH_GITLAB_SECRET" 24 | Imperial.GitlabRouter.callbackURL = "\(Me.serverURL().absoluteString.finished(with: "/"))auth/gitlab/callback" 25 | 26 | services.register { _ in 27 | GitlabJWTService(secret: jwtSecret) 28 | } 29 | 30 | GitlabLoginController.config = config 31 | 32 | ApiCoreBase.controllers.append(GitlabLoginController.self) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Gitlab/Model/GitlabUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitlabUser.swift 3 | // GitlabLogin 4 | // 5 | // Created by Ondrej Rafaj on 27/03/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | struct GitlabUser: Codable { 12 | 13 | struct Identity: Codable { 14 | let provider: String? 15 | let externUID: String? 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case provider = "provider" 19 | case externUID = "extern_uid" 20 | } 21 | } 22 | 23 | let id: Int 24 | let username: String 25 | let email: String 26 | let name: String? 27 | let state: String? 28 | let avatarURL: String? 29 | let webURL: String? 30 | let createdAt: String? 31 | let isAdmin: Bool? 32 | let bio: String? 33 | let location: String? 34 | let publicEmail: String? 35 | let skype: String? 36 | let linkedin: String? 37 | let twitter: String? 38 | let websiteURL: String? 39 | let organization: String? 40 | let lastSignInAt: String? 41 | let confirmedAt: String? 42 | let themeID: Int? 43 | let lastActivityOn: String? 44 | let colorSchemeID: Int? 45 | let projectsLimit: Int? 46 | let currentSignInAt: String? 47 | let identities: [Identity]? 48 | let canCreateGroup: Bool? 49 | let canCreateProject: Bool? 50 | let twoFactorEnabled: Bool? 51 | let external: Bool? 52 | let privateProfile: Bool? 53 | 54 | enum CodingKeys: String, CodingKey { 55 | case id = "id" 56 | case username = "username" 57 | case email = "email" 58 | case name = "name" 59 | case state = "state" 60 | case avatarURL = "avatar_url" 61 | case webURL = "web_url" 62 | case createdAt = "created_at" 63 | case isAdmin = "is_admin" 64 | case bio = "bio" 65 | case location = "location" 66 | case publicEmail = "public_email" 67 | case skype = "skype" 68 | case linkedin = "linkedin" 69 | case twitter = "twitter" 70 | case websiteURL = "website_url" 71 | case organization = "organization" 72 | case lastSignInAt = "last_sign_in_at" 73 | case confirmedAt = "confirmed_at" 74 | case themeID = "theme_id" 75 | case lastActivityOn = "last_activity_on" 76 | case colorSchemeID = "color_scheme_id" 77 | case projectsLimit = "projects_limit" 78 | case currentSignInAt = "current_sign_in_at" 79 | case identities = "identities" 80 | case canCreateGroup = "can_create_group" 81 | case canCreateProject = "can_create_project" 82 | case twoFactorEnabled = "two_factor_enabled" 83 | case external = "external" 84 | case privateProfile = "private_profile" 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /Sources/ApiCore/Auth/Gitlab/Model/GitlabUserInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInfo.swift 3 | // GitlabLogin 4 | // 5 | // Created by Ondrej Rafaj on 27/03/2019. 6 | // 7 | 8 | import Foundation 9 | import JWT 10 | 11 | 12 | public struct GitlabUserInfo: JWTPayload, UserSource { 13 | 14 | public enum Error: Swift.Error { 15 | case missingEmail 16 | } 17 | 18 | /// Expiration 19 | var exp: ExpirationClaim 20 | 21 | public let username: String 22 | public let firstname: String 23 | public let lastname: String 24 | public let email: String 25 | public let avatar: String? 26 | public let companies: [String] 27 | 28 | public var token: String? 29 | public let gitlabToken: String 30 | 31 | public var info: [String : String]? 32 | 33 | /// Initializer 34 | init(user: GitlabUser, gitlabToken: String, token: String? = nil) throws { 35 | username = user.username 36 | 37 | let name = user.name ?? "" 38 | if name.isEmpty { 39 | firstname = user.username 40 | lastname = "" 41 | } else { 42 | let parts = name.split(separator: " ") 43 | firstname = String(parts[0]) 44 | if parts.count > 1 { 45 | lastname = String(parts.last ?? "") 46 | } else { 47 | lastname = "" 48 | } 49 | } 50 | 51 | email = user.email 52 | avatar = user.avatarURL 53 | companies = user.organization?.split(separator: ",").map({ String($0).trimmingCharacters(in: .whitespacesAndNewlines) }) ?? [] 54 | 55 | exp = ExpirationClaim(value: Date().addingTimeInterval(120)) 56 | 57 | self.gitlabToken = gitlabToken 58 | self.token = token 59 | } 60 | 61 | /// Verify 62 | public func verify(using signer: JWTSigner) throws { 63 | try exp.verifyNotExpired() 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Sources/ApiCore/Config/Base setup/ApiCoreBase+CORS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreBase+CORS.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 17/04/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension ApiCoreBase { 12 | 13 | static func setupCORS() { 14 | let corsConfig = CORSMiddleware.Configuration( 15 | allowedOrigin: .all, 16 | allowedMethods: [.GET, .POST, .PUT, .OPTIONS, .DELETE, .PATCH], 17 | allowedHeaders: [.accept, .authorization, .contentType, .origin, .xRequestedWith, .userAgent], 18 | exposedHeaders: [ 19 | HTTPHeaderName.authorization.description, 20 | HTTPHeaderName.contentLength.description, 21 | HTTPHeaderName.contentType.description, 22 | HTTPHeaderName.contentDisposition.description, 23 | HTTPHeaderName.cacheControl.description, 24 | HTTPHeaderName.expires.description 25 | ] 26 | ) 27 | let cors = CORSMiddleware(configuration: corsConfig) 28 | middlewareConfig.use(cors) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ApiCore/Config/Base setup/ApiCoreBase+Configuration.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // ApiCoreBase+Configuration.swift 4 | // ApiCore 5 | // 6 | // Created by Ondrej Rafaj on 17/04/2019. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension ApiCoreBase { 13 | 14 | /// Main system configuration 15 | public static var configuration: Configuration { 16 | get { 17 | if _configuration == nil { 18 | // Create default configuration 19 | _configuration = Configuration.default 20 | 21 | // Override any properties with ENV 22 | _configuration?.loadEnv() 23 | } 24 | guard let configuration = _configuration else { 25 | fatalError("Configuration couldn't be loaded!") 26 | } 27 | return configuration 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ApiCore/Config/Base setup/ApiCoreBase+Controllers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreBase+Controllers.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 17/04/2019. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension ApiCoreBase { 12 | 13 | /// Boot routes for all registered controllers 14 | @discardableResult public static func boot(router: Router) throws -> (router: Router, secure: Router, debug: Router) { 15 | let group: Router 16 | if let prefix = configuration.server.pathPrefix { 17 | print("Using path prefix '\(prefix)' for all API endpoints") 18 | group = router.grouped(prefix) 19 | } else { 20 | group = router 21 | } 22 | 23 | let secureRouter = group.grouped(ApiAuthMiddleware.self) 24 | let debugRouter = group.grouped(DebugCheckMiddleware.self) 25 | 26 | for c in controllers { 27 | try c.boot(router: group, secure: secureRouter, debug: debugRouter) 28 | } 29 | 30 | return (group, secureRouter, debugRouter) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ApiCore/Config/Base setup/ApiCoreBase+Database.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreBase+Database.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 17/04/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import FluentPostgreSQL 11 | 12 | 13 | extension ApiCoreBase { 14 | 15 | static func setupDatabase(_ services: inout Services) throws { 16 | // Migrate models / tables 17 | 18 | add(model: Team.self, database: .psql) 19 | add(model: User.self, database: .psql) 20 | add(model: TeamUser.self, database: .psql) 21 | add(model: Token.self, database: .psql) 22 | add(model: ErrorLog.self, database: .psql) 23 | add(model: System.self, database: .psql) 24 | add(model: Setting.self, database: .psql) 25 | 26 | // Data migrations 27 | migrationConfig.add(migration: BaseMigration.self, database: .psql) 28 | 29 | // Set database on tables that don't have migration 30 | FluentDesign.defaultDatabase = .psql 31 | 32 | // Database - Load database details 33 | let host = configuration.database.host ?? "localhost" 34 | let port = configuration.database.port ?? 5432 35 | let databaseConfig = ApiCoreDb.config( 36 | hostname: host, 37 | user: configuration.database.user, 38 | password: configuration.database.password, 39 | database: configuration.database.database, 40 | port: port 41 | ) 42 | 43 | print("Configuring database '\(configuration.database.database)' on \(configuration.database.user)@\(host):\(port)") 44 | 45 | try services.register(FluentPostgreSQLProvider()) 46 | 47 | self.databaseConfig = databaseConfig 48 | 49 | services.register(databaseConfig) 50 | services.register(migrationConfig) 51 | } 52 | 53 | /// Add / register model 54 | public static func add(model: Model.Type, database: DatabaseIdentifier) where Model: Fluent.Migration, Model: Fluent.Model, Model.Database: Database { 55 | models.append(model) 56 | migrationConfig.add(model: model, database: database) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Sources/ApiCore/Config/Base setup/ApiCoreBase+Email.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreBase+Email.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 18/04/2019. 6 | // 7 | 8 | import Foundation 9 | import MailCore 10 | import Vapor 11 | 12 | 13 | extension ApiCoreBase { 14 | 15 | static func setupEmails(_ services: inout Services) throws { 16 | let mail: Mailer.Config 17 | if !configuration.mail.mailgun.key.isEmpty, !configuration.mail.mailgun.domain.isEmpty { 18 | mail = Mailer.Config.mailgun(key: configuration.mail.mailgun.key, domain: configuration.mail.mailgun.domain, region: .eu) 19 | print("Configuring Mailgun for domain \(configuration.mail.mailgun.domain) as the main mailing service") 20 | } else if !configuration.mail.smtp.isEmpty { 21 | let parts = configuration.mail.smtp.split(separator: ";") 22 | guard parts.count >= 3 else { 23 | fatalError("Invalid SMTP configuration; Should be `smtp_server;username;password;port`, where port is an optional value which defaults to 25") 24 | } 25 | let port: Int32 = (parts.count >= 4) ? Int32(parts[3]) ?? 25 : 25 26 | mail = Mailer.Config.smtp( 27 | SMTP( 28 | hostname: String(parts[0]), 29 | email: String(parts[1]), 30 | password: String(parts[2]), 31 | port: port 32 | ) 33 | ) 34 | print("Configuring SMTP for \(parts[1])@\(parts[0]):\(port) as the main mailing service") 35 | } else { 36 | let message = "Email service hasn't been configured" 37 | if try Environment.detect() == .production { 38 | fatalError(message) 39 | } else { 40 | print(message) 41 | mail = Mailer.Config.none 42 | } 43 | } 44 | try Mailer(config: mail, registerOn: &services) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ApiCore/Config/Base setup/ApiCoreBase+Middlewares.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreBase+Middlewares.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 17/04/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import ErrorsCore 11 | 12 | 13 | extension ApiCoreBase { 14 | 15 | static func setupMiddlewares(_ services: inout Services, _ env: inout Environment, _ config: inout Config) throws { 16 | // Errors 17 | middlewareConfig.use(ErrorLoggingMiddleware.self) 18 | services.register(ErrorLoggingMiddleware()) 19 | 20 | middlewareConfig.use(ErrorsCoreMiddleware.self) 21 | services.register(ErrorsCoreMiddleware(environment: env, log: PrintLogger())) 22 | 23 | // Authentication 24 | services.register(ApiAuthMiddleware()) 25 | services.register(DebugCheckMiddleware()) 26 | 27 | // Debugging 28 | if !env.isRelease { 29 | middlewareConfig.use(UrlPrinterMiddleware.self) 30 | services.register(UrlPrinterMiddleware()) 31 | } 32 | 33 | services.register { _ in 34 | JWTService(secret: configuration.jwtSecret) 35 | } 36 | services.register(AuthenticationCache.self) 37 | 38 | // Sessions middleware 39 | config.prefer(MemoryKeyedCache.self, for: KeyedCache.self) 40 | middlewareConfig.use(SessionsMiddleware.self) 41 | 42 | // Register middlewares 43 | services.register(middlewareConfig) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/ApiCore/Config/Base setup/ApiCoreBase+Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreBase+Storage.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 17/04/2019. 6 | // 7 | 8 | import Foundation 9 | import S3 10 | import ResourceCache 11 | 12 | 13 | extension ApiCoreBase { 14 | 15 | static func setupStorage(_ services: inout Services) throws { 16 | services.register( 17 | ResourceCache.Cache( 18 | Cache.Config( 19 | storagePath: configuration.storage.local.root.finished(with: "/").appending("ResourceCache") 20 | ) 21 | ) 22 | ) 23 | if configuration.storage.s3.enabled { 24 | let config = S3Signer.Config(accessKey: configuration.storage.s3.accessKey, 25 | secretKey: configuration.storage.s3.secretKey, 26 | region: configuration.storage.s3.region, 27 | securityToken: configuration.storage.s3.securityToken 28 | ) 29 | try services.register(s3: config, defaultBucket: configuration.storage.s3.bucket) 30 | try services.register(fileCoreManager: .s3( 31 | config, 32 | configuration.storage.s3.bucket 33 | )) 34 | } else { 35 | try services.register(fileCoreManager: .local(LocalConfig(root: configuration.storage.local.root))) 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Sources/ApiCore/Config/Configurable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configurable.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 22/05/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | /// Configurable 13 | public protocol Configurable: Codable { } 14 | 15 | 16 | extension Configurable { 17 | 18 | /// Load String property from env 19 | public func load(_ key: String, to property: inout String) { 20 | if let value = self.property(key: key) { 21 | property = value 22 | } 23 | } 24 | 25 | /// Load optional String property from env 26 | public func load(_ key: String, to property: inout String?) { 27 | if let value: String = self.property(key: key) { 28 | property = value 29 | } 30 | } 31 | 32 | /// Load Int property from env 33 | public func load(_ key: String, to property: inout Int) { 34 | if let value = self.property(key: key), let converted = Int(value) { 35 | property = converted 36 | } 37 | } 38 | 39 | /// Load optional Int property from env 40 | public func load(_ key: String, to property: inout Int?) { 41 | if let value = self.property(key: key), let converted = Int(value) { 42 | property = converted 43 | } 44 | } 45 | 46 | /// Load Double property from env 47 | public func load(_ key: String, to property: inout Double) { 48 | if let value = self.property(key: key), let converted = Double(value) { 49 | property = converted 50 | } 51 | } 52 | 53 | /// Load optional Double property from env 54 | public func load(_ key: String, to property: inout Double?) { 55 | if let value = self.property(key: key), let converted = Double(value) { 56 | property = converted 57 | } 58 | } 59 | 60 | /// Load Bool property from env 61 | public func load(_ key: String, to property: inout Bool) { 62 | if let value = self.property(key: key), let converted = value.bool { 63 | property = converted 64 | } 65 | } 66 | 67 | /// Load optional Bool property from env 68 | public func load(_ key: String, to property: inout Bool?) { 69 | if let value = self.property(key: key), let converted = value.bool { 70 | property = converted 71 | } 72 | } 73 | 74 | /// Load an array of comma separated strings from ENW 75 | public func load(_ key: String, to property: inout [String]) { 76 | if let value = self.property(key: key) { 77 | property = value.replacingOccurrences(of: ", ", with: ",").split(separator: ",").map({ String($0) }) 78 | } 79 | } 80 | 81 | /// Read property 82 | public func property(key: String) -> String? { 83 | // TODO: Convert all internal syntax to upper cased ssnake case so this method becomes obsolete!!! 84 | let value = Environment.get(key.snake_cased().uppercased()) 85 | return value 86 | } 87 | 88 | /// Load configuration from a file. If a relative path is given, source root will be used as a starting point 89 | public static func load(fromFile path: String) throws -> Configuration { 90 | let url: URL 91 | if path.prefix(1) == "/" { 92 | url = URL(fileURLWithPath: path) 93 | } else { 94 | let config = DirectoryConfig.detect() 95 | url = URL(fileURLWithPath: config.workDir).appendingPathComponent(path) 96 | } 97 | let data = try Data(contentsOf: url) 98 | return try load(fromData: data) 99 | } 100 | 101 | /// Load configuration from a JSON string representation 102 | public static func load(fromString string: String) throws -> Configuration { 103 | guard let data = string.data(using: .utf8) else { 104 | throw Configuration.Error.invalidConfigurationData 105 | } 106 | return try load(fromData: data) 107 | } 108 | 109 | /// Load configuration from a Data string representation 110 | public static func load(fromData data: Data) throws -> Configuration { 111 | return try JSONDecoder().decode(Configuration.self, from: data) 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /Sources/ApiCore/Controllers/GenericController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericController.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 13/01/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | /// Generic/default routes 13 | public class GenericController: Controller { 14 | 15 | /// Setup routes 16 | public static func boot(router: Router, secure: Router, debug: Router) throws { 17 | // Any uknown GET URL 18 | router.get(PathComponent.anything) { req in 19 | return try req.response.badUrl() 20 | } 21 | 22 | // Any uknown POST URL 23 | router.post(PathComponent.anything) { req in 24 | return try req.response.badUrl() 25 | } 26 | 27 | // Any uknown PUT URL 28 | router.put(PathComponent.anything) { req in 29 | return try req.response.badUrl() 30 | } 31 | 32 | // Any uknown PATCH URL 33 | router.patch(PathComponent.anything) { req in 34 | return try req.response.badUrl() 35 | } 36 | 37 | // Any uknown DELETE URL 38 | router.delete(PathComponent.anything) { req in 39 | return try req.response.badUrl() 40 | } 41 | 42 | // I am a teapot, really! 43 | router.get("teapot") { req in 44 | return try req.response.teapot() 45 | } 46 | 47 | // Ping response (ok, 200) 48 | router.get("ping") { req in 49 | return try req.response.ping() 50 | } 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ApiCore/Controllers/InstallController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InstallController.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 13/01/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import ErrorsCore 11 | //import DbCore 12 | import Fluent 13 | import FluentPostgreSQL 14 | 15 | 16 | public class InstallController: Controller { 17 | 18 | /// Error 19 | public enum Error: FrontendError { 20 | 21 | /// Data exists 22 | case dataExists 23 | 24 | /// Error code 25 | public var identifier: String { 26 | return "install_failed.data_exists" 27 | } 28 | 29 | /// Reason to fail 30 | public var reason: String { 31 | return "Data already exists" 32 | } 33 | 34 | /// Error HTTP code 35 | public var status: HTTPStatus { 36 | return .preconditionFailed 37 | } 38 | } 39 | 40 | /// Setup routes 41 | public static func boot(router: Router, secure: Router, debug: Router) throws { 42 | debug.get("install") { req->Future in 43 | return try install(on: req) 44 | } 45 | 46 | debug.get("uninstall") { req->Future in 47 | return try uninstall(on: req) 48 | } 49 | 50 | debug.get("database") { req in 51 | // TODO: Show table names and other info 52 | return FluentDesign.query(on: req).all() 53 | } 54 | } 55 | 56 | } 57 | 58 | 59 | extension InstallController { 60 | 61 | /// New super user 62 | static func su(on worker: BasicWorker) throws -> User { 63 | let user = try User(username: "admin", firstname: "Super", lastname: "Admin", email: "core@liveui.io", password: "sup3rS3cr3t".passwordHash(worker), disabled: false, su: true) 64 | user.verified = true 65 | return user 66 | } 67 | 68 | /// New admin team 69 | static var adminTeam: Team { 70 | return Team(name: "Admin team", identifier: "admin-team", admin: true) 71 | } 72 | 73 | /// Uninstall all data and drop all tables 74 | private static func uninstall(on req: Request) throws -> Future { 75 | var futures: [Future] = [] 76 | return req.requestPooledConnection(to: .psql).flatMap(to: Response.self) { connection in 77 | futures.append(ApiCoreBase.migrationConfig.revertAll(on: req)) 78 | return futures.flatten(on: req).map(to: Response.self) { _ in 79 | return try req.response.maintenanceFinished(message: "Uninstall finished, there are no data nor tables in the database; Please run `/install` before you continue") 80 | } 81 | } 82 | } 83 | 84 | /// Install all tables and data if neccessary 85 | private static func install(on req: Request) throws -> Future { 86 | return try install(files: req).flatMap({ 87 | return try install(migrations: req).map({ 88 | return try req.response.maintenanceFinished(message: "Installation finished, login as core@liveui.io with password sup3rS3cr3t") 89 | }) 90 | }) 91 | } 92 | 93 | /// Install base files 94 | private static func install(files req: Request) throws -> Future { 95 | return try Logo.install(on: req) 96 | } 97 | 98 | /// Install basic database data 99 | private static func install(migrations req: Request) throws -> Future { 100 | let migrations = FluentProvider.init() 101 | return try migrations.didBoot(req).flatMap(to: Void.self) { _ in 102 | return User.query(on: req).count().flatMap(to: Void.self) { count in 103 | if count > 0 { 104 | throw Error.dataExists 105 | } 106 | let user = try su(on: req) 107 | user.verified = true 108 | return user.save(on: req).flatMap(to: Void.self) { user in 109 | return adminTeam.save(on: req).flatMap(to: Void.self) { team in 110 | var futures = [ 111 | team.users.attach(user, on: req).flatten() 112 | ] 113 | try ApiCoreBase.installFutures.forEach({ closure in 114 | futures.append(try closure(req)) 115 | }) 116 | return futures.flatten(on: req) 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /Sources/ApiCore/Controllers/LogsController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogsController.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 11/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import FluentPostgreSQL 11 | 12 | #if os(Linux) 13 | import Glibc 14 | #else 15 | import Darwin.C 16 | #endif 17 | 18 | 19 | public class LogsController: Controller { 20 | 21 | /// Setup routes 22 | public static func boot(router: Router, secure: Router, debug: Router) throws { 23 | // Print out logged errors 24 | secure.get("errors") { req -> Future<[ErrorLog]> in 25 | return ErrorLog.query(on: req).sort(\ErrorLog.added, .descending).all() 26 | } 27 | 28 | // Flush system logs 29 | debug.get("flush") { req -> Response in 30 | fflush(stdout) 31 | return try req.response.success(status: .ok, code: "system", description: "Flushed") 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ApiCore/Controllers/SettingsController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsController.swift 3 | // SettingsCore 4 | // 5 | // Created by Ondrej Rafaj on 15/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import ErrorsCore 11 | import FluentPostgreSQL 12 | 13 | 14 | public class SettingsController: Controller { 15 | 16 | public static func boot(router: Router, secure: Router, debug: Router) throws { 17 | router.get("settings") { (req) -> Future in 18 | return Setting.query(on: req).all().flatMap(to: Response.self) { settings in 19 | if req.query.plain == true { 20 | var dic: [String: String] = [:] 21 | settings.forEach({ setting in 22 | dic[setting.name] = setting.config 23 | }) 24 | return try dic.asJson().asResponse(.ok, to: req) 25 | } else { 26 | return try settings.asResponse(.ok, to: req) 27 | } 28 | } 29 | } 30 | 31 | router.get("settings", DbIdentifier.parameter) { (req) -> Future in 32 | let id = try req.parameters.next(DbIdentifier.self) 33 | return Setting.query(on: req).filter(\Setting.id == id).first().flatMap(to: Response.self) { setting in 34 | guard let setting = setting else { 35 | throw ErrorsCore.HTTPError.notFound 36 | } 37 | if req.query.plain == true { 38 | return try setting.config.asResponse(.ok, to: req) 39 | } else { 40 | return try setting.asResponse(.ok, to: req) 41 | } 42 | } 43 | } 44 | 45 | secure.post("settings") { (req) -> Future in 46 | return try req.me.isSystemAdmin().flatMap(to: Response.self) { admin in 47 | guard admin else { 48 | throw ErrorsCore.HTTPError.notAuthorizedAsAdmin 49 | } 50 | return try req.content.decode(Setting.self).flatMap(to: Response.self) { updatedSetting in 51 | return try updatedSetting.save(on: req).asResponse(.created, to: req) 52 | } 53 | } 54 | } 55 | 56 | secure.put("settings", DbIdentifier.parameter) { (req) -> Future in 57 | return try req.me.isSystemAdmin().flatMap(to: Setting.self) { admin in 58 | guard admin else { 59 | throw ErrorsCore.HTTPError.notAuthorizedAsAdmin 60 | } 61 | let id = try req.parameters.next(DbIdentifier.self) 62 | return try req.content.decode(Setting.self).flatMap(to: Setting.self) { updatedSetting in 63 | return Setting.query(on: req).filter(\Setting.id == id).first().flatMap(to: Setting.self) { setting in 64 | guard let setting = setting else { 65 | throw ErrorsCore.HTTPError.notFound 66 | } 67 | updatedSetting.id = setting.id 68 | return updatedSetting.save(on: req) 69 | } 70 | } 71 | } 72 | } 73 | 74 | secure.delete("settings", DbIdentifier.parameter) { (req) -> Future in 75 | return try req.me.isSystemAdmin().flatMap(to: Response.self) { admin in 76 | guard admin else { 77 | throw ErrorsCore.HTTPError.notAuthorizedAsAdmin 78 | } 79 | let id = try req.parameters.next(DbIdentifier.self) 80 | return try Setting.query(on: req).filter(\Setting.id == id).delete().asResponse(to: req) 81 | } 82 | } 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/ApiCore/Database/ApiCoreDb.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreDb.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 20/09/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | import FluentPostgreSQL 12 | 13 | 14 | class ApiCoreDb { 15 | 16 | /// Database configuration 17 | public static func config(hostname: String, user: String, password: String?, database: String, port: Int = DbDefaultPort) -> DatabasesConfig { 18 | var databaseConfig = DatabasesConfig() 19 | let config = PostgreSQLDatabaseConfig(hostname: hostname, port: port, username: user, database: database, password: password) 20 | let database = ApiCoreDatabase(config: config) 21 | databaseConfig.add(database: database, as: .psql) 22 | 23 | // Enable SQL logging if required 24 | if ApiCoreBase.configuration.database.logging { 25 | databaseConfig.enableLogging(on: .psql) 26 | } 27 | 28 | return databaseConfig 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Sources/ApiCore/Database/DbCoreModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DbCoreModel.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 20/09/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | import FluentPostgreSQL 12 | 13 | 14 | /// Default DbCore model protocol 15 | public protocol DbCoreModel: PostgreSQLUUIDModel, Content, Equatable { } 16 | 17 | 18 | // MARK: - Equating 19 | 20 | extension DbCoreModel { 21 | 22 | public typealias Database = ApiCoreDatabase 23 | 24 | public static func ==(lhs: Self, rhs: Self) -> Bool { 25 | return lhs.id == rhs.id 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ApiCore/Database/DbError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DbError.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 20/09/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import ErrorsCore 11 | 12 | 13 | /// Database error 14 | public enum DbError: FrontendError { 15 | 16 | /// Insert operation has failed 17 | case insertFailed 18 | 19 | /// Update operation has failed 20 | case updateFailed 21 | 22 | /// Delete operation has failed 23 | case deleteFailed 24 | 25 | /// Error code 26 | public var identifier: String { 27 | switch self { 28 | case .insertFailed: 29 | return "db_error.insert_failed" 30 | case .updateFailed: 31 | return "db_error.update_failed" 32 | case .deleteFailed: 33 | return "db_error.delete_failed" 34 | } 35 | } 36 | 37 | /// Server status code 38 | public var status: HTTPStatus { 39 | return .internalServerError 40 | } 41 | 42 | /// Error reason 43 | public var reason: String { 44 | switch self { 45 | case .insertFailed: 46 | return "Insert failed" 47 | case .updateFailed: 48 | return "Update failed" 49 | case .deleteFailed: 50 | return "Delete failed" 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Content+Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Content+Response.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 13/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Content { 13 | 14 | /// Convert Content to a response 15 | public func asResponse(_ status: HTTPStatus, to req: Request) throws -> Future { 16 | return try encode(for: req).map(to: Response.self) { 17 | $0.http.status = status 18 | $0.http.headers.replaceOrAdd(name: HTTPHeaderName.contentType, value: "application/json; charset=utf-8") 19 | return $0 20 | } 21 | } 22 | 23 | } 24 | 25 | extension Decodable { 26 | 27 | /// Create and fill object from POST data 28 | public static func fill(post req: Request) throws -> Future { 29 | return try req.content.decode(Self.self) 30 | } 31 | 32 | /// Create and fill object from GET data 33 | public static func fill(get req: Request) throws -> Self { 34 | return try req.query.decode(Self.self) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Data+Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Response.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 15/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Data { 13 | 14 | /// Convert Data to a Response 15 | public func asResponse(_ status: HTTPStatus, contentType: String = "application/json; charset=utf-8", to req: Request) throws -> Future { 16 | let response = try req.response.basic(status: status) 17 | response.http.headers.replaceOrAdd(name: .contentType, value: contentType) 18 | response.http.body = HTTPBody(data: self) 19 | return req.eventLoop.newSucceededFuture(result: response) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Data+Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Tools.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 11/09/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Data { 13 | 14 | public func asUTF8String() -> String? { 15 | return String(data: self, encoding: .utf8) 16 | } 17 | 18 | } 19 | 20 | 21 | extension EventLoopFuture where T == Data { 22 | 23 | public func mapToImageResponse(on req: Request) -> EventLoopFuture { 24 | return self.map(to: Response.self) { data in 25 | let response = try req.response.image(data) 26 | return response 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/DatabaseIdentifier+Db.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseIdentifier+Db.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 20/09/2018. 6 | // 7 | 8 | import Foundation 9 | import Fluent 10 | import FluentPostgreSQL 11 | 12 | 13 | extension DatabaseIdentifier { 14 | 15 | /// Default databse identifier 16 | public static var db: DatabaseIdentifier { 17 | return .psql 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Date+Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Tools.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 14/01/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension Date { 12 | 13 | /// Add n number of months 14 | public func addMonth(n: Int) -> Date { 15 | let cal = NSCalendar.current 16 | return cal.date(byAdding: .month, value: n, to: self)! 17 | } 18 | 19 | /// Add n number of days 20 | public func addDay(n: Int) -> Date { 21 | let cal = NSCalendar.current 22 | return cal.date(byAdding: .day, value: n, to: self)! 23 | } 24 | 25 | /// Add n number of minutes 26 | public func addMinute(n: Int) -> Date { 27 | let cal = NSCalendar.current 28 | return cal.date(byAdding: .minute, value: n, to: self)! 29 | } 30 | 31 | /// Add n number of seconds 32 | public func addSec(n: Int) -> Date { 33 | let cal = NSCalendar.current 34 | return cal.date(byAdding: .second, value: n, to: self)! 35 | } 36 | 37 | /// Day in a month 38 | public var day: Int { 39 | let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian) 40 | return calendar?.component(NSCalendar.Unit.day, from: self) ?? 0 41 | } 42 | 43 | /// Month in a year 44 | public var month: Int { 45 | let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian) 46 | return calendar?.component(NSCalendar.Unit.month, from: self) ?? 0 47 | } 48 | 49 | /// Year 50 | public var year: Int { 51 | let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian) 52 | return calendar?.component(NSCalendar.Unit.year, from: self) ?? 0 53 | } 54 | 55 | /// Date folder path (YYYY/mm/dd) 56 | public var dateFolderPath: String { 57 | return "\(year)/\(month)/\(day)" 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Encodable+Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encodable+Tools.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 22/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Encodable { 13 | 14 | /// Convert to a PLIST 2.0 formatted Data 15 | public func asPropertyList() throws -> Data { 16 | let jsonData = try JSONEncoder().encode(self) 17 | let data = try JSONSerialization.jsonObject(with: jsonData, options: []) 18 | let plistData = try PropertyListSerialization.data(fromPropertyList: data, format: .xml, options: 0) 19 | return plistData 20 | } 21 | 22 | /// Convert to JSON Data 23 | public func asJson() throws -> Data { 24 | let encoder = JSONEncoder() 25 | if #available(macOS 10.12, *) { 26 | encoder.dateEncodingStrategy = .iso8601 27 | } else { 28 | fatalError("macOS SDK < 10.12 detected, no ISO-8601 JSON support") 29 | } 30 | let jsonData = try encoder.encode(self) 31 | return jsonData 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/EventLoop+Future.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventLoop+Future.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 22/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import NIO 11 | 12 | 13 | extension EventLoop { 14 | 15 | /// New succeeded Void future 16 | public func newSucceededVoidFuture() -> Future { 17 | return newSucceededFuture(result: Void()) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/FileCore/CoreManager+Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreManager+Location.swift 3 | // FileCore 4 | // 5 | // Created by Ondrej Rafaj on 25/05/2018. 6 | // 7 | 8 | import Foundation 9 | @_exported import FileCore 10 | @_exported import Vapor 11 | import S3 12 | 13 | 14 | extension CoreManager { 15 | 16 | /// Public URL for file 17 | public func url(for path: String, on req: Request) throws -> String { 18 | if ApiCoreBase.configuration.storage.s3.enabled { 19 | let s3 = try req.makeS3Client() 20 | let url = try s3.url(fileInfo: path, on: req) 21 | return url.absoluteString 22 | } else { 23 | let url = req.serverURL().appendingPathComponent(path).absoluteString 24 | return url 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Future+Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+Response.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 13/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Future where T: ResponseEncodable { 13 | 14 | /// Turn Future into a Future (.ok by default) 15 | public func asResponse(_ status: HTTPStatus = .ok, to req: Request) throws -> Future { 16 | return self.flatMap(to: Response.self) { try $0.encode(for: req) }.map(to: Response.self) { 17 | $0.http.status = status 18 | $0.http.headers.replaceOrAdd(name: HTTPHeaderName.contentType, value: "application/json; charset=utf-8") 19 | return $0 20 | } 21 | } 22 | 23 | /// Turn Future into a Future with text/html Content-Type (.ok by default) 24 | public func asHtmlResponse(_ status: HTTPStatus = .ok, to req: Request) throws -> Future { 25 | return self.flatMap(to: Response.self) { try $0.encode(for: req) }.map(to: Response.self) { 26 | $0.http.status = status 27 | $0.http.headers.replaceOrAdd(name: HTTPHeaderName.contentType, value: "text/html; charset=utf-8") 28 | return $0 29 | } 30 | } 31 | 32 | } 33 | 34 | extension Future where T == Void { 35 | 36 | /// Turn Future into a Future (204 - No content) 37 | public func asResponse(to req: Request) throws -> Future { 38 | return self.map(to: Response.self) { _ in 39 | return try req.response.noContent() 40 | } 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Future+Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+Tools.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 14/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Future { 13 | 14 | /// Flatten any Future into Future 15 | public func flatten() -> Future { 16 | return map(to: Void.self) { (_) -> Void in 17 | return Void() 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/HTTPHeaders+Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPHeaders+Tools.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 14/01/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension HTTPHeaders { 13 | 14 | /// Return value of an authorization header 15 | /// Stripping any prefix like Token or Bearer 16 | public var authorizationToken: String? { 17 | guard let token = self[HTTPHeaderName.authorization].first else { 18 | return nil 19 | } 20 | let parts = token.split(separator: " ") 21 | guard parts.count == 2 else { 22 | return nil 23 | } 24 | return String(parts[1]) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Model+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model+Helpers.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 01/12/2018. 6 | // 7 | 8 | import Foundation 9 | import ErrorsCore 10 | 11 | 12 | extension DbCoreModel { 13 | 14 | public func guaranteedId() throws -> DbIdentifier { 15 | guard let id = id else { 16 | throw ErrorsCore.HTTPError.missingId 17 | } 18 | return id 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Request+Auth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Auth.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 01/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Request { 13 | 14 | /// Me instance for current request 15 | public var me: Me { 16 | return Me(self) 17 | } 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Request+Files.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Files.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 22/01/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Request { 13 | 14 | /// Return file data from the request 15 | public var fileData: Future { 16 | let mb = Double(ApiCoreBase.configuration.server.maxUploadFilesize ?? 50) 17 | return http.body.consumeData(max: Int(Filesize.megabyte(mb).value), on: self) 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Request+URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+URL.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 22/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Request { 13 | 14 | /// Server's public URL 15 | public func serverURL() -> URL { 16 | return Me.serverURL() 17 | } 18 | 19 | /// Server's public base URL 20 | public func serverBaseUrl() -> URL { 21 | return serverURL().deletingPathExtension() 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Response+Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response+Tools.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 09/04/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Response { 13 | 14 | /// Return as a succeeded Future 15 | public func asFuture(on req: Request) -> Future { 16 | let future = req.eventLoop.newSucceededFuture(result: self) 17 | return future 18 | } 19 | 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/Router+Extended.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router+Extended.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 17/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | extension Router { 12 | 13 | /// OPTIONS request 14 | @discardableResult public func options(_ path: PathComponentsRepresentable..., use closure: @escaping (Request) throws -> T) -> Route where T: ResponseEncodable { 15 | return self.on(.OPTIONS, at: path, use: closure) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/String+Crypto.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Crypto.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 14/01/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Crypto 11 | import ErrorsCore 12 | 13 | 14 | extension String { 15 | 16 | /// Hashed password 17 | public func passwordHash(_ worker: BasicWorker) throws -> String { 18 | let cost = try Environment.detect().isRelease ? 12 : 4 19 | let hashedString = try BCrypt.hash(self, cost: cost) 20 | return hashedString 21 | } 22 | 23 | /// Verify password 24 | public func verify(against storedHash: String) -> Bool { 25 | let ok = (try? BCrypt.verify(self, created: storedHash)) ?? false 26 | return ok 27 | } 28 | 29 | /// Base64 decoded string 30 | public var base64Decoded: String? { 31 | guard let decodedData = Data(base64Encoded: self), let decodedString = String(data: decodedData, encoding: .utf8) else { 32 | return nil 33 | } 34 | return decodedString 35 | } 36 | 37 | /// MD5 of a string 38 | public var md5: String? { 39 | guard let data = data(using: .utf8) else { return nil } 40 | return try? MD5.hash(data).hexEncodedString() 41 | } 42 | 43 | /// SHA256 of a string 44 | public func sha() throws -> String { 45 | guard let data = data(using: .utf8) else { 46 | throw ErrorsCore.HTTPError.missingAuthorizationData 47 | } 48 | return try SHA256.hash(data).hexEncodedString() 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/String+Manipulation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Manipulation.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 18/01/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension String { 12 | 13 | /// Convert to safe text (convert-to-safe-text) 14 | public var safeText: String { 15 | var text = components(separatedBy: CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_").inverted).joined(separator: "-").lowercased() 16 | text = text.components(separatedBy: CharacterSet(charactersIn: "-")).filter { !$0.isEmpty }.joined(separator: "-") 17 | return text 18 | } 19 | 20 | /// Snake case from dotted syntax 21 | public func snake_cased() -> String { 22 | let text = split(separator: ".").joined(separator: "_") 23 | return text 24 | } 25 | 26 | /// Masked name 27 | public var maskedName: String { 28 | var text = components(separatedBy: CharacterSet.alphanumerics.inverted).joined(separator: "-").lowercased() 29 | text = text.components(separatedBy: CharacterSet(charactersIn: "-")).filter { !$0.isEmpty }.joined(separator: "-") 30 | return text 31 | } 32 | 33 | /// Gravatar MD5 hash from an email 34 | public var imageUrlHashFromMail: String { 35 | return md5 ?? "" 36 | } 37 | 38 | /// Name inititials (two letters) from a string 39 | public var initials: String { 40 | if count == 0 { 41 | return "??" 42 | } else if count <= 2 { 43 | return uppercased() 44 | } 45 | let capitals = filter { ("A"..."Z").contains($0) } 46 | if capitals.count < 2 { 47 | let capitalizedString = split(separator: " ").map { element -> String in 48 | element.capitalized 49 | }.joined(separator: " ") 50 | let capitals = capitalizedString.filter { ("A"..."Z").contains($0) } 51 | if capitals.count >= 2 { 52 | return String(String(capitals).prefix(2)).uppercased() 53 | } 54 | return uppercased().initials 55 | } 56 | return String(String(capitals).prefix(2)).uppercased() 57 | } 58 | 59 | /// Convert string to boolean if possible 60 | public func asBool() -> Bool? { 61 | switch self.lowercased() { 62 | case "true", "yes", "1": 63 | return true 64 | case "false", "no", "0": 65 | return false 66 | default: 67 | return nil 68 | } 69 | } 70 | 71 | // MARK: Internal tools only (not worth exposing) 72 | 73 | /// Get domain from an email 74 | func domainFromEmail() -> String? { 75 | let parts = split(separator: "@") 76 | guard parts.count == 2, let domain = parts.last else { 77 | return nil 78 | } 79 | return String(domain.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)) 80 | } 81 | 82 | } 83 | 84 | 85 | extension Optional where Wrapped == String { 86 | 87 | /// Convert optional string to boolean 88 | public func asBool() -> Bool { 89 | switch self?.lowercased() { 90 | case "true", "yes", "1": 91 | return true 92 | case "false", "no", "0": 93 | return false 94 | default: 95 | return false 96 | } 97 | } 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /Sources/ApiCore/Extensions/String+Security.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Security.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 12/09/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | extension String { 12 | 13 | /// Validate password 14 | public func validatePassword() throws -> Bool { 15 | guard count > 6 else { 16 | throw AuthError.invalidPassword(reason: .tooShort) 17 | } 18 | // TODO: Needs stronger validation!!!!!!!!! 19 | return true 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Audit/Audit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Audit.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 08/01/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | public protocol Audit { 13 | 14 | static func issues(for req: Request) throws -> EventLoopFuture<[ServerSecurity.Issue]> 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Audit/ConfigurationAudit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationAudit.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 08/01/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | public class ConfigurationAudit: Audit { 13 | 14 | public static var customIssues: [ServerSecurity.Issue] = [] 15 | 16 | public static func issues(for req: Request) throws -> EventLoopFuture<[ServerSecurity.Issue]> { 17 | var array: [EventLoopFuture] = [] 18 | try array.append(check(icon: req)) 19 | return array.map(to: [ServerSecurity.Issue].self, on: req, { issues in 20 | var arr = issues.compactMap({ $0 }) 21 | arr.append(contentsOf: customIssues) 22 | arr.append(contentsOf: nonFutureChecks()) 23 | return arr 24 | }) 25 | } 26 | 27 | public static func nonFutureChecks() -> [ServerSecurity.Issue] { 28 | var arr: [ServerSecurity.Issue] = [] 29 | if ApiCoreBase.configuration.mail.mailgun.domain == "sandbox-domain.mailgun.org" || 30 | ApiCoreBase.configuration.mail.mailgun.key == "secret-key" { 31 | arr.append( 32 | ServerSecurity.Issue( 33 | category: .danger, 34 | code: "email_not_configured", 35 | issue: "Email has not been configured" 36 | ) 37 | ) 38 | } 39 | return arr 40 | } 41 | 42 | public static func check(icon req: Request) throws -> EventLoopFuture { 43 | return try ServerIcon.icon(exists: .favicon, on: req).map(to: ServerSecurity.Issue?.self) { exists in 44 | if !exists { 45 | return ServerSecurity.Issue( 46 | category: .warning, 47 | code: "server_icon_not_set", 48 | issue: "Server icon has not been set" 49 | ) 50 | } else { 51 | return nil 52 | } 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Audit/SecurityAudit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecurityAudit.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 08/01/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | public class SecurityAudit: Audit { 13 | 14 | public static var customIssues: [ServerSecurity.Issue] = [] 15 | 16 | public static func issues(for req: Request) throws -> EventLoopFuture<[ServerSecurity.Issue]> { 17 | var array: [EventLoopFuture] = [] 18 | array.append(check(defaultLoginOn: req)) 19 | return array.map(to: [ServerSecurity.Issue].self, on: req, { issues in 20 | var arr = issues.compactMap({ $0 }) 21 | arr.append(contentsOf: customIssues) 22 | arr.append(contentsOf: nonFutureChecks()) 23 | return arr 24 | }) 25 | } 26 | 27 | public static func nonFutureChecks() -> [ServerSecurity.Issue] { 28 | var arr: [ServerSecurity.Issue] = [] 29 | if ApiCoreBase.configuration.jwtSecret == "secret" { 30 | arr.append( 31 | ServerSecurity.Issue( 32 | category: .danger, 33 | code: "default_secret_for_jwt", 34 | issue: "Default JWT secret is set to be 'secret' which is, well, not very secret. This can be set as an ENV variable 'APICORE_JWT_SECRET'." 35 | ) 36 | ) 37 | } 38 | return arr 39 | } 40 | 41 | public static func check(defaultLoginOn req: Request) -> EventLoopFuture { 42 | return UsersManager.get(user: "core@liveui.io", password: "sup3rS3cr3t", on: req).map(to: ServerSecurity.Issue?.self) { user in 43 | if user != nil { 44 | return ServerSecurity.Issue( 45 | category: .danger, 46 | code: "default_user_exists", 47 | issue: "Default user with publicly known username and password exists (core@liveui.io/sup3rS3cr3t). Please change the password or delete the user." 48 | ) 49 | } 50 | return nil 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/AuthError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthError.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 17/01/2018. 6 | // 7 | 8 | import Foundation 9 | import ErrorsCore 10 | import Vapor 11 | 12 | 13 | // QUESTION: Do we want to bring these in to the HTTPError or generic error in ErrorsCore? 14 | /// Authentication error 15 | public enum AuthError: FrontendError { 16 | 17 | /// Invalid input value reason 18 | public enum InvalidInputReason { 19 | 20 | /// Generic problem 21 | case generic 22 | 23 | /// Input is too short 24 | case tooShort 25 | 26 | /// Input doesn't match verification value 27 | case notMatching 28 | 29 | /// Needs special characters 30 | case needsSpecialCharacters 31 | 32 | /// Needs numeric values 33 | case needsNumericCharacters 34 | 35 | /// Custom reason 36 | case custom(String) 37 | 38 | /// Reason description 39 | public var description: String { 40 | switch self { 41 | case .generic: 42 | return "Password is invalid" 43 | case .tooShort: 44 | return "Value is too short" 45 | case .notMatching: 46 | return "Value doesn't match its verification" 47 | case .needsSpecialCharacters: 48 | return "Value needs additional special characters" 49 | case .needsNumericCharacters: 50 | return "Value needs numbers" 51 | case .custom(let message): 52 | return message 53 | } 54 | } 55 | 56 | } 57 | 58 | /// Authentication has failed 59 | case authenticationFailed 60 | 61 | /// Authentication token has expired 62 | case expiredToken 63 | 64 | /// Server error 65 | case serverError 66 | 67 | /// Email is invalid 68 | case invalidEmail 69 | 70 | /// Password is invalid 71 | case invalidPassword(reason: InvalidInputReason) 72 | 73 | /// Invalid token signature 74 | case invalidToken 75 | 76 | /// Account has not been verified yet 77 | case unverifiedAccount 78 | 79 | /// Account has been disabled 80 | case disabledAccount 81 | 82 | /// Email already exists 83 | case emailExists 84 | 85 | /// Email failed to be send 86 | case emailFailedToSend 87 | 88 | /// Error code 89 | public var identifier: String { 90 | switch self { 91 | case .authenticationFailed, .invalidEmail, .invalidPassword, .expiredToken: 92 | return "auth_error.authentication_failed" 93 | case .serverError: 94 | return "auth_error.server_error" 95 | case .emailFailedToSend: 96 | return "auth.email_failed" 97 | case .unverifiedAccount: 98 | return "auth.unverified_account" 99 | case .disabledAccount: 100 | return "auth.disabled_account" 101 | case .emailExists: 102 | return "auth.email_exists" 103 | case .invalidToken: 104 | return "auth.invalid_recovery_token" 105 | } 106 | } 107 | 108 | /// HTTP status code for the error 109 | public var status: HTTPStatus { 110 | switch self { 111 | case .authenticationFailed, .expiredToken: 112 | return .unauthorized 113 | case .invalidEmail, .invalidPassword: 114 | return .notAcceptable 115 | case .invalidToken, .unverifiedAccount, .emailExists: 116 | return .preconditionFailed 117 | default: 118 | return .internalServerError 119 | } 120 | } 121 | 122 | /// Reason for the error 123 | public var reason: String { 124 | switch self { 125 | case .authenticationFailed: 126 | return "Authentication has failed" 127 | case .expiredToken: 128 | return "Authentication token has expired" 129 | case .serverError: 130 | return "Server error" 131 | case .invalidEmail: 132 | return "Invalid email" 133 | case .invalidPassword(let reason): 134 | return "Invalid password (\(reason.description))" 135 | case .emailFailedToSend: 136 | return "Failed to send an email, please try again or contact system administrator" 137 | case .unverifiedAccount: 138 | return "Account has not been verified yet" 139 | case .disabledAccount: 140 | return "Account has been disabled" 141 | case .emailExists: 142 | return "Email already exists" 143 | case .invalidToken: 144 | return "Invalid recovery token" 145 | } 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Coding/Decodable+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Decodable+Helpers.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 07/02/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /// Decodable property 12 | public struct DecodableProperty { } 13 | 14 | 15 | extension DecodableProperty where ModelType: Decodable { 16 | 17 | /// Decode (fill) data from JSON file 18 | public static func fromJSON(file fileUrl: URL) throws -> ModelType { 19 | let data = try Data(contentsOf: fileUrl) 20 | return try fromJSON(data: data) 21 | } 22 | 23 | /// Decode (fill) data from JSON file 24 | public static func fromJSON(path: String) throws -> ModelType { 25 | let url = URL(fileURLWithPath: path) 26 | return try fromJSON(file: url) 27 | } 28 | 29 | /// Decode (fill) data from JSON string 30 | public static func fromJSON(string: String) throws -> ModelType { 31 | guard let data = string.data(using: .utf8) else { 32 | fatalError("Invalid string") 33 | } 34 | return try fromJSON(data: data) 35 | } 36 | 37 | /// Decode (fill) data from JSON data 38 | public static func fromJSON(data: Data) throws -> ModelType { 39 | let decoder = JSONDecoder() 40 | let object = try decoder.decode(ModelType.self, from: data) 41 | return object 42 | } 43 | 44 | } 45 | 46 | 47 | /// Decodable helper protocol 48 | public protocol DecodableHelper { 49 | 50 | /// Model type 51 | associatedtype ModelType 52 | 53 | /// Quick access to the decodable functionality 54 | static var decode: DecodableProperty.Type { get } 55 | 56 | } 57 | 58 | 59 | extension DecodableHelper { 60 | 61 | /// Quick access to the decodable functionality 62 | public static var decode: DecodableProperty.Type { 63 | return DecodableProperty.self 64 | } 65 | 66 | } 67 | 68 | 69 | public protocol JSONDecodable: Decodable, DecodableHelper { } 70 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Env.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment.swift 3 | // BoostCore 4 | // 5 | // Created by Ondrej Rafaj on 08/02/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | public struct Env { 13 | 14 | /// All available environmental values 15 | static var data: [String: String] { 16 | return ProcessInfo.processInfo.environment as [String: String] 17 | } 18 | 19 | /// Print all available environmental values 20 | public static func print() { 21 | if (try? Environment.detect()) ?? .production == .development { 22 | Swift.print("Environment variables:") 23 | data.sorted(by: { (item1, item2) -> Bool in 24 | item1.key < item2.key 25 | }).forEach { item in 26 | Swift.print("\t\(item.key)=\(item.value)") 27 | } 28 | Swift.print("\n") 29 | } else { 30 | Swift.print("Environment variables are only displayed in development/debug mode") 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Filesize.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Filesize.swift 4 | // ApiCore 5 | // 6 | // Created by Ondrej Rafaj on 22/01/2018. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// Filesize 13 | public enum Filesize { 14 | 15 | /// Kilobytes with ammount 16 | case kilobyte(Double) 17 | 18 | /// Megabytes with ammount 19 | case megabyte(Double) 20 | 21 | /// Gigabytes with ammount 22 | case gigabyte(Double) 23 | 24 | /// Calculated value 25 | public var value: Double { 26 | switch self { 27 | case .kilobyte(let no): 28 | return (no * 1000) 29 | case .megabyte(let no): 30 | return ((no * 1000) * 1000) 31 | case .gigabyte(let no): 32 | return (((no * 1000) * 1000) * 1000) 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Gravatar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gravatar.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 09/03/2018. 6 | // 7 | 8 | import Foundation 9 | import ErrorsCore 10 | import Vapor 11 | 12 | 13 | /// Gravatar 14 | public struct Gravatar { 15 | 16 | /// Error 17 | public enum Error: FrontendError { 18 | 19 | /// Unable to create MD5 from email 20 | case unableToCreateMD5FromEmail 21 | 22 | public var status: HTTPStatus { 23 | return .internalServerError 24 | } 25 | 26 | public var identifier: String { 27 | return "gravatar.unable_create_MD5_from_email" 28 | } 29 | 30 | public var reason: String { 31 | return "Unable to create MD5 from the given email" 32 | } 33 | 34 | } 35 | 36 | /// Generate gravatar link from an email 37 | public static func link(fromEmail email: String, size: Float? = nil) throws -> String { 38 | guard let md5 = email.md5 else { 39 | throw Error.unableToCreateMD5FromEmail 40 | } 41 | var url = "https://www.gravatar.com/avatar/\(md5)" 42 | if let size = size { 43 | url.append("?size=\(size)") 44 | } 45 | return url 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Images/Icon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Icon.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 17/05/2018. 6 | // 7 | 8 | import Foundation 9 | @_exported import SwiftGD 10 | 11 | 12 | /// Icon size 13 | public enum IconSize: Int, Codable { 14 | 15 | /// Favicon, 16x16 px 16 | case favicon = 16 17 | 18 | /// 64x64 px 19 | case at1x = 64 20 | 21 | /// 128x128 px 22 | case at2x = 128 23 | 24 | /// 192x192 px 25 | case at3x = 192 26 | 27 | /// 256x256 px 28 | case regular = 256 29 | 30 | /// 512x512 px 31 | case large = 512 32 | 33 | 34 | /// Size 35 | public var size: Size { 36 | return Size(width: rawValue, height: rawValue) 37 | } 38 | 39 | /// All values 40 | public static let all: [IconSize] = [ 41 | .favicon, 42 | .at1x, 43 | .at2x, 44 | .at3x, 45 | .regular, 46 | .large 47 | ] 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Me.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Me.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 13/05/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | import FluentPostgreSQL 12 | //import DbCore 13 | import ErrorsCore 14 | 15 | 16 | /// Information about currently authenticated request 17 | public struct Me { 18 | 19 | /// Current request 20 | let request: Request 21 | 22 | /// Initializer 23 | init(_ request: Request) { 24 | self.request = request 25 | } 26 | 27 | /// Currently authorized user 28 | public func user() throws -> User { 29 | let authenticationCache = try request.make(AuthenticationCache.self) 30 | guard let user = authenticationCache[User.self] else { 31 | throw ErrorsCore.HTTPError.notAuthorized 32 | } 33 | return user 34 | } 35 | 36 | /// Teams for currently authorized user 37 | public func teams() throws -> Future { 38 | let me = try user() 39 | return try me.teams.query(on: self.request).all() 40 | } 41 | 42 | /// Is currently authorized user a system admin 43 | public func isSystemAdmin() throws -> Future { 44 | let me = try user() 45 | return try me.teams.query(on: self.request).all().map(to: Bool.self) { teams in 46 | return teams.containsAdmin 47 | } 48 | } 49 | 50 | /// Team verified to contain currently authorized user 51 | public func verifiedTeam(id teamId: DbIdentifier) throws -> Future { 52 | let me = try user() 53 | return try me.teams.query(on: self.request).filter(\Team.id == teamId).first().map(to: Team.self) { team in 54 | guard let team = team else { 55 | throw ErrorsCore.HTTPError.notFound 56 | } 57 | return team 58 | } 59 | } 60 | 61 | /// Server URL 62 | 63 | public static func serverURL() -> URL { 64 | let stringUrl = ApiCoreBase.configuration.server.url ?? "http://localhost:8080" 65 | guard var url = URL(string: stringUrl) else { 66 | fatalError("Invalid server URL: \(stringUrl)") 67 | } 68 | if let prefix = ApiCoreBase.configuration.server.pathPrefix, !prefix.isEmpty { 69 | url.appendPathComponent(prefix) 70 | } 71 | return url 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/RequestIdService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestIdService.swift 3 | // BoostCore 4 | // 5 | // Created by Ondrej Rafaj on 07/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | final class RequestIdService: Service, ServiceType { 13 | 14 | /// Make service 15 | static func makeService(for worker: Container) throws -> RequestIdService { 16 | return RequestIdService() 17 | } 18 | 19 | /// Generate random UUID 20 | let uuid = UUID() 21 | 22 | } 23 | 24 | extension Request { 25 | 26 | /// Session Id 27 | /// *Unique for each request* 28 | public var sessionId: UUID { 29 | return try! self.privateContainer.make(RequestIdService.self).uuid 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 24/01/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /// Generic result object 12 | public enum Result { 13 | 14 | /// Complete 15 | case complete 16 | 17 | /// Success with generic result 18 | case success(T) 19 | 20 | /// Error 21 | case error(Swift.Error) 22 | 23 | /// Did result succeed? 24 | public var success: Bool { 25 | switch self { 26 | case .error(_): 27 | return false 28 | default: 29 | return true 30 | } 31 | } 32 | 33 | /// Error if available 34 | public var error: Swift.Error? { 35 | switch self { 36 | case .error(let error): 37 | return error 38 | default: 39 | return nil 40 | } 41 | } 42 | 43 | /// Generic object if successful 44 | public var object: T? { 45 | switch self { 46 | case .success(let object): 47 | return object 48 | default: 49 | return nil 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/ServerIcon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerIcon.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 08/01/2019. 6 | // 7 | 8 | import Foundation 9 | import FileCore 10 | import Vapor 11 | 12 | 13 | public class ServerIcon { 14 | 15 | public static func icon(size: IconSize = .regular, on req: Request) throws -> EventLoopFuture { 16 | return try icon(exists: size, on: req).flatMap() { exists in 17 | guard exists else { 18 | let data = Logo.data 19 | return try Logo.create(from: data, on: req).map() { _ in 20 | return data 21 | } 22 | } 23 | let fm = try req.makeFileCore() 24 | let fileName = "server/image/\(size.rawValue)" 25 | return try fm.get(file: fileName, on: req) 26 | } 27 | } 28 | 29 | public static func icon(exists size: IconSize, on req: Request) throws -> EventLoopFuture { 30 | let fm = try req.makeFileCore() 31 | let fileName = "server/image/\(size.rawValue)" 32 | return try fm.exists(file: fileName, on: req) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ApiCore/Libs/Templates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Templates.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 29/05/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import ErrorsCore 11 | import Leaf 12 | 13 | 14 | public class Templator: Service { 15 | 16 | public struct Templates: Codable { 17 | 18 | public struct Template: Codable { 19 | 20 | public let name: String 21 | 22 | public let link: String 23 | 24 | } 25 | 26 | public let name: String 27 | 28 | public let items: [Template] 29 | 30 | } 31 | 32 | public enum Error: FrontendError { 33 | 34 | case templateMissing(String) 35 | case invalidTemplateData(String) 36 | 37 | public var status: HTTPStatus { 38 | return .internalServerError 39 | } 40 | 41 | public var identifier: String { 42 | return "templator.missing_template" 43 | } 44 | 45 | public var reason: String { 46 | return "Template is missing" 47 | } 48 | 49 | } 50 | 51 | public let packageUrl: String 52 | 53 | public init(packageUrl: String) throws { 54 | self.packageUrl = packageUrl 55 | 56 | try loadTemplates() 57 | } 58 | 59 | public func get(name: String, data: C?, on req: Request) throws -> EventLoopFuture where C: Content { 60 | guard let templateContent = try? String(contentsOf: url(fileName: name)) else { 61 | throw Error.templateMissing(name) 62 | } 63 | guard let data = data else { 64 | return req.eventLoop.newSucceededFuture(result: templateContent) 65 | } 66 | guard let templateData = templateContent.data(using: .utf8) else { 67 | throw Error.invalidTemplateData(name) 68 | } 69 | let leaf = try req.make(LeafRenderer.self) 70 | return leaf.render(template: templateData, data).map(to: String.self) { view in 71 | guard let string = String(data: view.data, encoding: .utf8) else { 72 | throw Error.invalidTemplateData(name) 73 | } 74 | return string 75 | } 76 | } 77 | 78 | public func reset() throws { 79 | try loadTemplates() 80 | } 81 | 82 | // MARK Private interface 83 | 84 | private func url(fileName: String) -> URL { 85 | var url = URL( 86 | fileURLWithPath: ApiCoreBase.configuration.storage.local.root 87 | ) 88 | url.appendPathComponent("templates") 89 | url.appendPathComponent("email") 90 | 91 | do { 92 | try FileManager.default.createDirectory(atPath: url.path, withIntermediateDirectories: true) 93 | } catch { 94 | fatalError("Unable to create templates folder structure") 95 | } 96 | 97 | url.appendPathComponent(fileName) 98 | url.appendPathExtension("leaf") 99 | return url 100 | } 101 | 102 | func loadTemplates() throws { 103 | guard let url = URL(string: packageUrl) else { 104 | fatalError("Invalid template package URL: (\(packageUrl))") 105 | } 106 | let packageData = try Data(contentsOf: url) 107 | guard packageData.count > 0 else { 108 | fatalError("Invalid template package: (\(packageUrl))") 109 | } 110 | let package = try JSONDecoder().decode(Templates.self, from: packageData) 111 | for template in package.items { 112 | guard let url = URL(string: template.link) else { 113 | fatalError("Invalid template URL: (\(template.name) - \(template.link))") 114 | } 115 | let content = try Data(contentsOf: url) 116 | let fileUrl = self.url(fileName: template.name) 117 | try content.write(to: fileUrl) 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /Sources/ApiCore/Managers/AuthManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthManager.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 21/12/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import FluentPostgreSQL 11 | import ErrorsCore 12 | 13 | 14 | public class AuthManager { 15 | 16 | public static func logout(allFor token: String, on req: Request) throws -> Future { 17 | return try get(userFor: token, on: req).flatMap(to: Response.self) { token in 18 | return try Token.query(on: req).filter(\Token.userId == token.userId).delete().asResponse(to: req) 19 | } 20 | } 21 | 22 | static func get(userFor token: String, on req: Request) throws -> EventLoopFuture { 23 | return try Token.query(on: req).filter(\Token.token == token.sha()).first().flatMap(to: Token.self) { token in 24 | // Check token exists 25 | guard let token = token else { 26 | throw AuthError.authenticationFailed 27 | } 28 | // If token is expired, delete and fail authentication 29 | guard token.expires > Date() else { 30 | return token.delete(on: req).map(to: Token.self) { _ in 31 | throw AuthError.expiredToken 32 | } 33 | } 34 | return req.eventLoop.future(token) 35 | } 36 | } 37 | 38 | /// Renew token helper 39 | public static func token(request req: Request, token: String) throws -> Future { 40 | return try get(userFor: token, on: req).flatMap(to: Response.self) { token in 41 | return User.find(token.userId, on: req).flatMap(to: Response.self) { user in 42 | guard let user = user else { 43 | throw AuthError.authenticationFailed 44 | } 45 | return try Token.Public(token: token, user: user).asResponse(.ok, to: req).map(to: Response.self) { response in 46 | let jwtService = try req.make(JWTService.self) 47 | try response.http.headers.replaceOrAdd(name: "Authorization", value: "Bearer \(jwtService.signUserToToken(user: user))") 48 | return response 49 | } 50 | } 51 | } 52 | } 53 | 54 | /// Login helper 55 | public static func authData(request req: Request, user: User) throws -> Future<(Token.PublicFull, User)> { 56 | typealias ResultTupple = (Token.PublicFull, User) 57 | 58 | guard user.verified == true, user.disabled == false else { 59 | throw AuthError.unverifiedAccount 60 | } 61 | 62 | let token = try Token(user: user, type: .authentication) 63 | let tokenBackup = token.token 64 | token.token = try token.token.sha() 65 | return token.save(on: req).map(to: ResultTupple.self) { token in 66 | guard let _ = token.id else { 67 | throw AuthError.serverError 68 | } 69 | let publicToken = Token.PublicFull(token: token, user: user) 70 | publicToken.token = tokenBackup 71 | return (publicToken, user) 72 | } 73 | } 74 | 75 | /// Login helper 76 | public static func loginData(request req: Request, login: User.Auth.Login) throws -> Future<(Token.PublicFull, User)> { 77 | typealias ResultTupple = (Token.PublicFull, User) 78 | guard !login.email.isEmpty, !login.password.isEmpty else { 79 | throw AuthError.authenticationFailed 80 | } 81 | return UsersManager.get(user: login.email, password: login.password, on: req).flatMap(to: ResultTupple.self) { user in 82 | guard let user = user else { 83 | throw AuthError.authenticationFailed 84 | } 85 | return try authData(request: req, user: user) 86 | } 87 | } 88 | 89 | /// Login helper 90 | public static func login(request req: Request, login: User.Auth.Login) throws -> Future { 91 | guard ApiCoreBase.configuration.auth.allowLogin else { 92 | throw ErrorsCore.HTTPError.notAuthorized 93 | } 94 | return try loginData(request: req, login: login).flatMap(to: Response.self) { (publicToken, user) in 95 | return try publicToken.asResponse(.ok, to: req).map(to: Response.self) { response in 96 | try response.http.headers.replaceOrAdd(name: "Authorization", value: "Bearer \(user.asJWTToken(on: req))") 97 | return response 98 | } 99 | } 100 | 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Sources/ApiCore/Managers/SystemManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemManager.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 29/04/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | 12 | 13 | public final class SystemManager { 14 | 15 | /// Set a key value 16 | public static func set(value: String, for key: String, teamId: DbIdentifier? = nil, on req: Request) -> EventLoopFuture { 17 | return get(for: key, teamId: teamId, on: req).flatMap({ entry in 18 | guard let entry = entry else { 19 | let entry = System(teamId: teamId, key: key, value: value) 20 | return entry.save(on: req) 21 | } 22 | entry.value = value 23 | return entry.save(on: req) 24 | }) 25 | } 26 | 27 | /// Retrieve a value for a key/team 28 | public static func get(for key: String, teamId: DbIdentifier? = nil, on req: Request) -> EventLoopFuture { 29 | let q = System.query(on: req).filter(\System.key == key) 30 | q.filter(\System.teamId == teamId) 31 | return q.first() 32 | } 33 | 34 | /// Retrive the whole config 35 | public static func get(teamId: DbIdentifier? = nil, on req: Request) -> EventLoopFuture<[System]> { 36 | let q = System.query(on: req) 37 | q.filter(\System.teamId == teamId) 38 | if teamId != nil { 39 | _ = q.sort(\System.teamId, .ascending) 40 | } 41 | return q.all().map({ arr in 42 | let crossReference = Dictionary(grouping: arr, by: { $0.key }) 43 | var newArr: [System] = [] 44 | for key in crossReference.keys { 45 | let val = crossReference[key]?.sorted(by: { $0.teamId?.uuidString ?? "" > $1.teamId?.uuidString ?? "" }) 46 | guard let selection = val?.first else { continue } 47 | newArr.append(selection) 48 | } 49 | newArr.sort(by: { $0.key < $1.key }) 50 | 51 | return newArr 52 | }) 53 | } 54 | 55 | } 56 | 57 | 58 | extension SystemManager { 59 | 60 | /// Set a number of key/values at once 61 | public static func set(_ valueDoubles: [(value: String, key: String)], teamId: DbIdentifier? = nil, on req: Request) -> EventLoopFuture<[System]> { 62 | var futures: [EventLoopFuture] = [] 63 | for valueDouble in valueDoubles { 64 | futures.append( 65 | set( 66 | value: valueDouble.value, 67 | for: valueDouble.key, 68 | teamId: teamId, 69 | on: req 70 | ) 71 | ) 72 | } 73 | return futures.flatten(on: req) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ApiCore/Middleware/ApiAuthMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiAuthMiddleware.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 13/01/2018. 6 | // 7 | 8 | import Foundation 9 | import Async 10 | import Debugging 11 | import HTTP 12 | import Service 13 | import Vapor 14 | import ErrorsCore 15 | import JWT 16 | 17 | 18 | /// API authentication middleware 19 | public final class ApiAuthMiddleware: Middleware, Service { 20 | 21 | /// Respond to method of the middleware 22 | public func respond(to req: Request, chainingTo next: Responder) throws -> Future { 23 | debug(request: req) 24 | 25 | guard let userPayload = try? jwtPayload(request: req) else { 26 | return try req.response.notAuthorized().asFuture(on: req) 27 | } 28 | 29 | return User.find(userPayload.userId, on: req).flatMap(to: Response.self) { user in 30 | guard let user = user else { 31 | return try req.response.notAuthorized().asFuture(on: req) 32 | } 33 | 34 | let authenticationCache = try req.make(AuthenticationCache.self) 35 | authenticationCache[User.self] = user 36 | 37 | return try next.respond(to: req) 38 | } 39 | } 40 | 41 | /// Get JWT payload 42 | private func jwtPayload(request req: Request) throws -> JWTAuthPayload { 43 | // Get JWT token 44 | guard let token = req.http.headers.authorizationToken else { 45 | throw ErrorsCore.HTTPError.notAuthorized 46 | } 47 | let jwtService: JWTService = try req.make() 48 | 49 | // Get user payload 50 | guard let userPayload = try? JWT(from: token, verifiedUsing: jwtService.signer).payload else { 51 | throw ErrorsCore.HTTPError.notAuthorized 52 | } 53 | 54 | return userPayload 55 | } 56 | 57 | /// Debug 58 | private func debug(request req: Request) { 59 | if req.environment != .production, ApiCoreBase.debugRequests { 60 | req.http.body.consumeData(max: 500, on: req).addAwaiter { (d) in 61 | print("Debugging response:") 62 | print("HTTP [\(req.http.version.major).\(req.http.version.minor)] with status code [\(req.http)]") 63 | print("Headers:") 64 | for header in req.http.headers { 65 | print("\t\(header.name.description) = \(header.value)") 66 | } 67 | print("Content:") 68 | if let data = d.result, let s = String(data: data, encoding: .utf8) { 69 | print("\tContent:\n\(s)") 70 | } 71 | } 72 | } 73 | } 74 | 75 | /// Public initializer 76 | public init() { } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Sources/ApiCore/Middleware/DebugCheckMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugCheckMiddleware.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 31/10/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import ErrorsCore 11 | 12 | 13 | /// API authentication middleware 14 | public final class DebugCheckMiddleware: Middleware, Service { 15 | 16 | public func respond(to req: Request, chainingTo next: Responder) throws -> EventLoopFuture { 17 | if req.environment == .production { 18 | throw ErrorsCore.HTTPError.notAuthorized 19 | } 20 | return try next.respond(to: req) 21 | } 22 | 23 | /// Public initializer 24 | public init() { } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ApiCore/Middleware/ErrorLoggingMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorLoggingMiddleware.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 11/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Async 10 | import Debugging 11 | import HTTP 12 | import Service 13 | import Vapor 14 | import ErrorsCore 15 | 16 | 17 | /// Log errors to the DB middleware 18 | final class ErrorLoggingMiddleware: Middleware, Service { 19 | 20 | /// Respond to method of the middleware 21 | func respond(to req: Request, chainingTo next: Responder) throws -> Future { 22 | return try next.respond(to: req).catchFlatMap({ (error) -> (Future) in 23 | return ErrorLog(request: req, error: error).save(on: req).flatMap(to: Response.self) { log in 24 | throw error 25 | } 26 | }) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ApiCore/Middleware/UrlPrinterMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlPrinterMiddleware.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 31/10/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | /// API authentication middleware 13 | public final class UrlPrinterMiddleware: Middleware, Service { 14 | 15 | public func respond(to req: Request, chainingTo next: Responder) throws -> EventLoopFuture { 16 | print("[\(req.http.method)] \(req.http.url.path)") 17 | 18 | return try next.respond(to: req) 19 | } 20 | 21 | /// Public initializer 22 | public init() { } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ApiCore/Migrations/BaseMigration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseMigration.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 02/04/2019. 6 | // 7 | 8 | import Foundation 9 | import Fluent 10 | 11 | 12 | struct BaseMigration: Migration { 13 | 14 | typealias Database = ApiCoreDatabase 15 | 16 | static func prepare(on conn: ApiCoreConnection) -> EventLoopFuture { 17 | let user = try! InstallController.su(on: conn) 18 | user.verified = true 19 | return user.save(on: conn).flatMap(to: Void.self) { user in 20 | return InstallController.adminTeam.save(on: conn).flatMap(to: Void.self) { team in 21 | var futures = [ 22 | team.users.attach(user, on: conn).flatten() 23 | ] 24 | ApiCoreBase.installFutures.forEach({ closure in 25 | futures.append(try! closure(conn)) 26 | }) 27 | return futures.flatten(on: conn) 28 | } 29 | } 30 | } 31 | 32 | static func revert(on conn: ApiCoreConnection) -> EventLoopFuture { 33 | return User.query(on: conn).delete().flatMap(to: Void.self) { _ in 34 | return Team.query(on: conn).delete().flatMap(to: Void.self) { _ in 35 | return TeamUser.query(on: conn).delete() 36 | } 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/Authenticator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Authenticator.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 18/04/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | public struct Authenticator: Content { 13 | 14 | public var button: String 15 | 16 | public var name: String 17 | 18 | public var identifier: String 19 | 20 | public var icon: String 21 | 22 | public var color: String? 23 | 24 | public var type: AuthType 25 | 26 | public init(button: String, name: String, identifier: String, icon: String, color: String?, type: AuthType = .oauth) { 27 | self.button = button 28 | self.name = name 29 | self.identifier = identifier 30 | self.icon = icon 31 | self.color = color 32 | self.type = type 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/ErrorLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Log.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 11/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | import FluentPostgreSQL 12 | //import DbCore 13 | import ErrorsCore 14 | 15 | 16 | /// Error logs array type typealias 17 | public typealias ErrorLogs = [ErrorLog] 18 | 19 | 20 | /// ErrorLog object 21 | public final class ErrorLog: DbCoreModel { 22 | 23 | /// Object Id 24 | public var id: DbIdentifier? 25 | 26 | /// Date added/created 27 | public var added: Date 28 | 29 | /// URL 30 | public var uri: String 31 | 32 | /// Error 33 | public var error: String 34 | 35 | /// Initializer 36 | public init(id: DbIdentifier? = nil, request req: Request, error: Swift.Error) { 37 | let query = req.http.url.query != nil ? "?\(req.http.url.query!)" : "" 38 | self.uri = "\(req.http.url.path)\(query)" 39 | self.added = Date() 40 | 41 | if let e = error as? FrontendError { 42 | self.error = "(\(e.identifier)) - \(e.reason)" 43 | } 44 | else { 45 | self.error = error.localizedDescription 46 | } 47 | } 48 | 49 | } 50 | 51 | // MARK: - Migrations 52 | 53 | extension ErrorLog: Migration { 54 | 55 | /// Prepare migrations 56 | public static func prepare(on connection: ApiCoreConnection) -> Future { 57 | return Database.create(self, on: connection) { (schema) in 58 | schema.field(for: \.id, isIdentifier: true) 59 | schema.field(for: \.added, type: .timestamp) 60 | schema.field(for: \.uri, type: .varchar(250)) 61 | schema.field(for: \.error, type: .text) 62 | } 63 | } 64 | 65 | /// Revert migrations 66 | public static func revert(on connection: ApiCoreConnection) -> Future { 67 | return Database.delete(ErrorLog.self, on: connection) 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/FluentDesign.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FluentDesign.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 05/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | import FluentPostgreSQL 12 | //import DbCore 13 | import ErrorsCore 14 | 15 | 16 | /// FluentDesign array type typealias 17 | public typealias FluentDesigns = [FluentDesign] 18 | 19 | 20 | /// FluentDesigns object 21 | public final class FluentDesign: DbCoreModel { 22 | 23 | /// Table (entity) name override 24 | public static let entity: String = "fluent" 25 | 26 | /// Object Id 27 | public var id: DbIdentifier? 28 | 29 | /// Name 30 | public var name: String 31 | 32 | /// Batch 33 | public var batch: Int 34 | 35 | /// Created date 36 | public var createdAt: Date 37 | 38 | /// Updated date 39 | public var updatedAt: Date 40 | 41 | /// Initializer 42 | public init(id: DbIdentifier? = nil, name: String, batch: Int, createdAt: Date = Date(), updatedAt: Date = Date()) { 43 | self.id = id 44 | self.name = name 45 | self.batch = batch 46 | self.createdAt = createdAt 47 | self.updatedAt = updatedAt 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/FrontendSystemData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrontendSystemData.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 11/09/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | /// Model object containing data for frontend web templates 13 | public struct FrontendSystemData: Content { 14 | 15 | /// Server URL 16 | public var info: Info 17 | 18 | enum CodingKeys: String, CodingKey { 19 | case info = "info" 20 | } 21 | 22 | /// Initializer 23 | /// 24 | /// - Parameter req: Request 25 | /// - Throws: something ... from time to time 26 | public init(_ req: Request) throws { 27 | info = try Info(req) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/Info.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Info.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 25/05/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | /// Server info object 13 | public struct Info: Content { 14 | 15 | /// Icons 16 | public struct Icon: Codable { 17 | 18 | /// Size 19 | public let size: IconSize 20 | 21 | /// URL 22 | public var url: String 23 | 24 | } 25 | 26 | /// Config 27 | public struct Config: Codable { 28 | 29 | /// Team configuration (single or multi) 30 | public let singleTeam: Bool 31 | 32 | /// Classic login is enabled/disabled 33 | public let allowLogin: Bool 34 | 35 | /// New registrations are enabled/disabled 36 | public let allowRegistrations: Bool 37 | 38 | /// Is system registrations restricted to certain domains only? 39 | public let allowedRegistrationDomains: [String] 40 | 41 | /// Allow invitations 42 | public let allowInvitations: Bool 43 | 44 | /// Users are restricted to send invitations to certain domains only 45 | public let domainInvitationsRestricted: Bool 46 | 47 | /// Github login enabled 48 | public let githubEnabled: Bool 49 | 50 | /// Github teams 51 | public let allowedGithubTeams: [String] 52 | 53 | /// Gitlab login enabled 54 | public let gitlabEnabled: Bool 55 | 56 | /// Gitlab teams 57 | public let allowedGitlabGroups: [String] 58 | 59 | /// Commit hash (if available, n/a otherwise) 60 | public let commit: String 61 | 62 | enum CodingKeys: String, CodingKey { 63 | case allowLogin = "allow_login" 64 | case singleTeam = "single_team" 65 | case allowRegistrations = "allow_registrations" 66 | case allowedRegistrationDomains = "allowed_registration_domains" 67 | case allowInvitations = "allow_invitations" 68 | case domainInvitationsRestricted = "domain_invitations_restricted" 69 | case githubEnabled = "github_enabled" 70 | case allowedGithubTeams = "allowed_github_teams" 71 | case gitlabEnabled = "gitlab_enabled" 72 | case allowedGitlabGroups = "allowed_gitlab_groups" 73 | case commit 74 | } 75 | 76 | } 77 | 78 | /// Server name 79 | public let name: String 80 | 81 | /// Server subtitle 82 | public let subtitle: String? 83 | 84 | /// Server URL 85 | public let url: String 86 | 87 | /// Server URL 88 | public let interface: String? 89 | 90 | /// Server icons 91 | public var icons: [Icon] 92 | 93 | /// Server config 94 | public let config: Config 95 | 96 | 97 | /// Initializer 98 | /// 99 | /// - Parameter req: Request 100 | /// - Throws: yes 101 | public init(_ req: Request) throws { 102 | let fm = try req.makeFileCore() 103 | name = ApiCoreBase.configuration.server.name 104 | subtitle = ApiCoreBase.configuration.server.subtitle 105 | url = req.serverURL().absoluteString 106 | interface = ApiCoreBase.configuration.server.interface 107 | icons = try IconSize.all.sorted(by: { $0.rawValue < $1.rawValue }).map({ 108 | let url = try fm.url(for: "server/image/\($0.rawValue)", on: req) 109 | return Info.Icon(size: $0, url: url) 110 | }) 111 | 112 | let dc = DirectoryConfig.detect() 113 | let url = URL(fileURLWithPath: dc.workDir).appendingPathComponent("Resources").appendingPathComponent("commit.txt") 114 | let commit: String 115 | if FileManager.default.fileExists(atPath: url.path), let c = try? String(contentsOfFile: url.path) { 116 | commit = c 117 | } else { 118 | commit = "n/a" 119 | } 120 | 121 | config = Config( 122 | singleTeam: ApiCoreBase.configuration.general.singleTeam, 123 | allowLogin: ApiCoreBase.configuration.auth.allowLogin, 124 | allowRegistrations: ApiCoreBase.configuration.auth.allowRegistrations, 125 | allowedRegistrationDomains: ApiCoreBase.configuration.auth.allowedDomainsForRegistration, 126 | allowInvitations: ApiCoreBase.configuration.auth.allowInvitations, 127 | domainInvitationsRestricted: !ApiCoreBase.configuration.auth.allowedDomainsForInvitations.isEmpty, 128 | githubEnabled: ApiCoreBase.configuration.auth.github.enabled, 129 | allowedGithubTeams: ApiCoreBase.configuration.auth.github.teams, 130 | gitlabEnabled: ApiCoreBase.configuration.auth.gitlab.enabled, 131 | allowedGitlabGroups: ApiCoreBase.configuration.auth.gitlab.groups, 132 | commit: commit 133 | ) 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/Query/BasicQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicQuery.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 15/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | 12 | 13 | /// Basic URL query object 14 | public struct BasicQuery: Codable { 15 | 16 | /// Requesting plain text result (comparing to JSON result) 17 | public let plain: Bool? 18 | 19 | /// From (used in pagination, default is 0) 20 | public let from: Int? 21 | 22 | /// Limit number of items per page 23 | public let limit: Int? 24 | 25 | /// Search value 26 | public let search: String? 27 | 28 | /// Token 29 | public let token: String? 30 | 31 | } 32 | 33 | 34 | extension QueryContainer { 35 | 36 | /// Basic query values 37 | public var basic: BasicQuery? { 38 | let decoded = try? decode(BasicQuery.self) 39 | return decoded 40 | } 41 | 42 | /// Requesting plain text result (comparing to JSON result) 43 | public var plain: Bool? { 44 | return basic?.plain 45 | } 46 | 47 | /// Page (used in pagination, default is 0) 48 | public var from: Int? { 49 | return basic?.from 50 | } 51 | 52 | /// Limit number of items per page 53 | public var limit: Int? { 54 | return basic?.limit ?? 200 55 | } 56 | 57 | /// Search value 58 | public var search: String? { 59 | return basic?.search 60 | } 61 | 62 | public var token: String? { 63 | return basic?.token 64 | } 65 | 66 | } 67 | 68 | 69 | extension QueryBuilder { 70 | 71 | /// Apply pagination onto a database query 72 | public func paginate(on req: Request) throws -> Self { 73 | if let limit = req.query.basic?.limit { 74 | let from = req.query.basic?.from ?? 0 75 | return range(lower: from, upper: (from + (limit - 1))) 76 | } 77 | return self 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/ServerSecurity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerSecurity.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 21/12/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import FluentPostgreSQL 11 | 12 | 13 | public final class ServerSecurity: Content { 14 | 15 | public final class Issue: Content { 16 | 17 | public enum Category: String, Codable { 18 | 19 | case info 20 | 21 | case warning 22 | 23 | case danger 24 | 25 | } 26 | 27 | public var category: Category 28 | 29 | public var code: String 30 | 31 | public var issue: String 32 | 33 | public init(category: Category, code: String, issue: String) { 34 | self.category = category 35 | self.code = code 36 | self.issue = issue 37 | } 38 | 39 | } 40 | 41 | public var issues: [Issue] = [] 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/Setting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Setting.swift 3 | // SettingsCore 4 | // 5 | // Created by Ondrej Rafaj on 15/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | import FluentPostgreSQL 12 | import ErrorsCore 13 | 14 | 15 | public typealias Settings = [Setting] 16 | 17 | 18 | public final class Setting: DbCoreModel { 19 | 20 | public var id: DbIdentifier? 21 | public var name: String 22 | public var config: String 23 | 24 | public init(id: DbIdentifier? = nil, name: String, config: String) { 25 | self.id = id 26 | self.name = name 27 | self.config = config 28 | } 29 | 30 | } 31 | 32 | // MARK: - Migrations 33 | 34 | extension Setting: Migration { 35 | 36 | public static var idKey: WritableKeyPath = \Setting.id 37 | 38 | public static func prepare(on connection: ApiCoreConnection) -> Future { 39 | return Database.create(self, on: connection) { (schema) in 40 | schema.field(for: \.id, isIdentifier: true) 41 | schema.field(for: \.name) 42 | schema.field(for: \.config, type: .text) 43 | } 44 | } 45 | 46 | public static func revert(on connection: ApiCoreConnection) -> Future { 47 | return Database.delete(Setting.self, on: connection) 48 | } 49 | 50 | } 51 | 52 | extension Array where Element == Setting { 53 | 54 | public func asDictionary() -> [String: String] { 55 | return reduce([String: String]()) { (dict, setting) -> [String: String] in 56 | var dict = dict 57 | dict[setting.name] = setting.config 58 | return dict 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/System.swift: -------------------------------------------------------------------------------- 1 | // 2 | // System.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 29/04/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | import FluentPostgreSQL 12 | 13 | 14 | /// Systems array type typealias 15 | public typealias Systems = [System] 16 | 17 | 18 | /// System object 19 | public final class System: DbCoreModel { 20 | 21 | /// Object id 22 | public var id: DbIdentifier? 23 | 24 | /// Team Id (optional, per team only, overrides basic system settings) 25 | public var teamId: DbIdentifier? 26 | 27 | /// Key 28 | public var key: String 29 | 30 | /// Value 31 | public var value: String 32 | 33 | /// Initializer 34 | init(teamId: DbIdentifier? = nil, key: String, value: String) { 35 | self.teamId = teamId 36 | self.key = key 37 | self.value = value 38 | } 39 | 40 | enum CodingKeys: String, CodingKey { 41 | case id 42 | case teamId = "team_id" 43 | case key 44 | case value 45 | } 46 | 47 | } 48 | 49 | // MARK: - Migrations 50 | 51 | extension System: Migration { 52 | 53 | /// Migration preparations 54 | public static func prepare(on connection: ApiCoreConnection) -> Future { 55 | return Database.create(self, on: connection) { (schema) in 56 | schema.field(for: \.id, isIdentifier: true) 57 | schema.field(for: \.teamId, type: .uuid) 58 | schema.field(for: \.key, type: .varchar(64), .notNull) 59 | schema.field(for: \.value, type: .text, .notNull) 60 | } 61 | } 62 | 63 | /// Migration reverse 64 | public static func revert(on connection: ApiCoreConnection) -> Future { 65 | return Database.delete(System.self, on: connection) 66 | } 67 | 68 | } 69 | 70 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/TeamUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TeamUser.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 01/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Fluent 10 | //import DbCore 11 | import Vapor 12 | 13 | 14 | public final class TeamUser: ModifiablePivot, DbCoreModel { 15 | 16 | /// Left JOIN table 17 | public typealias Left = Team 18 | 19 | /// Right JOIN table 20 | public typealias Right = User 21 | 22 | /// Left JOIN Id key 23 | public static var leftIDKey: WritableKeyPath { 24 | return \.teamId 25 | } 26 | 27 | /// Right JOIN Id key 28 | public static var rightIDKey: WritableKeyPath { 29 | return \.userId 30 | } 31 | 32 | /// Object Id 33 | public var id: DbIdentifier? 34 | 35 | /// Team Id 36 | public var teamId: DbIdentifier 37 | 38 | /// User Id 39 | public var userId: DbIdentifier 40 | 41 | // MARK: Initialization 42 | 43 | /// Initialization 44 | public init(_ left: TeamUser.Left, _ right: TeamUser.Right) throws { 45 | teamId = try left.requireID() 46 | userId = try right.requireID() 47 | } 48 | 49 | } 50 | 51 | // MARK: - Migrations 52 | 53 | extension TeamUser: Migration { 54 | 55 | /// Migration preparations 56 | public static func prepare(on connection: ApiCoreConnection) -> Future { 57 | return Database.create(self, on: connection) { (schema) in 58 | schema.field(for: \TeamUser.id) 59 | schema.field(for: \TeamUser.teamId, type: .uuid, .notNull) 60 | schema.field(for: \TeamUser.userId, type: .uuid, .notNull) 61 | } 62 | } 63 | 64 | /// Migration reverse (DROP TABLE) 65 | public static func revert(on connection: ApiCoreConnection) -> Future { 66 | return Database.delete(TeamUser.self, on: connection) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Token.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 13/01/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Fluent 11 | import FluentPostgreSQL 12 | //import DbCore 13 | import ErrorsCore 14 | import Random 15 | 16 | 17 | /// Tokens array type typealias 18 | public typealias Tokens = [Token] 19 | 20 | 21 | /// Token object 22 | public final class Token: DbCoreModel { 23 | 24 | /// Token type 25 | public enum TokenType: String, PostgreSQLRawEnum { 26 | 27 | /// Authentication 28 | case authentication = "auth" 29 | 30 | /// All available cases 31 | public static var allCases: [TokenType] { 32 | return [ 33 | .authentication 34 | ] 35 | } 36 | 37 | } 38 | 39 | /// Error 40 | public enum Error: FrontendError { 41 | 42 | /// User Id is missing 43 | case missingUserId 44 | 45 | /// HTTP status 46 | public var status: HTTPStatus { 47 | return .preconditionFailed 48 | } 49 | 50 | /// Error identifier 51 | public var identifier: String { 52 | return "token.missing_user_id" 53 | } 54 | 55 | /// Reason for failure 56 | public var reason: String { 57 | return "User ID is missing" 58 | } 59 | 60 | } 61 | 62 | /// Displayable full public object 63 | /// for security reasons, the original object should never be displayed 64 | public final class PublicFull: DbCoreModel { 65 | 66 | /// Object id 67 | public var id: DbIdentifier? 68 | 69 | /// User 70 | public var user: User.Display 71 | 72 | /// Token 73 | public var token: String 74 | 75 | /// Token expiry date 76 | public var expires: Date 77 | 78 | /// Token type 79 | // public var type: TokenType 80 | 81 | /// Initializer 82 | public init(token: Token, user: User) { 83 | self.id = token.id 84 | self.user = User.Display(user) 85 | self.token = token.token 86 | self.expires = token.expires 87 | // self.type = token.type 88 | } 89 | } 90 | 91 | public final class PublicNoUser: DbCoreModel { 92 | 93 | /// Object id 94 | public var id: DbIdentifier? 95 | 96 | /// Token 97 | public var token: String 98 | 99 | /// Token expiry time 100 | public var expires: Date 101 | 102 | /// Token type 103 | // public var type: TokenType 104 | 105 | /// Initializer 106 | public init(token: Token) { 107 | self.id = token.id 108 | self.token = token.token 109 | self.expires = token.expires 110 | // self.type = token.type 111 | } 112 | } 113 | 114 | /// Displayable public object 115 | /// for security reasons, the original object should never be displayed 116 | public final class Public: DbCoreModel { 117 | 118 | /// Object id 119 | public var id: DbIdentifier? 120 | 121 | /// User 122 | public var user: User.Display 123 | 124 | /// Token expiry date 125 | public var expires: Date 126 | 127 | /// Initializer 128 | public init(token: Token, user: User) { 129 | self.id = token.id 130 | self.user = User.Display(user) 131 | self.expires = token.expires 132 | } 133 | } 134 | 135 | /// Object id 136 | public var id: DbIdentifier? 137 | 138 | /// User Id 139 | public var userId: DbIdentifier 140 | 141 | /// Token 142 | public var token: String 143 | 144 | /// Token expiry date 145 | public var expires: Date 146 | 147 | /// Token type 148 | // public var type: TokenType 149 | 150 | /// Initializer 151 | init(user: User, type: TokenType) throws { 152 | guard let userId = user.id else { 153 | throw Error.missingUserId 154 | } 155 | self.userId = userId 156 | let randData = try URandom().generateData(count: 60) 157 | let rand = randData.base64EncodedString() 158 | self.token = String(rand.prefix(60)) 159 | self.expires = Date().addMonth(n: 1) 160 | // self.type = type 161 | } 162 | 163 | enum CodingKeys: String, CodingKey { 164 | case id 165 | case userId = "user_id" 166 | case token 167 | case expires 168 | // case type 169 | } 170 | 171 | } 172 | 173 | // MARK: - Migrations 174 | 175 | extension Token: Migration { 176 | 177 | /// Migration preparations 178 | public static func prepare(on connection: ApiCoreConnection) -> Future { 179 | return Database.create(self, on: connection) { (schema) in 180 | schema.field(for: \.id, isIdentifier: true) 181 | schema.field(for: \.userId, type: .uuid, .notNull) 182 | schema.field(for: \.token, type: .varchar(64), .notNull) 183 | schema.field(for: \.expires, type: .timestamp, .notNull) 184 | // schema.field(for: \.type, type: .varchar(4), .notNull) 185 | } 186 | } 187 | 188 | /// Migration reverse 189 | public static func revert(on connection: ApiCoreConnection) -> Future { 190 | return Database.delete(Token.self, on: connection) 191 | } 192 | 193 | } 194 | 195 | -------------------------------------------------------------------------------- /Sources/ApiCore/Model/UserSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserSource.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 29/03/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | /// Common interface for third-party authentication 13 | public protocol UserSource: Codable { 14 | 15 | // Username / nickname 16 | var username: String { get } 17 | 18 | /// First name 19 | var firstname: String { get } 20 | 21 | /// Last name 22 | var lastname: String { get } 23 | 24 | /// Email 25 | var email: String { get } 26 | 27 | /// ApiCore permanent login token 28 | var token: String? { get set } 29 | 30 | /// Additional info 31 | var info: [String: String]? { get set } 32 | 33 | } 34 | 35 | 36 | extension UserSource { 37 | 38 | public func asUser(on req: Request) throws -> User { 39 | let user = User( 40 | username: username, 41 | firstname: firstname, 42 | lastname: lastname, 43 | email: email, 44 | password: nil, 45 | token: nil, 46 | disabled: false, 47 | su: false 48 | ) 49 | return user 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ApiCore/Protocols/Controller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Controller.swift 3 | // App 4 | // 5 | // Created by Ondrej Rafaj on 09/12/2017. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | /// Controller protocol 13 | public protocol Controller { 14 | 15 | /// Boot controller and register all it's routes 16 | static func boot(router: Router, secure: Router, debug: Router) throws 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ApiCore/Protocols/EmailTemplateData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmailTemplateData.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 29/05/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | public protocol EmailTemplateData: class, Content { 13 | 14 | var user: User.Display? { get set } 15 | 16 | var info: Info? { get set } 17 | 18 | var settings: [String: String]? { get set } 19 | 20 | } 21 | 22 | extension EmailTemplateData { 23 | 24 | public func setup(user: User.Display? = nil, on req: Request) throws -> EventLoopFuture { 25 | self.user = try user ?? req.me.user().asDisplay() 26 | self.info = try Info(req) 27 | return Setting.query(on: req).all().map() { settings in 28 | self.settings = settings.asDictionary() 29 | return Void() 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ApiCoreApp/configure.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Vapor 3 | import ApiCore 4 | import MailCore 5 | 6 | 7 | public func configure(_ config: inout Vapor.Config, _ env: inout Vapor.Environment, _ services: inout Services) throws { 8 | print("Starting ApiCore by LiveUI") 9 | sleep(1) 10 | Env.print() 11 | 12 | // Go! 13 | try ApiCoreBase.configure(&config, &env, &services) 14 | 15 | // Register routes 16 | let router = EngineRouter.default() 17 | try ApiCoreBase.boot(router: router) 18 | services.register(router, as: Router.self) 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ApiCoreRun/main.swift: -------------------------------------------------------------------------------- 1 | import ApiCoreApp 2 | import Service 3 | import Vapor 4 | 5 | do { 6 | var config = Config.default() 7 | var env = try Environment.detect() 8 | var services = Services.default() 9 | 10 | try ApiCoreApp.configure(&config, &env, &services) 11 | 12 | let app = try Application( 13 | config: config, 14 | environment: env, 15 | services: services 16 | ) 17 | 18 | try app.run() 19 | } catch { 20 | print("Top-level failure: \(error)") 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ApiCoreTestTools/ApiCoreTestTools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreTestTools.swift 3 | // ApiCoreTestTools 4 | // 5 | // Created by Ondrej Rafaj on 28/02/2018. 6 | // 7 | 8 | import Foundation 9 | import VaporTestTools 10 | import ApiCore 11 | 12 | 13 | extension User: Testable { } 14 | extension Team: Testable { } 15 | 16 | -------------------------------------------------------------------------------- /Sources/ApiCoreTestTools/Extensions/Application+Testable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application+Testable.swift 3 | // ApiCoreTestTools 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import Foundation 9 | @testable import ApiCore 10 | import Vapor 11 | import Fluent 12 | import VaporTestTools 13 | import MailCore 14 | import MailCoreTestTools 15 | 16 | 17 | public struct Paths { 18 | 19 | public var rootUrl: URL { 20 | let config = DirectoryConfig.detect() 21 | let url = URL(fileURLWithPath: config.workDir) 22 | return url 23 | } 24 | 25 | public var resourcesUrl: URL { 26 | let url = rootUrl.appendingPathComponent("Resources") 27 | return url 28 | } 29 | 30 | public var publicUrl: URL { 31 | let url = rootUrl.appendingPathComponent("Public") 32 | return url 33 | } 34 | 35 | } 36 | 37 | 38 | extension TestableProperty where TestableType: Application { 39 | 40 | public static var paths: Paths { 41 | return Paths() 42 | } 43 | 44 | public static func newApiCoreTestApp(databaseConfig: DatabasesConfig? = nil, _ configClosure: AppConfigClosure? = nil, _ routerClosure: AppRouterClosure? = nil) -> Application { 45 | let app = new({ (config, env, services) in 46 | // Reset static configs 47 | ApiCoreBase.migrationConfig = MigrationConfig() 48 | ApiCoreBase.middlewareConfig = MiddlewareConfig() 49 | 50 | try! ApiCoreBase.configure(&config, &env, &services) 51 | 52 | #if os(macOS) 53 | // Check the database ... if it doesn't contain test then make sure we are not pointing to a production DB 54 | if !ApiCoreBase.configuration.database.database.contains("-test") { 55 | ApiCoreBase.configuration.database.database = ApiCoreBase.configuration.database.database + "-test" 56 | } 57 | #endif 58 | 59 | // Set mailer mock 60 | MailerMock(services: &services) 61 | config.prefer(MailerMock.self, for: MailerService.self) 62 | 63 | configClosure?(&config, &env, &services) 64 | }) { (router) in 65 | routerClosure?(router) 66 | try! ApiCoreBase.boot(router: router) 67 | } 68 | 69 | return app 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ApiCoreTestTools/Extensions/HTTPRequest+Make.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPRequest+Make.swift 3 | // ApiCoreTestTools 4 | // 5 | // Created by Ondrej Rafaj on 05/03/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | @testable import ApiCore 11 | import VaporTestTools 12 | 13 | extension TestableProperty where TestableType == HTTPRequest { 14 | 15 | public static func get(uri: URI, headers: [String: String]? = nil, authorizedUser user: User, on app: Application) -> HTTPRequest { 16 | var headers = headers ?? [:] 17 | 18 | let jwtService = try! app.make(JWTService.self) 19 | headers["Authorization"] = try! "Bearer \(jwtService.signUserToToken(user: user))" 20 | 21 | let req = get(uri: uri, headers: headers) 22 | return req 23 | } 24 | 25 | public static func post(uri: URI, data: Data? = nil, headers: [String: String]? = nil, authorizedUser user: User, on app: Application) -> HTTPRequest { 26 | var headers = headers ?? [:] 27 | 28 | let jwtService = try! app.make(JWTService.self) 29 | headers["Authorization"] = try! "Bearer \(jwtService.signUserToToken(user: user))" 30 | 31 | let req = post(uri: uri, data: data, headers: headers) 32 | return req 33 | } 34 | 35 | public static func put(uri: URI, data: Data? = nil, headers: [String: String]? = nil, authorizedUser user: User, on app: Application) -> HTTPRequest { 36 | var headers = headers ?? [:] 37 | 38 | let jwtService = try! app.make(JWTService.self) 39 | headers["Authorization"] = try! "Bearer \(jwtService.signUserToToken(user: user))" 40 | 41 | let req = put(uri: uri, data: data, headers: headers) 42 | return req 43 | } 44 | 45 | public static func patch(uri: URI, data: Data? = nil, headers: [String: String]? = nil, authorizedUser user: User, on app: Application) -> HTTPRequest { 46 | var headers = headers ?? [:] 47 | 48 | let jwtService = try! app.make(JWTService.self) 49 | headers["Authorization"] = try! "Bearer \(jwtService.signUserToToken(user: user))" 50 | 51 | let req = patch(uri: uri, data: data, headers: headers) 52 | return req 53 | } 54 | 55 | public static func delete(uri: URI, headers: [String: String]? = nil, authorizedUser user: User, on app: Application) -> HTTPRequest { 56 | var headers = headers ?? [:] 57 | 58 | let jwtService = try! app.make(JWTService.self) 59 | headers["Authorization"] = try! "Bearer \(jwtService.signUserToToken(user: user))" 60 | 61 | let req = delete(uri: uri, headers: headers) 62 | return req 63 | } 64 | 65 | } 66 | 67 | -------------------------------------------------------------------------------- /Sources/ApiCoreTestTools/Extensions/Team+Testable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Team+Testable.swift 3 | // ApiCoreTestTools 4 | // 5 | // Created by Ondrej Rafaj on 01/03/2018. 6 | // 7 | 8 | import Foundation 9 | //import DbCore 10 | import ApiCore 11 | import Vapor 12 | import Fluent 13 | import VaporTestTools 14 | 15 | 16 | extension TestableProperty where TestableType == Team { 17 | 18 | @discardableResult public static func create(_ name: String, admin: Bool = false, on app: Application) -> Team { 19 | let team = Team(name: name, identifier: name.safeText, admin: admin) 20 | return create(team: team, on: app) 21 | } 22 | 23 | @discardableResult public static func create(team: Team, on app: Application) -> Team { 24 | let req = app.testable.fakeRequest() 25 | return try! team.save(on: req).wait() 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ApiCoreTestTools/Extensions/User+Testable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User+Testable.swift 3 | // ApiCoreTestTools 4 | // 5 | // Created by Ondrej Rafaj on 28/02/2018. 6 | // 7 | 8 | import Foundation 9 | //import DbCore 10 | import ApiCore 11 | import Vapor 12 | import Fluent 13 | import VaporTestTools 14 | 15 | 16 | extension TestableProperty where TestableType == User { 17 | 18 | @discardableResult public static func createSu(on app: Application) -> User { 19 | let req = app.testable.fakeRequest() 20 | let user = try! User(username: "admin", firstname: "Super", lastname: "Admin", email: "core@liveui.io", password: "sup3rS3cr3t".passwordHash(req), disabled: false, su: true) 21 | return create(user: user, on: app) 22 | } 23 | 24 | @discardableResult public static func create(username: String? = nil, firstname: String? = nil, lastname: String? = nil, email: String? = nil, password: String? = nil, token: String? = nil, expires: Date? = nil, disabled: Bool = true, su: Bool = false, on app: Application) -> User { 25 | let req = app.testable.fakeRequest() 26 | let fn = firstname ?? "Ondrej" 27 | let ln = lastname ?? "Rafaj" 28 | let un = username ?? "\(fn).\(ln)".safeText 29 | let user = try! User(username: un , firstname: fn, lastname: ln, email: email ?? "dev@liveui.io", password: (password ?? "sup3rS3cr3t").passwordHash(req), disabled: disabled, su: su) 30 | return create(user: user, on: app) 31 | } 32 | 33 | @discardableResult public static func create(user: User, on app: Application) -> User { 34 | let req = app.testable.fakeRequest() 35 | user.verified = true 36 | return try! user.save(on: req).wait() 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Sources/ApiCoreTestTools/Helpers/LinuxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinuxTests.swift 3 | // ApiCoreTests 4 | // 5 | // Created by Ondrej Rafaj on 01/03/2018. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | 11 | 12 | public protocol LinuxTests { 13 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 14 | static var defaultTestSuite: XCTestSuite { get } 15 | #endif 16 | static var allTests: [(String, Any)] { get } 17 | func testLinuxTests() 18 | } 19 | 20 | 21 | extension LinuxTests { 22 | 23 | /// Test the allTests dictionary has all the appropriate tests in it ... mac only 24 | public func doTestLinuxTestsAreOk() { 25 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 26 | // Count number of methods 27 | let thisClass = type(of: self) 28 | let linuxCount = thisClass.allTests.count 29 | let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount) 30 | XCTAssertEqual(linuxCount, darwinCount, "There is \(darwinCount - linuxCount) tests missing from allTests") 31 | 32 | // Look for duplicates 33 | let crossReferenceKeys = Dictionary(grouping: thisClass.allTests, by: { $0.0 }) 34 | let duplicateKeys = crossReferenceKeys.filter { $1.count > 1 }.sorted { $0.1.count > $1.1.count } 35 | XCTAssertTrue(duplicateKeys.isEmpty, "You shouldn't have any duplicate keys in allTests: \(duplicateKeys)") 36 | 37 | // let crossReferenceFuncs = Dictionary(grouping: thisClass.allTests, by: { ($0.1 as () -> ()) }) 38 | // let duplicateFuncs = crossReferenceFuncs.filter { $1.count > 1 }.sorted { $0.1.count > $1.1.count } 39 | // XCTAssertTrue(duplicateFuncs.isEmpty, "You shouldn't have any duplicate function references in allTests") 40 | #endif 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Sources/ApiCoreTestTools/Helpers/TeamsTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TeamsTestCase.swift 3 | // ApiCoreTests 4 | // 5 | // Created by Ondrej Rafaj on 01/03/2018. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | import Vapor 11 | import VaporTestTools 12 | import ApiCore 13 | 14 | 15 | public protocol TeamsTestCase: UsersTestCase { 16 | var team1: Team! { get set } 17 | var team2: Team! { get set } 18 | } 19 | 20 | 21 | extension TeamsTestCase { 22 | 23 | public func setupTeams() { 24 | setupUsers() 25 | 26 | let req = app.testable.fakeRequest() 27 | 28 | team1 = Team.testable.create("team 1", on: app) 29 | _ = try! team1.users.attach(user1, on: req).wait() 30 | 31 | team2 = Team.testable.create("team 2", on: app) 32 | _ = try! team2.users.attach(user2, on: req).wait() 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ApiCoreTestTools/Helpers/UsersTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersTestCase.swift 3 | // ApiCoreTests 4 | // 5 | // Created by Ondrej Rafaj on 28/02/2018. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | import Vapor 11 | import VaporTestTools 12 | import FluentTestTools 13 | @testable import ApiCore 14 | 15 | 16 | public protocol UsersTestCase: class { 17 | var app: Application! { get } 18 | 19 | var adminTeam: Team! { get set } 20 | var user1: User! { get set } 21 | var user2: User! { get set } 22 | } 23 | 24 | 25 | extension UsersTestCase { 26 | 27 | public func setupUsers() { 28 | app.testable.delete(allFor: TeamUser.self) 29 | app.testable.delete(allFor: Team.self) 30 | app.testable.delete(allFor: User.self) 31 | 32 | let req = app.testable.fakeRequest() 33 | 34 | adminTeam = Team.testable.create("Admin team", admin: true, on: app) 35 | 36 | user1 = User.testable.createSu(on: app) 37 | _ = try! adminTeam.users.attach(user1, on: req).wait() 38 | 39 | let authenticationCache = try! app.make(AuthenticationCache.self) 40 | authenticationCache[User.self] = user1 41 | 42 | user2 = User.testable.create(on: app) 43 | _ = try! adminTeam.users.attach(user2, on: req).wait() 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/FileCore/Clients/Local/LocalConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalConfig.swift 3 | // FileCore 4 | // 5 | // Created by Ondrej Rafaj on 12/05/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /// Local filesystem configuration 12 | public struct LocalConfig { 13 | 14 | /// Root folder for storing files 15 | public let root: String 16 | 17 | /// Initializer 18 | /// 19 | /// - parameters: 20 | /// - root: Root folder to store all files 21 | public init(root: String) { 22 | self.root = root 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/FileCore/Clients/S3/S3LibClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // S3LibClient.swift 3 | // FileCore 4 | // 5 | // Created by Ondrej Rafaj on 12/05/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import S3 11 | 12 | 13 | /// S3 filesystem client 14 | class S3LibClient: FileManagement, Service { 15 | 16 | /// Marks if service is remote or local 17 | public let isRemote: Bool = true 18 | 19 | /// Error alias 20 | typealias Error = FileCoreManager.Error 21 | 22 | /// Configuration 23 | let config: S3Signer.Config 24 | 25 | /// S3 connector 26 | let s3: S3Client 27 | 28 | let bucket: String 29 | 30 | /// Return a server url for the filesystem 31 | public func serverUrl() throws -> URL? { 32 | let url = URL(string: config.region.hostUrlString(bucket: bucket)) 33 | return url 34 | } 35 | 36 | /// Save file 37 | public func save(file data: Data, to path: String, mime: MediaType, on container: Container) throws -> EventLoopFuture { 38 | let file = File.Upload.init(data: data, destination: path, access: .publicRead, mime: mime.description) 39 | return try s3.put(file: file, on: container).map(to: Void.self) { response in 40 | return Void() 41 | }.catchMap({ error in 42 | throw Error.failedWriting(path, error) 43 | }) 44 | } 45 | 46 | /// Save local file to an S3 bucket 47 | func copy(file path: String, to destination: String, on container: Container) throws -> EventLoopFuture { 48 | let url = URL(fileURLWithPath: path) 49 | let data: Data 50 | do { 51 | guard let localData = try load(localFile: url) else { 52 | throw Error.fileNotFound(path) 53 | } 54 | data = localData 55 | } catch { 56 | throw Error.failedCopy(path, destination, error) 57 | } 58 | return try save(file: data, to: destination, mime: (MediaType.fileExtension(url.pathExtension) ?? .plainText), on: container) 59 | } 60 | 61 | /// Move local file to an S3 bucket 62 | public func move(file path: String, to destination: String, on container: Container) throws -> EventLoopFuture { 63 | return try copy(file: path, to: destination, on: container).map(to: Void.self) { void in 64 | try FileManager.default.removeItem(atPath: path) 65 | return void 66 | } 67 | } 68 | 69 | /// Retrieve file 70 | public func get(file path: String, on container: Container) throws -> EventLoopFuture { 71 | return try s3.get(file: path, on: container).map(to: Data.self) { file in 72 | return file.data 73 | }.catchMap({ error in 74 | throw Error.failedReading(path, error) 75 | }) 76 | } 77 | 78 | /// Delete file 79 | public func delete(file path: String, on container: Container) throws -> EventLoopFuture { 80 | return try s3.delete(file: path, on: container).catchMap({ error in 81 | throw Error.failedRemoving(path, error) 82 | }) 83 | } 84 | 85 | /// Check if file exists 86 | public func exists(file path: String, on container: Container) throws -> EventLoopFuture { 87 | return try s3.get(file: path, on: container).map(to: Bool.self) { file in 88 | return true 89 | }.catchMap({ error in 90 | return false 91 | }) 92 | } 93 | 94 | /// Initializer 95 | init(_ config: S3Signer.Config, bucket: String) throws { 96 | self.config = config 97 | self.bucket = bucket 98 | 99 | s3 = try S3(defaultBucket: bucket, signer: S3Signer(config)) as S3Client 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /Sources/FileCore/Extensions/Request+Service.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Service.swift 3 | // FileCore 4 | // 5 | // Created by Ondrej Rafaj on 13/05/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Request { 13 | 14 | /// Make file core instance 15 | public func makeFileCore() throws -> CoreManager { 16 | return try make() 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/FileCore/Extensions/Services+Registration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Services+Registration.swift 3 | // FileCore 4 | // 5 | // Created by Ondrej Rafaj on 13/05/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | extension Services { 13 | 14 | /// Register FileCoreManager as a service 15 | public mutating func register(fileCoreManager config: FileCoreManager.Configuration) throws { 16 | try register(FileCoreManager(config), as: CoreManager.self) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/FileCore/Libs/Async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Async.swift 3 | // FileCore 4 | // 5 | // Created by Ondrej Rafaj on 12/05/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | /// Async stuff handling 12 | class Async { 13 | 14 | /// Default background dispatch queue 15 | static var dispatchQueue: DispatchQueue = { 16 | return DispatchQueue(label: "io.liveui.filecore") 17 | }() 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Sources/FileCore/Libs/Configuration/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 12/05/2018. 6 | // 7 | 8 | import Foundation 9 | import S3 10 | 11 | 12 | extension FileCoreManager.Configuration { 13 | 14 | /// Get local filesystem configuration if available 15 | public func localConfig() -> LocalConfig? { 16 | switch self { 17 | case .local(let config): 18 | return config 19 | default: 20 | return nil 21 | } 22 | } 23 | 24 | /// Get S3 configuration and bucket if available 25 | public func s3Config() -> (config: S3Signer.Config, bucket: String)? { 26 | switch self { 27 | case .s3(let config, let bucket): 28 | return (config: config, bucket: bucket) 29 | default: 30 | return nil 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/FileCore/Protocols/CoreManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreManager.swift 3 | // ApiCore 4 | // 5 | // Created by Ondrej Rafaj on 12/05/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | /// FileCore main protocol 13 | public protocol CoreManager: FileManagement { } 14 | -------------------------------------------------------------------------------- /Sources/FileCore/Protocols/FileManagement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManagement.swift 3 | // FileCore 4 | // 5 | // Created by Ondrej Rafaj on 12/05/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | /// Basic file management protocol 13 | public protocol FileManagement { 14 | 15 | /// Marks if service is remote or local 16 | var isRemote: Bool { get } 17 | 18 | 19 | /// Return a server url for the filesystem 20 | /// 21 | /// - Returns: URL (Optional) 22 | /// - Throws: Error 23 | func serverUrl() throws -> URL? 24 | 25 | /// Save file from data 26 | /// 27 | /// - parameters: 28 | /// - file: File data 29 | /// - to: Destination path 30 | /// - mime: File media type 31 | /// - on: Container to execure the operation on 32 | /// - returns: 33 | /// - Future 34 | func save(file: Data, to path: String, mime: MediaType, on: Container) throws -> Future 35 | 36 | /// Copy file from local file system 37 | /// 38 | /// - parameters: 39 | /// - file: Local file path 40 | /// - to: Destination path 41 | /// - on: Container to execure the operation on 42 | /// - returns: 43 | /// - Future 44 | func copy(file: String, to path: String, on: Container) throws -> Future 45 | 46 | /// Move file from local file system 47 | /// 48 | /// - parameters: 49 | /// - file: Local file path 50 | /// - to: Destination path 51 | /// - on: Container to execure the operation on 52 | /// - returns: 53 | /// - Future 54 | func move(file: String, to path: String, on: Container) throws -> Future 55 | 56 | /// Retrieve file 57 | /// 58 | /// - parameters: 59 | /// - file: Path to the file 60 | /// - on: Container to execure the operation on 61 | /// - returns: 62 | /// - Future 63 | func get(file: String, on: Container) throws -> Future 64 | 65 | /// Delete file 66 | /// 67 | /// - parameters: 68 | /// - file: Path to the file 69 | /// - on: Container to execure the operation on 70 | /// - returns: 71 | /// - Future 72 | func delete(file: String, on: Container) throws -> Future 73 | 74 | 75 | /// Check if file exists 76 | /// 77 | /// - Parameters: 78 | /// - file: Path to the file 79 | /// - on: Container to execure the operation on 80 | /// - returns: 81 | /// - Future 82 | func exists(file: String, on: Container) throws -> Future 83 | 84 | } 85 | 86 | // MARK: - Private helpers 87 | 88 | extension FileManagement { 89 | 90 | func load(localFile url: URL) throws -> Data? { 91 | if FileManager.default.fileExists(atPath: url.path) { 92 | let data = try Data(contentsOf: url) 93 | return data 94 | } 95 | return nil 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Sources/ImageCore/Extensions/Data+MediaType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+MediaType.swift 3 | // ImageCore 4 | // 5 | // Created by Ondrej Rafaj on 13/05/2018. 6 | // 7 | 8 | import Foundation 9 | @_exported import Vapor 10 | @_exported import SwiftGD 11 | 12 | 13 | extension Data { 14 | 15 | /// Image file extension 16 | /// Recognizes jpg, png, gif & tiff 17 | /// 18 | /// - returns: 19 | /// - String enxtension or nil if not valid image type 20 | public var imageFileExtension: String? { 21 | var values = [UInt8](repeating:0, count:1) 22 | copyBytes(to: &values, count: 1) 23 | switch (values[0]) { 24 | case 0xFF: 25 | return "jpg" 26 | case 0x89: 27 | return "png" 28 | case 0x47: 29 | return "gif" 30 | case 0x49, 0x4D : 31 | return "tiff" 32 | default: 33 | return nil 34 | } 35 | } 36 | 37 | /// Image file MediaType 38 | /// 39 | /// - returns: 40 | /// - MediaType or nil if not valid image type 41 | public func imageFileMediaType() -> MediaType? { 42 | guard let ext = imageFileExtension else { 43 | return nil 44 | } 45 | return MediaType.fileExtension(ext) 46 | } 47 | 48 | /// Check if data is a web image 49 | /// 50 | /// - returns: 51 | /// - Bool 52 | public func isWebImage() -> Bool { 53 | guard let ext = imageFileExtension else { 54 | return false 55 | } 56 | return ext == "jpg" || ext == "png" || ext == "gif" 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Sources/ImageCore/Extensions/MediaType+GD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaType+GD.swift 3 | // ImageCore 4 | // 5 | // Created by Ondrej Rafaj on 19/05/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import SwiftGD 11 | 12 | 13 | /// Method helpers for MediaType/GD 14 | extension MediaType { 15 | 16 | /// Convert MediaType to a SwiftGD compatible format 17 | public func gdMime() -> ImportableFormat? { 18 | switch self { 19 | case .gif: 20 | return .gif 21 | case .jpeg: 22 | return .jpg 23 | case .png: 24 | return .png 25 | default: 26 | return nil 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Sources/ImageCore/Extensions/RequestResponse+ImageCore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestResponse+ImageCore.swift 3 | // ImageCore 4 | // 5 | // Created by Ondrej Rafaj on 16/05/2018. 6 | // 7 | 8 | import Foundation 9 | import ErrorsCore 10 | import Vapor 11 | 12 | 13 | extension RequestResponse { 14 | 15 | /// Basic image response 16 | /// 17 | /// - parameters: 18 | /// - status: HTTPStatus, default .ok (200) 19 | /// - data: Image Data() 20 | public func image(_ data: Data, status: HTTPStatus = .ok) throws -> Response { 21 | let response = Response(using: request) 22 | response.http.status = status 23 | let mediaType = data.imageFileMediaType() 24 | let headers = HTTPHeaders([ 25 | ("Content-Type", (mediaType ?? .png).description), 26 | ("Content-Length", String(data.count)), 27 | ]) 28 | response.http.headers = headers 29 | response.http.body = HTTPBody(data: data) 30 | return response 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ImageCore/Extensions/Size+Tools.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Size+Tools.swift 3 | // ImageCore 4 | // 5 | // Created by Ondrej Rafaj on 17/05/2018. 6 | // 7 | 8 | import Foundation 9 | @_exported import SwiftGD 10 | 11 | 12 | extension Size { 13 | 14 | public func toString() -> String { 15 | return "{ width: \(width), height: \(height) }" 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ImageCore/Libs/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // ImageCore 4 | // 5 | // Created by Ondrej Rafaj on 15/04/2018. 6 | // 7 | 8 | import Foundation 9 | import COperatingSystem 10 | 11 | 12 | /// Color 13 | public class Color { 14 | 15 | /// Red 16 | public let r: Int 17 | 18 | /// Green 19 | public let g: Int 20 | 21 | /// Blue 22 | public let b: Int 23 | 24 | 25 | // MARK: Initialization 26 | 27 | /// Initializer 28 | public init(r: Int, g: Int, b: Int) { 29 | self.r = r 30 | self.g = g 31 | self.b = b 32 | } 33 | 34 | // MARK: Public interface 35 | 36 | /// Value of the color in hex (FF0000) 37 | public var hexValue: String { 38 | return Color.convert(r: r, g: g, b: b) 39 | } 40 | 41 | /// Is the color dark? 42 | public var isDark: Bool { 43 | let RGB = floatComponents() 44 | return (0.2126 * RGB[0] + 0.7152 * RGB[1] + 0.0722 * RGB[2]) < 0.5 45 | } 46 | 47 | /// Is the color B/W 48 | public var isBlackOrWhite: Bool { 49 | let RGB = floatComponents() 50 | return (RGB[0] > 0.91 && RGB[1] > 0.91 && RGB[2] > 0.91) || (RGB[0] < 0.09 && RGB[1] < 0.09 && RGB[2] < 0.09) 51 | } 52 | 53 | /// Is the color black 54 | public var isBlack: Bool { 55 | let RGB = floatComponents() 56 | return (RGB[0] < 0.09 && RGB[1] < 0.09 && RGB[2] < 0.09) 57 | } 58 | 59 | /// Is the color white 60 | public var isWhite: Bool { 61 | let RGB = floatComponents() 62 | return (RGB[0] > 0.91 && RGB[1] > 0.91 && RGB[2] > 0.91) 63 | } 64 | 65 | /// Is the color distinct from another color 66 | public func isDistinct(from color: Color) -> Bool { 67 | let bg = floatComponents() 68 | let fg = color.floatComponents() 69 | let threshold: Double = 0.25 70 | var result = false 71 | 72 | if fabs(bg[0] - fg[0]) > threshold || fabs(bg[1] - fg[1]) > threshold || fabs(bg[2] - fg[2]) > threshold { 73 | if fabs(bg[0] - bg[1]) < 0.03 && fabs(bg[0] - bg[2]) < 0.03 { 74 | if fabs(fg[0] - fg[1]) < 0.03 && fabs(fg[0] - fg[2]) < 0.03 { 75 | result = false 76 | } 77 | } 78 | result = true 79 | } 80 | 81 | return result 82 | } 83 | 84 | /// Is color contrasting with nother color 85 | public func isContrasting(with color: Color) -> Bool { 86 | let bg = floatComponents() 87 | let fg = color.floatComponents() 88 | 89 | let bgLum = 0.2126 * bg[0] + 0.7152 * bg[1] + 0.0722 * bg[2] 90 | let fgLum = 0.2126 * fg[0] + 0.7152 * fg[1] + 0.0722 * fg[2] 91 | let contrast = bgLum > fgLum 92 | ? (bgLum + 0.05) / (fgLum + 0.05) 93 | : (fgLum + 0.05) / (bgLum + 0.05) 94 | 95 | return 1.6 < contrast 96 | } 97 | 98 | // MARK: Private interface 99 | 100 | /// Float components 101 | internal func floatComponents() -> [Double] { 102 | return [r.floatColorValue, g.floatColorValue, b.floatColorValue] 103 | } 104 | 105 | // MARK: Static helpers 106 | 107 | /// Convert color to hex 108 | public static func convert(r: Int, g: Int, b: Int) -> String { 109 | let hexValue = String(format:"%02X", r) + String(format:"%02X", g) + String(format:"%02X", b) 110 | return hexValue 111 | } 112 | 113 | /// Get random color 114 | public static func randomColor() -> Color { 115 | return Color(r: Color.randomRGBValue, g: Color.randomRGBValue, b: Color.randomRGBValue) 116 | } 117 | 118 | } 119 | 120 | extension Color { 121 | 122 | // TODO: Replace with unified random from swift 4.2 when it becomes available!!! 123 | /// Make random Int within a range 124 | public static func randomInt(min: Int = 0, max: Int = Int.max) -> Int { 125 | let top = max - min + 1 126 | #if os(Linux) 127 | // will always be initialized 128 | guard randomInitializedBoost else { fatalError() } 129 | return Int(COperatingSystem.random() % top) + min 130 | #else 131 | return Int(arc4random_uniform(UInt32(top))) + min 132 | #endif 133 | } 134 | 135 | /// Random value 136 | public static var randomRGBValue: Int { 137 | return randomInt(min: 0, max: 255) 138 | } 139 | 140 | } 141 | 142 | extension Int { 143 | 144 | /// Float color value 145 | internal var floatColorValue: Double { 146 | return Double(self) / 255.0 147 | } 148 | 149 | } 150 | 151 | #if os(Linux) 152 | /// Generates a random number between (and inclusive of) 153 | /// the given minimum and maximum. 154 | private let randomInitializedBoost: Bool = { 155 | /// This stylized initializer is used to work around dispatch_once 156 | /// not existing and still guarantee thread safety 157 | let current = Date().timeIntervalSinceReferenceDate 158 | let salt = current.truncatingRemainder(dividingBy: 1) * 100000000 159 | COperatingSystem.srand(UInt32(current + salt)) 160 | return true 161 | }() 162 | #endif 163 | -------------------------------------------------------------------------------- /Sources/ImageCore/Libs/ImageError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageError.swift 3 | // ImageCore 4 | // 5 | // Created by Ondrej Rafaj on 13/05/2018. 6 | // 7 | 8 | import Foundation 9 | import ErrorsCore 10 | import Vapor 11 | 12 | 13 | /// Generic image errors 14 | public enum ImageError: FrontendError { 15 | 16 | /// Invalid image format 17 | case invalidImageFormat 18 | 19 | /// Error code 20 | public var identifier: String { 21 | switch self { 22 | case .invalidImageFormat: 23 | return "imagecore.invalid_image_format" 24 | } 25 | } 26 | 27 | /// Error desctiption 28 | public var reason: String { 29 | switch self { 30 | case .invalidImageFormat: 31 | return "Invalid image format" 32 | } 33 | } 34 | 35 | /// HTTP status code of the error 36 | public var status: HTTPStatus { 37 | switch self { 38 | case .invalidImageFormat: 39 | return .preconditionFailed 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ResourceCache/Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cache.swift 3 | // ResourceCache 4 | // 5 | // Created by Ondrej Rafaj on 28/05/2019. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | 11 | 12 | public class Cache: Service { 13 | 14 | public enum Error: Debuggable { 15 | 16 | case resourceNotFound 17 | 18 | case invalidUrl 19 | 20 | public var identifier: String { 21 | switch self { 22 | case .resourceNotFound: 23 | return "resource_cache.not_found" 24 | case .invalidUrl: 25 | return "resource_cache.invalid URL" 26 | } 27 | } 28 | 29 | public var reason: String { 30 | switch self { 31 | case .resourceNotFound: 32 | return "Resource has not been found" 33 | case .invalidUrl: 34 | return "Invalid URL" 35 | } 36 | } 37 | 38 | } 39 | 40 | public struct Config { 41 | 42 | public let storagePath: String 43 | 44 | public init(storagePath: String) { 45 | self.storagePath = storagePath 46 | } 47 | 48 | } 49 | 50 | public let config: Config 51 | 52 | public init(_ config: Config) { 53 | self.config = config 54 | } 55 | 56 | // MARK: Class interface 57 | 58 | public func get(url: URL, on req: Request) throws -> EventLoopFuture { 59 | guard let value = saved(file: url) else { 60 | let client = try req.make(Client.self) 61 | return client.get(url).flatMap({ response in 62 | return response.http.body.consumeData(max: 5_000_000, on: req).map({ [weak self] data in 63 | guard let value = String(data: data, encoding: .utf8) else { 64 | throw Error.resourceNotFound 65 | } 66 | try self?.save(content: value, from: url, on: req) 67 | return value 68 | }) 69 | }) 70 | } 71 | return req.eventLoop.newSucceededFuture(result: value) 72 | } 73 | 74 | public func get(url: String, on req: Request) throws -> EventLoopFuture { 75 | guard let url = URL(string: url) else { 76 | throw Error.invalidUrl 77 | } 78 | return try get(url: url, on: req) 79 | } 80 | 81 | // MARK: Private interface 82 | 83 | func saved(file url: URL) -> String? { 84 | guard let data = try? Data(contentsOf: file(path: url)) else { 85 | return nil 86 | } 87 | let string = String(data: data, encoding: .utf8) 88 | return string 89 | } 90 | 91 | func safe(text: String) -> String { 92 | var text = text.components(separatedBy: CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-").inverted).joined(separator: "-").lowercased() 93 | text = text.components(separatedBy: CharacterSet(charactersIn: "-")).filter { !$0.isEmpty }.joined(separator: "-") 94 | return text 95 | } 96 | 97 | func file(name url: URL) -> String { 98 | return safe(text: url.absoluteString).finished(with: ".").appending("cache") 99 | } 100 | 101 | func file(path url: URL) -> URL { 102 | let fileName = file(name: url) 103 | let path = URL(fileURLWithPath: config.storagePath).appendingPathComponent(fileName) 104 | return path 105 | } 106 | 107 | func save(content: String, from url: URL, on req: Request) throws { 108 | let path = file(path: url) 109 | try content.write(to: path, atomically: true, encoding: .utf8) 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /Tests/ApiCoreTests/ApiCoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiCoreTests.swift 3 | // ApiCoreTests 4 | // 5 | // Created by Ondrej Rafaj on 05/03/2018. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | import Vapor 11 | import VaporTestTools 12 | import ApiCoreTestTools 13 | import ErrorsCore 14 | @testable import ApiCore 15 | 16 | 17 | typealias CoreUser = User 18 | 19 | 20 | final class ApiCoreTests : XCTestCase, UsersTestCase, LinuxTests { 21 | 22 | var app: Application! 23 | 24 | var adminTeam: Team! 25 | 26 | var user1: User! 27 | var user2: User! 28 | 29 | 30 | // MARK: Setup 31 | 32 | override func setUp() { 33 | super.setUp() 34 | 35 | app = Application.testable.newApiCoreTestApp() 36 | 37 | setupUsers() 38 | } 39 | 40 | // MARK: Linux 41 | 42 | static let allTests: [(String, Any)] = [ 43 | ("testRequestHoldsSessionID", testRequestHoldsSessionID), 44 | ("testLinuxTests", testLinuxTests) 45 | ] 46 | 47 | func testLinuxTests() { 48 | doTestLinuxTestsAreOk() 49 | } 50 | 51 | // MARK: Tests 52 | 53 | func testRequestHoldsSessionID() { 54 | let req = HTTPRequest.testable.get(uri: "/ping", authorizedUser: user1, on: app) 55 | 56 | let r = app.testable.response(to: req) 57 | 58 | r.response.testable.debug() 59 | 60 | let uuid = r.request.sessionId 61 | XCTAssertEqual(uuid, r.request.sessionId, "Session ID needs to be the same") 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Tests/ApiCoreTests/Controllers/GenericControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenericControllerTests.swift 3 | // ApiCoreTests 4 | // 5 | // Created by Ondrej Rafaj on 27/02/2018. 6 | // 7 | 8 | import XCTest 9 | import Vapor 10 | import ApiCore 11 | import VaporTestTools 12 | import ApiCoreTestTools 13 | 14 | 15 | class GenericControllerTests: XCTestCase, UsersTestCase, LinuxTests { 16 | 17 | var app: Application! 18 | 19 | var adminTeam: Team! 20 | 21 | var user1: User! 22 | var user2: User! 23 | 24 | // MARK: Linux 25 | 26 | static let allTests: [(String, Any)] = [ 27 | ("testUnknownGet", testUnknownGet), 28 | ("testUnknownPost", testUnknownPost), 29 | ("testUnknownPut", testUnknownPut), 30 | ("testUnknownPatch", testUnknownPatch), 31 | ("testUnknownDelete", testUnknownDelete), 32 | ("testPing", testPing), 33 | ("testTeapot", testTeapot), 34 | ("testTables", testTables), 35 | ("testLinuxTests", testLinuxTests) 36 | ] 37 | 38 | func testLinuxTests() { 39 | doTestLinuxTestsAreOk() 40 | } 41 | 42 | // MARK: Setup 43 | 44 | override func setUp() { 45 | super.setUp() 46 | 47 | app = Application.testable.newApiCoreTestApp() 48 | 49 | setupUsers() 50 | } 51 | 52 | // MARK: Tests 53 | 54 | func testUnknownGet() { 55 | let req = HTTPRequest.testable.get(uri: "/unknown", authorizedUser: user1, on: app) 56 | let r = app.testable.response(to: req) 57 | 58 | r.response.testable.debug() 59 | 60 | testUnknown(response: r.response) 61 | } 62 | 63 | func testUnknownPost() { 64 | let req = HTTPRequest.testable.post(uri: "/unknown", authorizedUser: user1, on: app) 65 | let r = app.testable.response(to: req) 66 | 67 | r.response.testable.debug() 68 | 69 | testUnknown(response: r.response) 70 | } 71 | 72 | func testUnknownPut() { 73 | let req = HTTPRequest.testable.put(uri: "/unknown", authorizedUser: user1, on: app) 74 | let r = app.testable.response(to: req) 75 | 76 | r.response.testable.debug() 77 | 78 | testUnknown(response: r.response) 79 | } 80 | 81 | func testUnknownPatch() { 82 | let req = HTTPRequest.testable.patch(uri: "/unknown", authorizedUser: user1, on: app) 83 | let r = app.testable.response(to: req) 84 | 85 | r.response.testable.debug() 86 | 87 | testUnknown(response: r.response) 88 | } 89 | 90 | func testUnknownDelete() { 91 | let req = HTTPRequest.testable.delete(uri: "/unknown", authorizedUser: user1, on: app) 92 | let r = app.testable.response(to: req) 93 | 94 | r.response.testable.debug() 95 | 96 | testUnknown(response: r.response) 97 | } 98 | 99 | func testPing() { 100 | let req = HTTPRequest.testable.get(uri: "/ping") 101 | let r = app.testable.response(to: req) 102 | 103 | r.response.testable.debug() 104 | 105 | XCTAssertTrue(r.response.testable.has(statusCode: .ok), "Wrong status code") 106 | XCTAssertTrue(r.response.testable.has(contentType: "application/json; charset=utf-8"), "Missing content type") 107 | XCTAssertTrue(r.response.testable.has(contentLength: 15), "Wrong content length") 108 | XCTAssertTrue(r.response.testable.has(content: "{\"code\":\"pong\"}"), "Incorrect content") 109 | } 110 | 111 | func testTeapot() { 112 | let req = Request.testable.http.get(uri: "/teapot") 113 | let r = app.testable.response(to: req) 114 | 115 | r.response.testable.debug() 116 | 117 | XCTAssertTrue(r.response.testable.has(statusCode: .custom(code: 418, reasonPhrase: "I am teampot")), "Wrong status code") 118 | XCTAssertTrue(r.response.testable.has(contentType: "application/json; charset=utf-8"), "Missing content type") 119 | XCTAssertTrue(r.response.testable.has(contentLength: 178), "Wrong content length") 120 | } 121 | 122 | func testTables() { 123 | let req = Request.testable.http.get(uri: "/tables") 124 | let r = app.testable.response(to: req) 125 | 126 | r.response.testable.debug() 127 | } 128 | 129 | } 130 | 131 | 132 | extension GenericControllerTests { 133 | 134 | private func testUnknown(response res: Response) { 135 | res.testable.debug() 136 | 137 | XCTAssertTrue(res.testable.has(statusCode: .notFound), "Wrong status code. Should be not found (404)") 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /Tests/ApiCoreTests/Libs/StringCryptoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+CryptoTests.swift 3 | // ApiCoreTests 4 | // 5 | // Created by Ondrej Rafaj on 14/01/2018. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import ApiCore 11 | import Dispatch 12 | import XCTest 13 | import Crypto 14 | import VaporTestTools 15 | 16 | 17 | final class StringCryptoTests : XCTestCase { 18 | 19 | var app: Application! 20 | 21 | // MARK: Linux 22 | 23 | static let allTests = [ 24 | ("testPasswordHash", testPasswordHash) 25 | ] 26 | 27 | // MARK: Setup 28 | 29 | override func setUp() { 30 | super.setUp() 31 | 32 | app = Application.testable.newApiCoreTestApp() 33 | } 34 | 35 | // MARK: Tests 36 | 37 | func testPasswordHash() throws { 38 | let req = app.testable.fakeRequest() 39 | let hashed = try! "password".passwordHash(req) 40 | XCTAssertTrue("password".verify(against: hashed), "Hashed password is invalid") 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Tests/ApiCoreTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension ApiCoreTests { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__ApiCoreTests = [ 9 | ("testLinuxTests", testLinuxTests), 10 | ("testRequestHoldsSessionID", testRequestHoldsSessionID), 11 | ] 12 | } 13 | 14 | extension AuthControllerTests { 15 | // DO NOT MODIFY: This is autogenerated, use: 16 | // `swift test --generate-linuxmain` 17 | // to regenerate. 18 | static let __allTests__AuthControllerTests = [ 19 | ("testExpiredGetTokenAuthRequest", testExpiredGetTokenAuthRequest), 20 | ("testFailingPasswordCheck", testFailingPasswordCheck), 21 | ("testHtmlInputRecoveryRequest", testHtmlInputRecoveryRequest), 22 | ("testInvalidGetAuthRequest", testInvalidGetAuthRequest), 23 | ("testInvalidGetTokenAuthRequest", testInvalidGetTokenAuthRequest), 24 | ("testInvalidPostAuthRequest", testInvalidPostAuthRequest), 25 | ("testInvalidPostTokenAuthRequest", testInvalidPostTokenAuthRequest), 26 | ("testLinuxTests", testLinuxTests), 27 | ("testStartRecovery", testStartRecovery), 28 | ("testSuccessfulPasswordCheck", testSuccessfulPasswordCheck), 29 | ("testValidGetAuthRequest", testValidGetAuthRequest), 30 | ("testValidGetTokenAuthRequest", testValidGetTokenAuthRequest), 31 | ("testValidPostAuthRequest", testValidPostAuthRequest), 32 | ("testValidPostTokenAuthRequest", testValidPostTokenAuthRequest), 33 | ] 34 | } 35 | 36 | extension GenericControllerTests { 37 | // DO NOT MODIFY: This is autogenerated, use: 38 | // `swift test --generate-linuxmain` 39 | // to regenerate. 40 | static let __allTests__GenericControllerTests = [ 41 | ("testLinuxTests", testLinuxTests), 42 | ("testPing", testPing), 43 | ("testTables", testTables), 44 | ("testTeapot", testTeapot), 45 | ("testUnknownDelete", testUnknownDelete), 46 | ("testUnknownGet", testUnknownGet), 47 | ("testUnknownPatch", testUnknownPatch), 48 | ("testUnknownPost", testUnknownPost), 49 | ("testUnknownPut", testUnknownPut), 50 | ] 51 | } 52 | 53 | extension StringCryptoTests { 54 | // DO NOT MODIFY: This is autogenerated, use: 55 | // `swift test --generate-linuxmain` 56 | // to regenerate. 57 | static let __allTests__StringCryptoTests = [ 58 | ("testPasswordHash", testPasswordHash), 59 | ] 60 | } 61 | 62 | extension TeamsControllerTests { 63 | // DO NOT MODIFY: This is autogenerated, use: 64 | // `swift test --generate-linuxmain` 65 | // to regenerate. 66 | static let __allTests__TeamsControllerTests = [ 67 | ("testCreateEmptyTeam", testCreateEmptyTeam), 68 | ("testCreateTeam", testCreateTeam), 69 | ("testDeleteAdminTeam", testDeleteAdminTeam), 70 | ("testDeleteSingleTeam", testDeleteSingleTeam), 71 | ("testGetSingleTeam", testGetSingleTeam), 72 | ("testGetTeams", testGetTeams), 73 | ("testGetTeamUsers", testGetTeamUsers), 74 | ("testInvalidTeamNameCheck", testInvalidTeamNameCheck), 75 | ("testLinkUser", testLinkUser), 76 | ("testLinkUserSingleTeamUpdateFail", testLinkUserSingleTeamUpdateFail), 77 | ("testLinkUserThatDoesntExist", testLinkUserThatDoesntExist), 78 | ("testLinuxTests", testLinuxTests), 79 | ("testSingleTeamDeleteFail", testSingleTeamDeleteFail), 80 | ("testSingleTeamUpdateFail", testSingleTeamUpdateFail), 81 | ("testTryLinkUserWhereHeIs", testTryLinkUserWhereHeIs), 82 | ("testTryUnlinkUserWhereHeIsNot", testTryUnlinkUserWhereHeIsNot), 83 | ("testUnableToDeleteOtherPeoplesTeam", testUnableToDeleteOtherPeoplesTeam), 84 | ("testUnlinkUser", testUnlinkUser), 85 | ("testUnlinkUserSingleTeamUpdateFail", testUnlinkUserSingleTeamUpdateFail), 86 | ("testUnlinkUserThatDoesntExist", testUnlinkUserThatDoesntExist), 87 | ("testUnlinkYourselfWhenLastUser", testUnlinkYourselfWhenLastUser), 88 | ("testUpdateSingleTeam", testUpdateSingleTeam), 89 | ("testValidTeamNameCheck", testValidTeamNameCheck), 90 | ("testValidTeamNameCheckSingleTeamUpdateFail", testValidTeamNameCheckSingleTeamUpdateFail), 91 | ] 92 | } 93 | 94 | extension UsersControllerTests { 95 | // DO NOT MODIFY: This is autogenerated, use: 96 | // `swift test --generate-linuxmain` 97 | // to regenerate. 98 | static let __allTests__UsersControllerTests = [ 99 | ("testGetUsers", testGetUsers), 100 | ("testInviteUser", testInviteUser), 101 | ("testLinuxTests", testLinuxTests), 102 | ("testRegisterUser", testRegisterUser), 103 | ("testRegisterUserInvalidDomain1", testRegisterUserInvalidDomain1), 104 | ("testRegisterUserInvalidDomain2", testRegisterUserInvalidDomain2), 105 | ("testRegisterUserValidDomain", testRegisterUserValidDomain), 106 | ("testRegistrationsHaveBeenDisabled", testRegistrationsHaveBeenDisabled), 107 | ("testSearchUsersWithoutParams", testSearchUsersWithoutParams), 108 | ] 109 | } 110 | 111 | public func __allTests() -> [XCTestCaseEntry] { 112 | return [ 113 | testCase(ApiCoreTests.__allTests__ApiCoreTests), 114 | testCase(AuthControllerTests.__allTests__AuthControllerTests), 115 | testCase(GenericControllerTests.__allTests__GenericControllerTests), 116 | testCase(StringCryptoTests.__allTests__StringCryptoTests), 117 | testCase(TeamsControllerTests.__allTests__TeamsControllerTests), 118 | testCase(UsersControllerTests.__allTests__UsersControllerTests), 119 | ] 120 | } 121 | #endif 122 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ApiCoreTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ApiCoreTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /docker-compose.override.dist.yaml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | 3 | services: 4 | api: 5 | ports: 6 | - 8081:8080 7 | 8 | adminer: 9 | ports: 10 | - 8082:8080 11 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | 3 | services: 4 | api: 5 | image: einstore/einstore-base:2.0 6 | volumes: 7 | - .:/app 8 | working_dir: /app 9 | restart: on-failure 10 | environment: 11 | APICORE_STORAGE_LOCAL_ROOT: /home/apicore 12 | 13 | APICORE_SERVER_NAME: "ApiCore" 14 | APICORE_SERVER_PATH_PREFIX: ~ 15 | APICORE_SERVER_MAX_UPLOAD_FILESIZE: 800 16 | 17 | APICORE_DATABASE_HOST: postgres 18 | APICORE_DATABASE_USER: apicore 19 | APICORE_DATABASE_PASSWORD: apicore 20 | APICORE_DATABASE_DATABASE: apicore 21 | APICORE_DATABASE_PORT: 5432 22 | APICORE_DATABASE_LOGGING: 'false' 23 | 24 | APICORE_STORAGE_S3_ENABLED: 'false' 25 | 26 | APICORE_JWT_SECRET: secret 27 | 28 | command: ["swift", "run", "ApiCoreRun", "serve", "--hostname", "0.0.0.0", "--port", "8080"] 29 | 30 | postgres: 31 | image: postgres:11-alpine 32 | restart: always 33 | environment: 34 | POSTGRES_USER: apicore 35 | POSTGRES_PASSWORD: apicore 36 | POSTGRES_DB: apicore 37 | healthcheck: 38 | test: ["CMD-SHELL", "pg_isready -U apicore"] 39 | interval: 5s 40 | timeout: 5s 41 | retries: 5 42 | 43 | adminer: 44 | image: michalhosna/adminer:master 45 | environment: 46 | ADMINER_DB: apicore 47 | ADMINER_DRIVER: pgsql 48 | ADMINER_PASSWORD: apicore 49 | ADMINER_SERVER: postgres 50 | ADMINER_USERNAME: apicore 51 | ADMINER_AUTOLOGIN: 1 52 | ADMINER_NAME: ApiCore 53 | depends_on: 54 | - postgres 55 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker-compose build --no-cache 4 | docker-compose up --abort-on-container-exit 5 | -------------------------------------------------------------------------------- /scripts/docker-shortcuts/kill-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Kill and remove all old containers 4 | echo "💀 Kill all running containers" 5 | docker kill $(docker ps -q) 6 | 7 | echo "💀 Delete old containers" 8 | docker rm $(docker ps -a -q) 9 | -------------------------------------------------------------------------------- /scripts/generate-linuxmain.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | swift test --generate-linuxmain 4 | -------------------------------------------------------------------------------- /scripts/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | cmdname=$(basename $0) 5 | 6 | echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $TIMEOUT -gt 0 ]]; then 28 | echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" 29 | else 30 | echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" 31 | fi 32 | start_ts=$(date +%s) 33 | while : 34 | do 35 | if [[ $ISBUSY -eq 1 ]]; then 36 | nc -z $HOST $PORT 37 | result=$? 38 | else 39 | (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 40 | result=$? 41 | fi 42 | if [[ $result -eq 0 ]]; then 43 | end_ts=$(date +%s) 44 | echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" 45 | break 46 | fi 47 | sleep 1 48 | done 49 | return $result 50 | } 51 | 52 | wait_for_wrapper() 53 | { 54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 55 | if [[ $QUIET -eq 1 ]]; then 56 | timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 57 | else 58 | timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & 59 | fi 60 | PID=$! 61 | trap "kill -INT -$PID" INT 62 | wait $PID 63 | RESULT=$? 64 | if [[ $RESULT -ne 0 ]]; then 65 | echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" 66 | fi 67 | return $RESULT 68 | } 69 | 70 | # process arguments 71 | while [[ $# -gt 0 ]] 72 | do 73 | case "$1" in 74 | *:* ) 75 | hostport=(${1//:/ }) 76 | HOST=${hostport[0]} 77 | PORT=${hostport[1]} 78 | shift 1 79 | ;; 80 | --child) 81 | CHILD=1 82 | shift 1 83 | ;; 84 | -q | --quiet) 85 | QUIET=1 86 | shift 1 87 | ;; 88 | -s | --strict) 89 | STRICT=1 90 | shift 1 91 | ;; 92 | -h) 93 | HOST="$2" 94 | if [[ $HOST == "" ]]; then break; fi 95 | shift 2 96 | ;; 97 | --host=*) 98 | HOST="${1#*=}" 99 | shift 1 100 | ;; 101 | -p) 102 | PORT="$2" 103 | if [[ $PORT == "" ]]; then break; fi 104 | shift 2 105 | ;; 106 | --port=*) 107 | PORT="${1#*=}" 108 | shift 1 109 | ;; 110 | -t) 111 | TIMEOUT="$2" 112 | if [[ $TIMEOUT == "" ]]; then break; fi 113 | shift 2 114 | ;; 115 | --timeout=*) 116 | TIMEOUT="${1#*=}" 117 | shift 1 118 | ;; 119 | --) 120 | shift 121 | CLI=("$@") 122 | break 123 | ;; 124 | --help) 125 | usage 126 | ;; 127 | *) 128 | echoerr "Unknown argument: $1" 129 | usage 130 | ;; 131 | esac 132 | done 133 | 134 | if [[ "$HOST" == "" || "$PORT" == "" ]]; then 135 | echoerr "Error: you need to provide a host and port to test." 136 | usage 137 | fi 138 | 139 | TIMEOUT=${TIMEOUT:-15} 140 | STRICT=${STRICT:-0} 141 | CHILD=${CHILD:-0} 142 | QUIET=${QUIET:-0} 143 | 144 | # check to see if timeout is from busybox? 145 | # check to see if timeout is from busybox? 146 | TIMEOUT_PATH=$(realpath $(which timeout)) 147 | if [[ $TIMEOUT_PATH =~ "busybox" ]]; then 148 | ISBUSY=1 149 | BUSYTIMEFLAG="-t" 150 | else 151 | ISBUSY=0 152 | BUSYTIMEFLAG="" 153 | fi 154 | 155 | if [[ $CHILD -gt 0 ]]; then 156 | wait_for 157 | RESULT=$? 158 | exit $RESULT 159 | else 160 | if [[ $TIMEOUT -gt 0 ]]; then 161 | wait_for_wrapper 162 | RESULT=$? 163 | else 164 | wait_for 165 | RESULT=$? 166 | fi 167 | fi 168 | 169 | if [[ $CLI != "" ]]; then 170 | if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then 171 | echoerr "$cmdname: strict mode, refusing to execute subprocess" 172 | exit $RESULT 173 | fi 174 | exec "${CLI[@]}" 175 | else 176 | exit $RESULT 177 | fi 178 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker-compose -f docker-compose.test.yml build --no-cache 4 | docker-compose -f docker-compose.test.yml up 5 | # docker-compose -f docker-compose.test.yml up --abort-on-container-exit 6 | --------------------------------------------------------------------------------