├── Resources
├── commit.txt
└── Templates
│ ├── email.password-recovery.plain.leaf
│ ├── email.registration.plain.leaf
│ ├── email.invitation.plain.leaf
│ ├── email.password-recovery.html.leaf
│ ├── email.registration.html.leaf
│ └── email.invitation.html.leaf
├── Other
├── logo.png
├── default-logo.png
└── default-logo-512.png
├── scripts
├── generate-linuxmain.sh
├── docker-shortcuts
│ └── kill-all.sh
└── wait-for-it.sh
├── run.sh
├── Tests
├── LinuxMain.swift
└── ApiCoreTests
│ ├── Libs
│ └── StringCryptoTests.swift
│ ├── ApiCoreTests.swift
│ ├── Controllers
│ └── GenericControllerTests.swift
│ └── XCTestManifests.swift
├── docker-compose.override.dist.yaml
├── test.sh
├── .gitignore
├── Sources
├── FileCore
│ ├── Protocols
│ │ ├── CoreManager.swift
│ │ └── FileManagement.swift
│ ├── Extensions
│ │ ├── Request+Service.swift
│ │ └── Services+Registration.swift
│ ├── Libs
│ │ ├── Async.swift
│ │ └── Configuration
│ │ │ └── Configuration.swift
│ └── Clients
│ │ ├── Local
│ │ └── LocalConfig.swift
│ │ └── S3
│ │ └── S3LibClient.swift
├── ApiCoreTestTools
│ ├── ApiCoreTestTools.swift
│ ├── Extensions
│ │ ├── Team+Testable.swift
│ │ ├── User+Testable.swift
│ │ ├── Application+Testable.swift
│ │ └── HTTPRequest+Make.swift
│ └── Helpers
│ │ ├── TeamsTestCase.swift
│ │ ├── UsersTestCase.swift
│ │ └── LinuxTests.swift
├── ApiCore
│ ├── Libs
│ │ ├── Audit
│ │ │ ├── Audit.swift
│ │ │ ├── ConfigurationAudit.swift
│ │ │ └── SecurityAudit.swift
│ │ ├── RequestIdService.swift
│ │ ├── Filesize.swift
│ │ ├── Images
│ │ │ └── Icon.swift
│ │ ├── Env.swift
│ │ ├── ServerIcon.swift
│ │ ├── Result.swift
│ │ ├── Gravatar.swift
│ │ ├── Coding
│ │ │ └── Decodable+Helpers.swift
│ │ ├── Me.swift
│ │ ├── Templates.swift
│ │ └── AuthError.swift
│ ├── Extensions
│ │ ├── Request+Auth.swift
│ │ ├── EventLoop+Future.swift
│ │ ├── DatabaseIdentifier+Db.swift
│ │ ├── Future+Tools.swift
│ │ ├── Model+Helpers.swift
│ │ ├── Response+Tools.swift
│ │ ├── Request+URL.swift
│ │ ├── Request+Files.swift
│ │ ├── Router+Extended.swift
│ │ ├── String+Security.swift
│ │ ├── Data+Tools.swift
│ │ ├── HTTPHeaders+Tools.swift
│ │ ├── Data+Response.swift
│ │ ├── FileCore
│ │ │ └── CoreManager+Location.swift
│ │ ├── Encodable+Tools.swift
│ │ ├── Content+Response.swift
│ │ ├── String+Crypto.swift
│ │ ├── Future+Response.swift
│ │ ├── Date+Tools.swift
│ │ └── String+Manipulation.swift
│ ├── Protocols
│ │ ├── Controller.swift
│ │ └── EmailTemplateData.swift
│ ├── Auth
│ │ ├── Github
│ │ │ ├── Model
│ │ │ │ ├── GithubEmail.swift
│ │ │ │ ├── GithubUserInfo.swift
│ │ │ │ └── GithubUser.swift
│ │ │ ├── Configuration
│ │ │ │ └── GithubConfig.swift
│ │ │ ├── GithubLoginManager.swift
│ │ │ └── Extensions
│ │ │ │ └── URL+Parameters.swift
│ │ ├── Gitlab
│ │ │ ├── Configuration
│ │ │ │ └── GitlabConfig.swift
│ │ │ ├── GitlabLoginManager.swift
│ │ │ ├── Extensions
│ │ │ │ └── Gitlab+URLParameters.swift
│ │ │ └── Model
│ │ │ │ ├── GitlabUserInfo.swift
│ │ │ │ └── GitlabUser.swift
│ │ └── Generic
│ │ │ ├── Extensions
│ │ │ └── URL+Authenticated.swift
│ │ │ ├── Libs
│ │ │ └── AuthenticableJWTService.swift
│ │ │ ├── Protocols
│ │ │ └── Authenticable.swift
│ │ │ ├── Model
│ │ │ └── Authenticated.swift
│ │ │ └── Auth.swift
│ ├── Database
│ │ ├── DbCoreModel.swift
│ │ ├── ApiCoreDb.swift
│ │ └── DbError.swift
│ ├── Middleware
│ │ ├── UrlPrinterMiddleware.swift
│ │ ├── DebugCheckMiddleware.swift
│ │ ├── ErrorLoggingMiddleware.swift
│ │ └── ApiAuthMiddleware.swift
│ ├── Model
│ │ ├── FrontendSystemData.swift
│ │ ├── Authenticator.swift
│ │ ├── ServerSecurity.swift
│ │ ├── UserSource.swift
│ │ ├── FluentDesign.swift
│ │ ├── Setting.swift
│ │ ├── System.swift
│ │ ├── TeamUser.swift
│ │ ├── Query
│ │ │ └── BasicQuery.swift
│ │ ├── ErrorLog.swift
│ │ ├── Info.swift
│ │ └── Token.swift
│ ├── Config
│ │ ├── Base setup
│ │ │ ├── ApiCoreBase+Configuration.swift
│ │ │ ├── ApiCoreBase+Controllers.swift
│ │ │ ├── ApiCoreBase+CORS.swift
│ │ │ ├── ApiCoreBase+Storage.swift
│ │ │ ├── ApiCoreBase+Middlewares.swift
│ │ │ ├── ApiCoreBase+Email.swift
│ │ │ └── ApiCoreBase+Database.swift
│ │ └── Configurable.swift
│ ├── Controllers
│ │ ├── LogsController.swift
│ │ ├── GenericController.swift
│ │ ├── SettingsController.swift
│ │ └── InstallController.swift
│ ├── Migrations
│ │ └── BaseMigration.swift
│ ├── Managers
│ │ ├── SystemManager.swift
│ │ └── AuthManager.swift
│ └── ApiCoreBase.swift
├── ImageCore
│ ├── Extensions
│ │ ├── Size+Tools.swift
│ │ ├── MediaType+GD.swift
│ │ ├── RequestResponse+ImageCore.swift
│ │ └── Data+MediaType.swift
│ └── Libs
│ │ ├── ImageError.swift
│ │ └── Color.swift
├── ApiCoreRun
│ └── main.swift
├── ApiCoreApp
│ └── configure.swift
└── ResourceCache
│ └── Cache.swift
├── Dockerfile
├── Jenkinsfile
├── docker-compose.yaml
├── Makefile
└── Package.swift
/Resources/commit.txt:
--------------------------------------------------------------------------------
1 | unknown
--------------------------------------------------------------------------------
/Other/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LiveUI/ApiCore/HEAD/Other/logo.png
--------------------------------------------------------------------------------
/Other/default-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LiveUI/ApiCore/HEAD/Other/default-logo.png
--------------------------------------------------------------------------------
/scripts/generate-linuxmain.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | swift test --generate-linuxmain
4 |
--------------------------------------------------------------------------------
/Other/default-logo-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LiveUI/ApiCore/HEAD/Other/default-logo-512.png
--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | docker-compose build --no-cache
4 | docker-compose up --abort-on-container-exit
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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+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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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+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/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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------