├── .dockerignore ├── .github └── FUNDING.yml ├── .gitignore ├── Dockerfile ├── Package.resolved ├── Package.swift ├── Sources └── App │ ├── Controllers │ ├── .gitkeep │ ├── TodoController.swift │ └── UserController.swift │ ├── Middleware │ └── AdminMiddleware.swift │ ├── Migrations │ ├── CreateTodo.swift │ ├── CreateToken.swift │ └── CreateUser.swift │ ├── Models │ ├── Todo.swift │ ├── Token.swift │ └── User.swift │ ├── configure.swift │ ├── entrypoint.swift │ └── routes.swift ├── Tests └── AppTests │ └── AppTests.swift ├── docker-compose.yml └── startLocalDockerDB.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .swiftpm/ 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # repo: 0xTim/VaporAuthExample/.github 2 | # filename: FUNDING.YML 3 | 4 | github: 0xTim 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | xcuserdata 4 | *.xcodeproj 5 | DerivedData/ 6 | .DS_Store 7 | db.sqlite 8 | .swiftpm 9 | .vscode/ 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ================================ 2 | # Build image 3 | # ================================ 4 | FROM vapor/swift:5.2 as build 5 | WORKDIR /build 6 | 7 | # Copy entire repo into container 8 | COPY . . 9 | 10 | # Compile with optimizations 11 | RUN swift build \ 12 | --enable-test-discovery \ 13 | -c release \ 14 | -Xswiftc -g 15 | 16 | # ================================ 17 | # Run image 18 | # ================================ 19 | FROM vapor/ubuntu:18.04 20 | WORKDIR /run 21 | 22 | # Copy build artifacts 23 | COPY --from=build /build/.build/release /run 24 | # Copy Swift runtime libraries 25 | COPY --from=build /usr/lib/swift/ /usr/lib/swift/ 26 | 27 | ENTRYPOINT ["./Run"] 28 | CMD ["serve", "--env", "production", "--hostname", "0.0.0.0"] -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "816bb3f372339116173559a9b7817671b5e64c035c7bec2e738f00b45be1b84f", 3 | "pins" : [ 4 | { 5 | "identity" : "async-http-client", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/swift-server/async-http-client.git", 8 | "state" : { 9 | "revision" : "2119f0d9cc1b334e25447fe43d3693c0e60e6234", 10 | "version" : "1.24.0" 11 | } 12 | }, 13 | { 14 | "identity" : "async-kit", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/vapor/async-kit.git", 17 | "state" : { 18 | "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31", 19 | "version" : "1.20.0" 20 | } 21 | }, 22 | { 23 | "identity" : "console-kit", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/vapor/console-kit.git", 26 | "state" : { 27 | "revision" : "966d89ae64cd71c652a1e981bc971de59d64f13d", 28 | "version" : "4.15.1" 29 | } 30 | }, 31 | { 32 | "identity" : "fluent", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/vapor/fluent.git", 35 | "state" : { 36 | "revision" : "223b27d04ab2b51c25503c9922eecbcdf6c12f89", 37 | "version" : "4.12.0" 38 | } 39 | }, 40 | { 41 | "identity" : "fluent-kit", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/vapor/fluent-kit.git", 44 | "state" : { 45 | "revision" : "614d3ec27cdef50cfb9fc3cfd382b6a4d9578cff", 46 | "version" : "1.49.0" 47 | } 48 | }, 49 | { 50 | "identity" : "fluent-postgres-driver", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/vapor/fluent-postgres-driver.git", 53 | "state" : { 54 | "revision" : "fd57101e426d3edf66a32ba63a7d0b8ced4d7499", 55 | "version" : "2.10.0" 56 | } 57 | }, 58 | { 59 | "identity" : "multipart-kit", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/vapor/multipart-kit.git", 62 | "state" : { 63 | "revision" : "3498e60218e6003894ff95192d756e238c01f44e", 64 | "version" : "4.7.1" 65 | } 66 | }, 67 | { 68 | "identity" : "postgres-kit", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/vapor/postgres-kit.git", 71 | "state" : { 72 | "revision" : "0b72fa83b1023c4b82072e4049a3db6c29781fff", 73 | "version" : "2.13.5" 74 | } 75 | }, 76 | { 77 | "identity" : "postgres-nio", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/vapor/postgres-nio.git", 80 | "state" : { 81 | "revision" : "fd0e415a705c490499f983639b04f491a2ed9d99", 82 | "version" : "1.23.0" 83 | } 84 | }, 85 | { 86 | "identity" : "routing-kit", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/vapor/routing-kit.git", 89 | "state" : { 90 | "revision" : "8c9a227476555c55837e569be71944e02a056b72", 91 | "version" : "4.9.1" 92 | } 93 | }, 94 | { 95 | "identity" : "sql-kit", 96 | "kind" : "remoteSourceControl", 97 | "location" : "https://github.com/vapor/sql-kit.git", 98 | "state" : { 99 | "revision" : "e0b35ff07601465dd9f3af19a1c23083acaae3bd", 100 | "version" : "3.32.0" 101 | } 102 | }, 103 | { 104 | "identity" : "swift-algorithms", 105 | "kind" : "remoteSourceControl", 106 | "location" : "https://github.com/apple/swift-algorithms.git", 107 | "state" : { 108 | "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", 109 | "version" : "1.2.0" 110 | } 111 | }, 112 | { 113 | "identity" : "swift-asn1", 114 | "kind" : "remoteSourceControl", 115 | "location" : "https://github.com/apple/swift-asn1.git", 116 | "state" : { 117 | "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", 118 | "version" : "1.3.0" 119 | } 120 | }, 121 | { 122 | "identity" : "swift-async-algorithms", 123 | "kind" : "remoteSourceControl", 124 | "location" : "https://github.com/apple/swift-async-algorithms.git", 125 | "state" : { 126 | "revision" : "4c3ea81f81f0a25d0470188459c6d4bf20cf2f97", 127 | "version" : "1.0.3" 128 | } 129 | }, 130 | { 131 | "identity" : "swift-atomics", 132 | "kind" : "remoteSourceControl", 133 | "location" : "https://github.com/apple/swift-atomics.git", 134 | "state" : { 135 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 136 | "version" : "1.2.0" 137 | } 138 | }, 139 | { 140 | "identity" : "swift-collections", 141 | "kind" : "remoteSourceControl", 142 | "location" : "https://github.com/apple/swift-collections.git", 143 | "state" : { 144 | "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", 145 | "version" : "1.1.4" 146 | } 147 | }, 148 | { 149 | "identity" : "swift-crypto", 150 | "kind" : "remoteSourceControl", 151 | "location" : "https://github.com/apple/swift-crypto.git", 152 | "state" : { 153 | "revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779", 154 | "version" : "3.10.0" 155 | } 156 | }, 157 | { 158 | "identity" : "swift-distributed-tracing", 159 | "kind" : "remoteSourceControl", 160 | "location" : "https://github.com/apple/swift-distributed-tracing.git", 161 | "state" : { 162 | "revision" : "6483d340853a944c96dbcc28b27dd10b6c581703", 163 | "version" : "1.1.2" 164 | } 165 | }, 166 | { 167 | "identity" : "swift-http-types", 168 | "kind" : "remoteSourceControl", 169 | "location" : "https://github.com/apple/swift-http-types", 170 | "state" : { 171 | "revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3", 172 | "version" : "1.3.1" 173 | } 174 | }, 175 | { 176 | "identity" : "swift-log", 177 | "kind" : "remoteSourceControl", 178 | "location" : "https://github.com/apple/swift-log.git", 179 | "state" : { 180 | "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", 181 | "version" : "1.6.2" 182 | } 183 | }, 184 | { 185 | "identity" : "swift-metrics", 186 | "kind" : "remoteSourceControl", 187 | "location" : "https://github.com/apple/swift-metrics.git", 188 | "state" : { 189 | "revision" : "e0165b53d49b413dd987526b641e05e246782685", 190 | "version" : "2.5.0" 191 | } 192 | }, 193 | { 194 | "identity" : "swift-nio", 195 | "kind" : "remoteSourceControl", 196 | "location" : "https://github.com/apple/swift-nio.git", 197 | "state" : { 198 | "revision" : "dca6594f65308c761a9c409e09fbf35f48d50d34", 199 | "version" : "2.77.0" 200 | } 201 | }, 202 | { 203 | "identity" : "swift-nio-extras", 204 | "kind" : "remoteSourceControl", 205 | "location" : "https://github.com/apple/swift-nio-extras.git", 206 | "state" : { 207 | "revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6", 208 | "version" : "1.24.1" 209 | } 210 | }, 211 | { 212 | "identity" : "swift-nio-http2", 213 | "kind" : "remoteSourceControl", 214 | "location" : "https://github.com/apple/swift-nio-http2.git", 215 | "state" : { 216 | "revision" : "170f4ca06b6a9c57b811293cebcb96e81b661310", 217 | "version" : "1.35.0" 218 | } 219 | }, 220 | { 221 | "identity" : "swift-nio-ssl", 222 | "kind" : "remoteSourceControl", 223 | "location" : "https://github.com/apple/swift-nio-ssl.git", 224 | "state" : { 225 | "revision" : "c7e95421334b1068490b5d41314a50e70bab23d1", 226 | "version" : "2.29.0" 227 | } 228 | }, 229 | { 230 | "identity" : "swift-nio-transport-services", 231 | "kind" : "remoteSourceControl", 232 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 233 | "state" : { 234 | "revision" : "bbd5e63cf949b7db0c9edaf7a21e141c52afe214", 235 | "version" : "1.23.0" 236 | } 237 | }, 238 | { 239 | "identity" : "swift-numerics", 240 | "kind" : "remoteSourceControl", 241 | "location" : "https://github.com/apple/swift-numerics.git", 242 | "state" : { 243 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 244 | "version" : "1.0.2" 245 | } 246 | }, 247 | { 248 | "identity" : "swift-service-context", 249 | "kind" : "remoteSourceControl", 250 | "location" : "https://github.com/apple/swift-service-context.git", 251 | "state" : { 252 | "revision" : "0c62c5b4601d6c125050b5c3a97f20cce881d32b", 253 | "version" : "1.1.0" 254 | } 255 | }, 256 | { 257 | "identity" : "swift-service-lifecycle", 258 | "kind" : "remoteSourceControl", 259 | "location" : "https://github.com/swift-server/swift-service-lifecycle.git", 260 | "state" : { 261 | "revision" : "f70b838872863396a25694d8b19fe58bcd0b7903", 262 | "version" : "2.6.2" 263 | } 264 | }, 265 | { 266 | "identity" : "swift-system", 267 | "kind" : "remoteSourceControl", 268 | "location" : "https://github.com/apple/swift-system.git", 269 | "state" : { 270 | "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", 271 | "version" : "1.4.0" 272 | } 273 | }, 274 | { 275 | "identity" : "vapor", 276 | "kind" : "remoteSourceControl", 277 | "location" : "https://github.com/vapor/vapor.git", 278 | "state" : { 279 | "revision" : "4d7456c0d4b33ef82783a90ecfeae33a52a3972a", 280 | "version" : "4.111.0" 281 | } 282 | }, 283 | { 284 | "identity" : "websocket-kit", 285 | "kind" : "remoteSourceControl", 286 | "location" : "https://github.com/vapor/websocket-kit.git", 287 | "state" : { 288 | "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", 289 | "version" : "2.15.0" 290 | } 291 | } 292 | ], 293 | "version" : 3 294 | } 295 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "AuthExample", 6 | platforms: [ 7 | .macOS(.v12), 8 | ], 9 | dependencies: [ 10 | // 💧 A server-side Swift web framework. 11 | .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), 12 | .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), 13 | .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") 14 | ], 15 | targets: [ 16 | .executableTarget(name: "App", dependencies: [ 17 | .product(name: "Fluent", package: "fluent"), 18 | .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), 19 | .product(name: "Vapor", package: "vapor") 20 | ]), 21 | .testTarget(name: "AppTests", dependencies: [ 22 | .target(name: "App"), 23 | .product(name: "XCTVapor", package: "vapor"), 24 | ]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Sources/App/Controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xTim/VaporAuthExample/da09ccc0a3108ab8769a5a6d6037c7dcc0aa86b0/Sources/App/Controllers/.gitkeep -------------------------------------------------------------------------------- /Sources/App/Controllers/TodoController.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import Vapor 3 | 4 | struct TodoController: RouteCollection { 5 | 6 | func boot(routes: RoutesBuilder) throws { 7 | let tokenAuthRoutes = routes.grouped("todos").grouped(Token.authenticator(), User.guardMiddleware()) 8 | tokenAuthRoutes.get(use: indexHandler) 9 | tokenAuthRoutes.post(use: createHandler) 10 | tokenAuthRoutes.delete(":todoID", use: deleteHandler) 11 | } 12 | 13 | func indexHandler(_ req: Request) async throws -> [Todo] { 14 | return try await Todo.query(on: req.db).all() 15 | } 16 | 17 | func createHandler(_ req: Request) async throws -> Todo { 18 | let data = try req.content.decode(TodoCreateData.self) 19 | let user = try req.auth.require(User.self) 20 | let todo = try Todo(title: data.title, userID: user.requireID()) 21 | try await todo.save(on: req.db) 22 | return todo 23 | } 24 | 25 | func deleteHandler(_ req: Request) async throws -> HTTPStatus { 26 | guard let todo: Todo = try await Todo.find(req.parameters.get("todoID"), on: req.db) else { 27 | throw Abort(.notFound) 28 | } 29 | 30 | let user = try req.auth.require(User.self) 31 | guard try user.userType == .admin || user.requireID() == todo.$user.id else { 32 | throw Abort(.forbidden) 33 | } 34 | 35 | try await todo.delete(on: req.db) 36 | return .ok 37 | 38 | } 39 | } 40 | 41 | struct TodoCreateData: Content { 42 | let title: String 43 | } 44 | -------------------------------------------------------------------------------- /Sources/App/Controllers/UserController.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import Vapor 3 | 4 | struct UserController: RouteCollection { 5 | 6 | func boot(routes: RoutesBuilder) throws { 7 | let userRoutes = routes.grouped("users") 8 | userRoutes.get(use: indexHandler) 9 | userRoutes.post(use: createHandler) 10 | let httpBasicAuthRoutes = userRoutes.grouped(User.authenticator()) 11 | httpBasicAuthRoutes.post("login", use: loginHandler) 12 | 13 | // Token.authenticator.middleware() adds Bearer authentication with middleware, 14 | // Guard middlware ensures a user is logged in 15 | let tokenAuthRoutes = userRoutes.grouped(Token.authenticator(), User.guardMiddleware()) 16 | tokenAuthRoutes.get("me", use: getMyDetailsHandler) 17 | 18 | let adminMiddleware = tokenAuthRoutes.grouped(AdminMiddleware()) 19 | adminMiddleware.delete(":userID", use: deleteHandler) 20 | } 21 | 22 | func indexHandler(_ req: Request) async throws -> [User] { 23 | return try await User.query(on: req.db).all() 24 | } 25 | 26 | func createHandler(_ req: Request) async throws -> User { 27 | let userData = try req.content.decode(CreateUserData.self) 28 | let passwordHash = try Bcrypt.hash(userData.password) 29 | let user = User(name: userData.name, email: userData.email, passwordHash: passwordHash, userType: userData.userType) 30 | try await user.save(on: req.db) 31 | return user 32 | } 33 | 34 | func deleteHandler(_ req: Request) async throws -> HTTPStatus { 35 | guard let user: User = try await User.find(req.parameters.get("userID"), on: req.db) else { 36 | throw Abort(.notFound) 37 | } 38 | try await user.delete(on: req.db) 39 | return .ok 40 | } 41 | 42 | func loginHandler(_ req: Request) async throws -> Token { 43 | let user = try req.auth.require(User.self) 44 | let token = try user.generateToken() 45 | try await token.save(on: req.db) 46 | return token 47 | } 48 | 49 | func getMyDetailsHandler(_ req: Request) throws -> User { 50 | try req.auth.require(User.self) 51 | } 52 | } 53 | 54 | struct CreateUserData: Content { 55 | let name: String 56 | let email: String 57 | let password: String 58 | let userType: UserType 59 | } 60 | -------------------------------------------------------------------------------- /Sources/App/Middleware/AdminMiddleware.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | struct AdminMiddleware: AsyncMiddleware { 4 | func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response { 5 | guard let user = request.auth.get(User.self), user.userType == .admin else { 6 | throw Abort(.unauthorized) 7 | } 8 | 9 | return try await next.respond(to: request) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/App/Migrations/CreateTodo.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | 3 | struct CreateTodo: AsyncMigration { 4 | func prepare(on database: Database) async throws { 5 | try await database.schema("todos") 6 | .id() 7 | .field("title", .string, .required) 8 | .field("user_id", .uuid, .required, .references("users", "id")) 9 | .create() 10 | } 11 | 12 | func revert(on database: Database) async throws { 13 | try await database.schema("todos").delete() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/App/Migrations/CreateToken.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | 3 | struct CreateToken: AsyncMigration { 4 | func prepare(on database: Database) async throws { 5 | try await database.schema("tokens") 6 | .id() 7 | .field("token_value", .string, .required) 8 | .field("user_id", .uuid, .required, .references("users", "id")) 9 | .field("expires_at", .date, .required) 10 | .field("is_revoked", .bool, .required) 11 | .unique(on: "token_value") 12 | .create() 13 | } 14 | 15 | func revert(on database: Database) async throws { 16 | try await database.schema("tokens").delete() 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /Sources/App/Migrations/CreateUser.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | 3 | struct CreateUser: AsyncMigration { 4 | func prepare(on database: Database) async throws { 5 | try await database.schema("users") 6 | .id() 7 | .field("name", .string, .required) 8 | .field("email", .string, .required) 9 | .field("password_hash", .string, .required) 10 | .field("user_type", .string, .required) 11 | .unique(on: "email") 12 | .create() 13 | } 14 | 15 | func revert(on database: Database) async throws { 16 | try await database.schema("users").delete() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/App/Models/Todo.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import Vapor 3 | 4 | final class Todo: Model, Content, @unchecked Sendable { 5 | static let schema = "todos" 6 | 7 | @ID(key: .id) 8 | var id: UUID? 9 | 10 | @Field(key: "title") 11 | var title: String 12 | 13 | @Parent(key: "user_id") 14 | var user: User 15 | 16 | init() { } 17 | 18 | init(id: UUID? = nil, title: String, userID: User.IDValue) { 19 | self.id = id 20 | self.title = title 21 | self.$user.id = userID 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/App/Models/Token.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import Vapor 3 | 4 | final class Token: Model, Content, ModelTokenAuthenticatable, @unchecked Sendable { 5 | typealias User = App.User 6 | static let schema = "tokens" 7 | static var valueKey: KeyPath> { \.$value } 8 | static var userKey: KeyPath> { \.$user } 9 | 10 | var isValid: Bool { 11 | return self.expiresAt > Date() && !self.isRevoked 12 | } 13 | 14 | @ID(key: .id) 15 | var id: UUID? 16 | 17 | @Field(key: "token_value") 18 | var value: String 19 | 20 | @Parent(key: "user_id") 21 | var user: User 22 | 23 | @Field(key: "expires_at") 24 | var expiresAt: Date 25 | 26 | @Field(key: "is_revoked") 27 | var isRevoked: Bool 28 | 29 | init() { } 30 | 31 | init(id: UUID? = nil, value: String, userID: User.IDValue) { 32 | self.id = id 33 | self.value = value 34 | self.$user.id = userID 35 | // Set expirty to 30 days 36 | self.expiresAt = Date().advanced(by: 60 * 60 * 24 * 30) 37 | self.isRevoked = false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/App/Models/User.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import Vapor 3 | 4 | final class User: Model, Content, ModelAuthenticatable, @unchecked Sendable { 5 | static let schema = "users" 6 | static var usernameKey: KeyPath> { \.$email } 7 | static var passwordHashKey: KeyPath> { \.$passwordHash } 8 | 9 | @ID(key: .id) 10 | var id: UUID? 11 | 12 | @Field(key: "name") 13 | var name: String 14 | 15 | @Field(key: "email") 16 | var email: String 17 | 18 | @Field(key: "password_hash") 19 | var passwordHash: String 20 | 21 | @Field(key: "user_type") 22 | var userType: UserType 23 | 24 | init() { } 25 | 26 | init(id: UUID? = nil, name: String, email: String, passwordHash: String, userType: UserType) { 27 | self.id = id 28 | self.name = name 29 | self.email = email 30 | self.passwordHash = passwordHash 31 | self.userType = userType 32 | } 33 | 34 | func verify(password: String) throws -> Bool { 35 | try Bcrypt.verify(password, created: self.passwordHash) 36 | } 37 | } 38 | 39 | extension User { 40 | func generateToken() throws -> Token { 41 | try .init( 42 | value: [UInt8].random(count: 32).base64, 43 | userID: self.requireID() 44 | ) 45 | } 46 | } 47 | 48 | enum UserType: String, Content { 49 | case normal 50 | case admin 51 | } 52 | -------------------------------------------------------------------------------- /Sources/App/configure.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import FluentPostgresDriver 3 | import Vapor 4 | 5 | // configures your application 6 | public func configure(_ app: Application) async throws { 7 | // uncomment to serve files from /Public folder 8 | // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) 9 | 10 | app.databases.use(DatabaseConfigurationFactory.postgres(configuration: .init( 11 | hostname: Environment.get("DATABASE_HOST") ?? "localhost", 12 | port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber, 13 | username: Environment.get("DATABASE_USERNAME") ?? "vapor_username", 14 | password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password", 15 | database: Environment.get("DATABASE_NAME") ?? "vapor_database", 16 | tls: .prefer(try .init(configuration: .clientDefault))) 17 | ), as: .psql) 18 | 19 | app.migrations.add(CreateUser()) 20 | app.migrations.add(CreateTodo()) 21 | app.migrations.add(CreateToken()) 22 | 23 | // register routes 24 | try routes(app) 25 | } 26 | -------------------------------------------------------------------------------- /Sources/App/entrypoint.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import Logging 3 | import NIOCore 4 | import NIOPosix 5 | 6 | @main 7 | enum Entrypoint { 8 | static func main() async throws { 9 | var env = try Environment.detect() 10 | try LoggingSystem.bootstrap(from: &env) 11 | 12 | let app = try await Application.make(env) 13 | 14 | // This attempts to install NIO as the Swift Concurrency global executor. 15 | // You can enable it if you'd like to reduce the amount of context switching between NIO and Swift Concurrency. 16 | // Note: this has caused issues with some libraries that use `.wait()` and cleanly shutting down. 17 | // If enabled, you should be careful about calling async functions before this point as it can cause assertion failures. 18 | // let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor() 19 | // app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)]) 20 | 21 | do { 22 | try await configure(app) 23 | } catch { 24 | app.logger.report(error: error) 25 | try? await app.asyncShutdown() 26 | throw error 27 | } 28 | try await app.execute() 29 | try await app.asyncShutdown() 30 | } 31 | } -------------------------------------------------------------------------------- /Sources/App/routes.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import Vapor 3 | 4 | func routes(_ app: Application) throws { 5 | app.get { req in 6 | return "It works!" 7 | } 8 | 9 | app.get("hello") { req -> String in 10 | return "Hello, world!" 11 | } 12 | 13 | try app.register(collection: UserController()) 14 | try app.register(collection: TodoController()) 15 | } 16 | -------------------------------------------------------------------------------- /Tests/AppTests/AppTests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTVapor 3 | 4 | final class AppTests: XCTestCase { 5 | func testHelloWorld() throws { 6 | let app = Application(.testing) 7 | defer { app.shutdown() } 8 | try configure(app) 9 | 10 | try app.test(.GET, "hello") { res in 11 | XCTAssertEqual(res.status, .ok) 12 | XCTAssertEqual(res.body.string, "Hello, world!") 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker Compose file for Vapor 2 | # 3 | # Install Docker on your system to run and test 4 | # your Vapor app in a production-like environment. 5 | # 6 | # Note: This file is intended for testing and does not 7 | # implement best practices for a production deployment. 8 | # 9 | # Learn more: https://docs.docker.com/compose/reference/ 10 | # 11 | # Build images: docker-compose build 12 | # Start app: docker-compose up app 13 | # Start database: docker-compose up db 14 | # Run migrations: docker-compose up migrate 15 | # Stop all: docker-compose down (add -v to wipe db) 16 | # 17 | version: '3.7' 18 | 19 | volumes: 20 | db_data: 21 | 22 | x-shared_environment: &shared_environment 23 | LOG_LEVEL: ${LOG_LEVEL:-debug} 24 | DATABASE_HOST: db 25 | DATABASE_NAME: vapor_database 26 | DATABASE_USERNAME: vapor_username 27 | DATABASE_PASSWORD: vapor_password 28 | 29 | services: 30 | app: 31 | image: AuthExample:latest 32 | build: 33 | context: . 34 | environment: 35 | <<: *shared_environment 36 | depends_on: 37 | - db 38 | ports: 39 | - '8080:80' 40 | command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "80"] 41 | migrate: 42 | image: AuthExample:latest 43 | build: 44 | context: . 45 | environment: 46 | <<: *shared_environment 47 | depends_on: 48 | - db 49 | command: ["migrate", "--yes"] 50 | deploy: 51 | replicas: 0 52 | revert: 53 | image: AuthExample:latest 54 | build: 55 | context: . 56 | environment: 57 | <<: *shared_environment 58 | depends_on: 59 | - db 60 | command: ["migrate", "--revert", "--yes"] 61 | deploy: 62 | replicas: 0 63 | db: 64 | image: postgres:12.1-alpine 65 | volumes: 66 | - db_data:/var/lib/postgresql/data/pgdata 67 | environment: 68 | PGDATA: /var/lib/postgresql/data/pgdata 69 | POSTGRES_USER: vapor_username 70 | POSTGRES_PASSWORD: vapor_password 71 | POSTGRES_DB: vapor_database 72 | ports: 73 | - '5432:5432' -------------------------------------------------------------------------------- /startLocalDockerDB.sh: -------------------------------------------------------------------------------- 1 | docker run --name postgres-authtest -e POSTGRES_DB=vapor_database -e POSTGRES_USER=vapor_username -e POSTGRES_PASSWORD=vapor_password -p 5432:5432 -d postgres 2 | --------------------------------------------------------------------------------