├── .assets └── VaporSwift-Github-1280x640px.png ├── .dockerignore ├── .gitignore ├── .spi.yml ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── Package.resolved ├── Package.swift ├── Public └── .gitkeep ├── README.md ├── SECURITY.md ├── Sources ├── ExampleApp │ ├── Controllers │ │ ├── .gitkeep │ │ ├── AccountController.swift │ │ └── AuthController.swift │ ├── Migrations │ │ ├── 0001-CreateUser.swift │ │ ├── 0002-CreateUserToken.swift │ │ └── migrations.swift │ ├── Models │ │ ├── API │ │ │ ├── Request │ │ │ │ ├── CreateUserData.swift │ │ │ │ └── SignInWithAppleToken.swift │ │ │ └── Response │ │ │ │ ├── LoginResponse.swift │ │ │ │ └── User+Public.swift │ │ └── Fluent │ │ │ ├── App+FieldKeys.swift │ │ │ ├── User.swift │ │ │ └── UserToken.swift │ ├── app.swift │ ├── configure.swift │ └── routes.swift └── StreamSDKVapor │ ├── Application+Stream.swift │ ├── Request+Stream.swift │ ├── StreamClient.swift │ ├── StreamConfiguration.swift │ ├── StreamJWTPayload.swift │ └── StreamToken.swift ├── Tests └── ExampleAppTests │ └── ExampleAppTests.swift ├── docker-compose.yml ├── scripts └── localDockerDB.swift └── vapor.paw /.assets/VaporSwift-Github-1280x640px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-chat-vapor-swift/10ff8721f882a2714e06e5dd63713d0cf697a929/.assets/VaporSwift-Github-1280x640px.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .swiftpm/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | xcuserdata 4 | *.xcodeproj 5 | DerivedData/ 6 | .DS_Store 7 | db.sqlite 8 | .swiftpm 9 | .env 10 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [StreamSDKVapor] 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "lldb", 5 | "request": "launch", 6 | "name": "Debug Run", 7 | "program": "${workspaceFolder:StreamChat}/.build/debug/Run", 8 | "args": [], 9 | "cwd": "${workspaceFolder:StreamChat}", 10 | "preLaunchTask": "swift: Build Debug Run" 11 | }, 12 | { 13 | "type": "lldb", 14 | "request": "launch", 15 | "name": "Release Run", 16 | "program": "${workspaceFolder:StreamChat}/.build/release/Run", 17 | "args": [], 18 | "cwd": "${workspaceFolder:StreamChat}", 19 | "preLaunchTask": "swift: Build Release Run" 20 | }, 21 | { 22 | "type": "lldb", 23 | "request": "launch", 24 | "name": "Debug ExampleApp", 25 | "program": "${workspaceFolder:StreamChat}/.build/debug/ExampleApp", 26 | "args": [], 27 | "cwd": "${workspaceFolder:StreamChat}", 28 | "preLaunchTask": "swift: Build Debug ExampleApp" 29 | }, 30 | { 31 | "type": "lldb", 32 | "request": "launch", 33 | "name": "Release ExampleApp", 34 | "program": "${workspaceFolder:StreamChat}/.build/release/ExampleApp", 35 | "args": [], 36 | "cwd": "${workspaceFolder:StreamChat}", 37 | "preLaunchTask": "swift: Build Release ExampleApp" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ================================ 2 | # Build image 3 | # ================================ 4 | FROM swift:5.5-focal as build 5 | 6 | # Install OS updates and, if needed, sqlite3 7 | RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ 8 | && apt-get -q update \ 9 | && apt-get -q dist-upgrade -y \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | # Set up a build area 13 | WORKDIR /build 14 | 15 | # First just resolve dependencies. 16 | # This creates a cached layer that can be reused 17 | # as long as your Package.swift/Package.resolved 18 | # files do not change. 19 | COPY ./Package.* ./ 20 | RUN swift package resolve 21 | 22 | # Copy entire repo into container 23 | COPY . . 24 | 25 | # Build everything, with optimizations 26 | RUN swift build -c release --static-swift-stdlib 27 | 28 | # Switch to the staging area 29 | WORKDIR /staging 30 | 31 | # Copy main executable to staging area 32 | RUN cp "$(swift build --package-path /build -c release --show-bin-path)/Run" ./ 33 | 34 | # Copy any resources from the public directory and views directory if the directories exist 35 | # Ensure that by default, neither the directory nor any of its contents are writable. 36 | RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true 37 | RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true 38 | 39 | # ================================ 40 | # Run image 41 | # ================================ 42 | FROM ubuntu:focal 43 | 44 | # Make sure all system packages are up to date. 45 | RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true && \ 46 | apt-get -q update && apt-get -q dist-upgrade -y && apt-get -q install -y ca-certificates && \ 47 | rm -r /var/lib/apt/lists/* 48 | 49 | # Create a vapor user and group with /app as its home directory 50 | RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor 51 | 52 | # Switch to the new home directory 53 | WORKDIR /app 54 | 55 | # Copy built executable and any staged resources from builder 56 | COPY --from=build --chown=vapor:vapor /staging /app 57 | 58 | # Ensure all further commands run as the vapor user 59 | USER vapor:vapor 60 | 61 | # Let Docker bind to port 8080 62 | EXPOSE 8080 63 | 64 | # Start the Vapor service when the image is run, default to listening on 8080 in production environment 65 | ENTRYPOINT ["./Run"] 66 | CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 STREAM.IO, INC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "794dc9d42720af97cedd395e8cd2add9173ffd9a", 10 | "version": "1.11.1" 11 | } 12 | }, 13 | { 14 | "package": "async-kit", 15 | "repositoryURL": "https://github.com/vapor/async-kit.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "017dc7da68c1ec9f0f46fcd1a8002d14a5662732", 19 | "version": "1.12.0" 20 | } 21 | }, 22 | { 23 | "package": "console-kit", 24 | "repositoryURL": "https://github.com/vapor/console-kit.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "c085098102c18ac5b3eac741fd4101868ddb5a2e", 28 | "version": "4.4.1" 29 | } 30 | }, 31 | { 32 | "package": "fluent", 33 | "repositoryURL": "https://github.com/vapor/fluent.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "ea707ee318066a073c95b2b2df1aa640fcb67f9e", 37 | "version": "4.4.0" 38 | } 39 | }, 40 | { 41 | "package": "fluent-kit", 42 | "repositoryURL": "https://github.com/vapor/fluent-kit.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "2b3ac0556740ea8d68f8616917daf64338d54c1b", 46 | "version": "1.29.2" 47 | } 48 | }, 49 | { 50 | "package": "fluent-postgres-driver", 51 | "repositoryURL": "https://github.com/vapor/fluent-postgres-driver.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "5230817eaab7184bcaf0b6ebb471f175df980035", 55 | "version": "2.3.0" 56 | } 57 | }, 58 | { 59 | "package": "Imperial", 60 | "repositoryURL": "https://github.com/vapor-community/Imperial.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "3795364cf6fd2753be7bb14cc55e414f01727c6c", 64 | "version": "1.1.0" 65 | } 66 | }, 67 | { 68 | "package": "jwt", 69 | "repositoryURL": "https://github.com/vapor/jwt.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "f9dc4cb5c49e120dd73b740a571cfd289ac30cb8", 73 | "version": "4.2.0" 74 | } 75 | }, 76 | { 77 | "package": "jwt-kit", 78 | "repositoryURL": "https://github.com/vapor/jwt-kit.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "718e94f917a4c053758e5bb50c28553561a6ef91", 82 | "version": "4.6.0" 83 | } 84 | }, 85 | { 86 | "package": "multipart-kit", 87 | "repositoryURL": "https://github.com/vapor/multipart-kit.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "0d55c35e788451ee27222783c7d363cb88092fab", 91 | "version": "4.5.2" 92 | } 93 | }, 94 | { 95 | "package": "postgres-kit", 96 | "repositoryURL": "https://github.com/vapor/postgres-kit.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "10797f400ff0054cce59a035b3ac37376c0ea4f7", 100 | "version": "2.8.0" 101 | } 102 | }, 103 | { 104 | "package": "postgres-nio", 105 | "repositoryURL": "https://github.com/vapor/postgres-nio.git", 106 | "state": { 107 | "branch": null, 108 | "revision": "d648c5b4594ffbc2f6173318f70f5531e05ccb4e", 109 | "version": "1.11.0" 110 | } 111 | }, 112 | { 113 | "package": "routing-kit", 114 | "repositoryURL": "https://github.com/vapor/routing-kit.git", 115 | "state": { 116 | "branch": null, 117 | "revision": "9e181d685a3dec1eef1fc6dacf606af364f86d68", 118 | "version": "4.5.0" 119 | } 120 | }, 121 | { 122 | "package": "sql-kit", 123 | "repositoryURL": "https://github.com/vapor/sql-kit.git", 124 | "state": { 125 | "branch": null, 126 | "revision": "89b0a0a5f110e77272fb5a775064a31bfc1f155c", 127 | "version": "3.18.0" 128 | } 129 | }, 130 | { 131 | "package": "swift-algorithms", 132 | "repositoryURL": "https://github.com/apple/swift-algorithms.git", 133 | "state": { 134 | "branch": null, 135 | "revision": "b14b7f4c528c942f121c8b860b9410b2bf57825e", 136 | "version": "1.0.0" 137 | } 138 | }, 139 | { 140 | "package": "swift-backtrace", 141 | "repositoryURL": "https://github.com/swift-server/swift-backtrace.git", 142 | "state": { 143 | "branch": null, 144 | "revision": "f25620d5d05e2f1ba27154b40cafea2b67566956", 145 | "version": "1.3.3" 146 | } 147 | }, 148 | { 149 | "package": "swift-collections", 150 | "repositoryURL": "https://github.com/apple/swift-collections.git", 151 | "state": { 152 | "branch": null, 153 | "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", 154 | "version": "1.0.2" 155 | } 156 | }, 157 | { 158 | "package": "swift-crypto", 159 | "repositoryURL": "https://github.com/apple/swift-crypto.git", 160 | "state": { 161 | "branch": null, 162 | "revision": "d9825fa541df64b1a7b182178d61b9a82730d01f", 163 | "version": "2.1.0" 164 | } 165 | }, 166 | { 167 | "package": "swift-log", 168 | "repositoryURL": "https://github.com/apple/swift-log.git", 169 | "state": { 170 | "branch": null, 171 | "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", 172 | "version": "1.4.2" 173 | } 174 | }, 175 | { 176 | "package": "swift-metrics", 177 | "repositoryURL": "https://github.com/apple/swift-metrics.git", 178 | "state": { 179 | "branch": null, 180 | "revision": "1c1408bf8fc21be93713e897d2badf500ea38419", 181 | "version": "2.3.1" 182 | } 183 | }, 184 | { 185 | "package": "swift-nio", 186 | "repositoryURL": "https://github.com/apple/swift-nio.git", 187 | "state": { 188 | "branch": null, 189 | "revision": "124119f0bb12384cef35aa041d7c3a686108722d", 190 | "version": "2.40.0" 191 | } 192 | }, 193 | { 194 | "package": "swift-nio-extras", 195 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 196 | "state": { 197 | "branch": null, 198 | "revision": "a75e92bde3683241c15df3dd905b7a6dcac4d551", 199 | "version": "1.12.1" 200 | } 201 | }, 202 | { 203 | "package": "swift-nio-http2", 204 | "repositoryURL": "https://github.com/apple/swift-nio-http2.git", 205 | "state": { 206 | "branch": null, 207 | "revision": "108ac15087ea9b79abb6f6742699cf31de0e8772", 208 | "version": "1.22.0" 209 | } 210 | }, 211 | { 212 | "package": "swift-nio-ssl", 213 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 214 | "state": { 215 | "branch": null, 216 | "revision": "42436a25ff32c390465567f5c089a9a8ce8d7baf", 217 | "version": "2.20.0" 218 | } 219 | }, 220 | { 221 | "package": "swift-nio-transport-services", 222 | "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", 223 | "state": { 224 | "branch": null, 225 | "revision": "2cb54f91ddafc90832c5fa247faf5798d0a7c204", 226 | "version": "1.13.0" 227 | } 228 | }, 229 | { 230 | "package": "swift-numerics", 231 | "repositoryURL": "https://github.com/apple/swift-numerics", 232 | "state": { 233 | "branch": null, 234 | "revision": "0a5bc04095a675662cf24757cc0640aa2204253b", 235 | "version": "1.0.2" 236 | } 237 | }, 238 | { 239 | "package": "vapor", 240 | "repositoryURL": "https://github.com/vapor/vapor.git", 241 | "state": { 242 | "branch": null, 243 | "revision": "12e2e7460ab912b65fb7a0fe47e4f638a7d5e642", 244 | "version": "4.62.0" 245 | } 246 | }, 247 | { 248 | "package": "websocket-kit", 249 | "repositoryURL": "https://github.com/vapor/websocket-kit.git", 250 | "state": { 251 | "branch": null, 252 | "revision": "09212f4c2b9ebdef00f04b913b57f5d77bc4ea62", 253 | "version": "2.4.1" 254 | } 255 | } 256 | ] 257 | }, 258 | "version": 1 259 | } 260 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "StreamChatVapor", 6 | platforms: [ 7 | .macOS(.v12) 8 | ], 9 | products: [ 10 | .library(name: "StreamSDKVapor", targets: ["StreamSDKVapor"]), 11 | ], 12 | dependencies: [ 13 | // 💧 A server-side Swift web framework. 14 | .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), 15 | .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), 16 | .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"), 17 | .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"), 18 | .package(url: "https://github.com/vapor-community/Imperial.git", from: "1.0.0"), 19 | ], 20 | targets: [ 21 | .executableTarget( 22 | name: "ExampleApp", 23 | dependencies: [ 24 | .product(name: "Fluent", package: "fluent"), 25 | .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), 26 | .product(name: "Vapor", package: "vapor"), 27 | .product(name: "JWT", package: "jwt"), 28 | "StreamSDKVapor", 29 | .product(name: "ImperialGoogle", package: "Imperial"), 30 | ], 31 | swiftSettings: [ 32 | // Enable better optimizations when building in Release configuration. Despite the use of 33 | // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release 34 | // builds. See for details. 35 | .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) 36 | ] 37 | ), 38 | .testTarget(name: "ExampleAppTests", dependencies: [ 39 | .target(name: "ExampleApp"), 40 | .product(name: "XCTVapor", package: "vapor"), 41 | ]), 42 | .target( 43 | name: "StreamSDKVapor", 44 | dependencies: [ 45 | .product(name: "JWT", package: "jwt"), 46 | .product(name: "Vapor", package: "vapor"), 47 | ] 48 | ), 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /Public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-chat-vapor-swift/10ff8721f882a2714e06e5dd63713d0cf697a929/Public/.gitkeep -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StreamChat Vapor Swift Backend 2 |

3 | 4 |

5 | 6 | This library contains the code to integrate client applications with Stream's backend using [Vapor Swift](https://vapor.codes). It also contains an example application demonstrating how to integrate the library with a Vapor applications. 7 | 8 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FGetStream%2Fstream-chat-vapor-swift%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/GetStream/stream-chat-vapor-swift) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FGetStream%2Fstream-chat-vapor-swift%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/GetStream/stream-chat-vapor-swift) 9 | 10 | ## Integrating the libray 11 | 12 | Add the library as a dependency to your `Package.swift`: 13 | 14 | ```swift 15 | dependencies: [ 16 | // ... 17 | .package(name: "StreamChat", url: "https://github.com/GetStream/stream-chat-vapor-swift.git", from: "0.1.0"), 18 | ], 19 | ``` 20 | 21 | Then add the the dependency to your application, e.g.: 22 | 23 | ```swift 24 | .target( 25 | name: "App", 26 | dependencies: [ 27 | .product(name: "StreamSDKVapor", package: "StreamChat"), 28 | // ... 29 | ], 30 | // ... 31 | ``` 32 | 33 | ## Configuration 34 | 35 | The Stream library requires your Stream access key and access secret to work. Configure the libray as so: 36 | 37 | ```swift 38 | // Get the keys, for example from environment variables 39 | guard let streamAccessKey = Environment.get("STREAM_ACCESS_KEY"), let streamAccessSecret = Environment.get("STREAM_ACCESS_SECRET") else { 40 | app.logger.critical("STREAM keys not set") 41 | fatalError("STREAM keys not set") 42 | } 43 | 44 | let streamConfig = StreamConfiguration(accessKey: streamAccessKey, accessSecret: streamAccessSecret) 45 | app.stream.use(streamConfig) 46 | ``` 47 | 48 | ## Generating a JWT 49 | 50 | To generate a JWT to use with Stream's backend you need the user ID. You can then call the Stream library to generate the token: 51 | 52 | ```swift 53 | let streamToken = try req.stream.createToken(name: userID) 54 | ``` 55 | 56 | You can pass an optional date for the token to expire at as well: 57 | 58 | ```swift 59 | let streamToken = try req.stream.createToken(name: userID, expiresAt: Date().addingTimeInterval(3600)) 60 | ``` 61 | 62 | ## Running the sample application 63 | 64 | To get started you need to do a couple things: 65 | 66 | - Install Docker Desktop. 67 | - Run `./scripts/localDockerDB.swift start` 68 | - open Package.swift 69 | - Set working dir of scheme in Xcode to root of project. 70 | - Run project 71 | 72 | The application allows users to register traditionally by sending a **POST** request to `/auth/register` and also using Sign In With Apple. 73 | 74 | When registering or logging in the Vapor app returns both an API key for interacting with the API as the authenticated user and also a JWT for use with Stream's backend. 75 | 76 | # More example code 77 | 78 | See these supporting repositories to see how integrating this libary would look in your codebase: 79 | 80 | - https://github.com/GetStream/stream-chat-vapor-swift-demo 81 | - https://github.com/GetStream/stream-chat-vapor-swift-demo-ios 82 | 83 | # Blog article 84 | We also have a [blog article describing what is in this repository and how to get started](https://getstream.io/blog/vapor-swift-stream-server/). 85 | 86 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | At Stream we are committed to the security of our Software. We appreciate your efforts in disclosing vulnerabilities responsibly and we will make every effort to acknowledge your contributions. 3 | 4 | Report security vulnerabilities at the following email address: 5 | ``` 6 | [security@getstream.io](mailto:security@getstream.io) 7 | ``` 8 | Alternatively it is also possible to open a new issue in the affected repository, tagging it with the `security` tag. 9 | 10 | A team member will acknowledge the vulnerability and will follow-up with more detailed information. A representative of the security team will be in touch if more information is needed. 11 | 12 | # Information to include in a report 13 | While we appreciate any information that you are willing to provide, please make sure to include the following: 14 | * Which repository is affected 15 | * Which branch, if relevant 16 | * Be as descriptive as possible, the team will replicate the vulnerability before working on a fix. 17 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-chat-vapor-swift/10ff8721f882a2714e06e5dd63713d0cf697a929/Sources/ExampleApp/Controllers/.gitkeep -------------------------------------------------------------------------------- /Sources/ExampleApp/Controllers/AccountController.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import JWT 3 | import StreamSDKVapor 4 | 5 | struct AccountController: RouteCollection { 6 | func boot(routes: RoutesBuilder) throws { 7 | let accountRoutes = routes.grouped(UserToken.authenticator(), User.guardMiddleware()).grouped("account") 8 | accountRoutes.get(use: getMeHandler) 9 | } 10 | 11 | func getMeHandler(_ req: Request) async throws -> User.Public { 12 | let user = try req.auth.require(User.self) 13 | return user.toPublic() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Controllers/AuthController.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import JWT 3 | import Fluent 4 | import ImperialGoogle 5 | 6 | struct AuthController: RouteCollection { 7 | func boot(routes: RoutesBuilder) throws { 8 | let authRoutes = routes.grouped("auth") 9 | authRoutes.post("register", use: registerHandler) 10 | authRoutes.post("siwa", use: signInWithAppleHandler) 11 | 12 | let basicAuthRoutes = authRoutes.grouped(User.authenticator(), User.guardMiddleware()) 13 | basicAuthRoutes.post("login", use: loginHandler) 14 | 15 | if let googleCallbackURL = Environment.get("GOOGLE_CALLBACK_URL") { 16 | try routes.oAuth(from: Google.self, authenticate: "login-google", callback: googleCallbackURL, scope: ["profile", "email"], completion: processGoogleLogin) 17 | routes.get("iOS", "login-google", use: iOSGoogleLogin) 18 | } 19 | } 20 | 21 | func registerHandler(_ req: Request) async throws -> LoginResponse { 22 | try CreateUserData.validate(content: req) 23 | 24 | let data = try req.content.decode(CreateUserData.self) 25 | let passwordHash = try await req.password.async.hash(data.password) 26 | let user = User(name: data.name, email: data.email, username: data.username, passwordHash: passwordHash, siwaID: nil) 27 | do { 28 | try await user.create(on: req.db) 29 | } catch { 30 | if let error = error as? DatabaseError, error.isConstraintFailure { 31 | throw Abort(.badRequest, reason: "A user with that email already exists") 32 | } else { 33 | throw error 34 | } 35 | } 36 | let token = try user.generateToken() 37 | try await token.create(on: req.db) 38 | let streamToken = try req.stream.createToken(id: user.username) 39 | return LoginResponse(apiToken: token, streamToken: streamToken.jwt) 40 | } 41 | 42 | // Uses basic authentication to provide an actual bearer token 43 | func loginHandler(_ req: Request) async throws -> LoginResponse { 44 | let user = try req.auth.require(User.self) 45 | let token = try user.generateToken() 46 | try await token.create(on: req.db) 47 | let streamToken = try req.stream.createToken(id: user.email) 48 | return LoginResponse(apiToken: token, streamToken: streamToken.jwt) 49 | } 50 | 51 | func signInWithAppleHandler(_ req: Request) async throws -> LoginResponse { 52 | let data = try req.content.decode(SignInWithAppleToken.self) 53 | guard let appIdentifier = Environment.get("IOS_APPLICATION_IDENTIFIER") else { 54 | throw Abort(.internalServerError) 55 | } 56 | let siwaToken = try await req.jwt.apple.verify(data.token, applicationIdentifier: appIdentifier) 57 | let user: User 58 | if let foundUser = try await User.query(on: req.db).filter(\.$siwaID == siwaToken.subject.value).first() { 59 | user = foundUser 60 | } else { 61 | guard let email = siwaToken.email, let name = data.name else { 62 | throw Abort(.badRequest) 63 | } 64 | // Set the password t oa secure random value. This won't be run through BCrypt so can't be used to log in anyway 65 | // The username is restricted to certain characters in Stream's backend 66 | let username = data.username ?? email.replacingOccurrences(of: "@", with: "-") 67 | user = User(name: name, email: email, username: username, passwordHash: [UInt8].random(count: 32).base64, siwaID: siwaToken.subject.value) 68 | try await user.create(on: req.db) 69 | } 70 | let token = try user.generateToken() 71 | try await token.create(on: req.db) 72 | let streamToken = try req.stream.createToken(id: user.username) 73 | return LoginResponse(apiToken: token, streamToken: streamToken.jwt) 74 | } 75 | 76 | // MARK: - OAuth 77 | func processGoogleLogin(request: Request, token: String) throws -> EventLoopFuture { 78 | request.eventLoop.performWithTask { 79 | try await processGoogleLoginAsync(request: request, token: token) 80 | } 81 | } 82 | 83 | func processGoogleLoginAsync(request: Request, token: String) async throws -> ResponseEncodable { 84 | let userInfo = try await Google.getUser(on: request) 85 | let foundUser = try await User.query(on: request.db).filter(\.$email == userInfo.email).first() 86 | guard let existingUser = foundUser else { 87 | let username = userInfo.email.replacingOccurrences(of: "@", with: "-") 88 | let user = User(name: userInfo.name, email: userInfo.email, username: username, passwordHash: [UInt8].random(count: 32).base64, siwaID: nil) 89 | try await user.save(on: request.db) 90 | request.session.authenticate(user) 91 | return try await generateRedirect(on: request, for: user) 92 | } 93 | request.session.authenticate(existingUser) 94 | return try await generateRedirect(on: request, for: existingUser) 95 | } 96 | 97 | func iOSGoogleLogin(_ req: Request) -> Response { 98 | req.session.data["oauth_login"] = "iOS" 99 | return req.redirect(to: "/login-google") 100 | } 101 | 102 | func generateRedirect(on req: Request, for user: User) async throws -> ResponseEncodable { 103 | let redirectURL: String 104 | if req.session.data["oauth_login"] == "iOS" { 105 | let token = try user.generateToken() 106 | try await token.save(on: req.db) 107 | guard let appURL = Environment.get("APP_REDIRECT_URL") else { 108 | req.logger.warning("APP_REDIRECT_URL not set") 109 | throw Abort(.internalServerError) 110 | } 111 | redirectURL = "\(appURL)://auth?token=\(token.value)" 112 | } else { 113 | redirectURL = "/" 114 | } 115 | req.session.data["oauth_login"] = nil 116 | return req.redirect(to: redirectURL) 117 | } 118 | } 119 | 120 | struct GoogleUserInfo: Content { 121 | let email: String 122 | let name: String 123 | } 124 | 125 | extension Google { 126 | static func getUser(on request: Request) async throws -> GoogleUserInfo { 127 | var headers = HTTPHeaders() 128 | headers.bearerAuthorization = try BearerAuthorization(token: request.accessToken()) 129 | 130 | let googleAPIURL: URI = "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" 131 | let response = try await request.client.get(googleAPIURL, headers: headers) 132 | guard response.status == .ok else { 133 | if response.status == .unauthorized { 134 | throw Abort.redirect(to: "/login-google") 135 | } else { 136 | throw Abort(.internalServerError) 137 | } 138 | } 139 | return try response.content.decode(GoogleUserInfo.self) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Migrations/0001-CreateUser.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | 3 | extension User { 4 | struct CreateUser: AsyncMigration { 5 | func prepare(on database: Database) async throws { 6 | try await database.schema(v20220314.schemaName) 7 | .id() 8 | .field(v20220314.name, .string, .required) 9 | .field(v20220314.email, .string, .required) 10 | .field(v20220314.passwordHash, .string, .required) 11 | .field(v20220314.siwaID, .string) 12 | .unique(on: v20220314.username) 13 | .unique(on: v20220314.email) 14 | .create() 15 | } 16 | 17 | func revert(on database: Database) async throws { 18 | try await database.schema(v20220314.schemaName).delete() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Migrations/0002-CreateUserToken.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | 3 | extension UserToken { 4 | struct CreaetUserToken: AsyncMigration { 5 | func prepare(on database: Database) async throws { 6 | try await database.schema(v20220314.schemaName) 7 | .id() 8 | .field(v20220314.value, .string, .required) 9 | .field(v20220314.userID, .uuid, .required, .references(User.v20220314.schemaName, User.v20220314.id)) 10 | .field(v20220314.expiration, .datetime, .required) 11 | .unique(on: v20220314.value) 12 | .create() 13 | } 14 | 15 | func revert(on database: Database) async throws { 16 | try await database.schema(v20220314.schemaName).delete() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Migrations/migrations.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import Vapor 3 | 4 | func migrations(_ app: Application) throws { 5 | app.migrations.add(User.CreateUser()) 6 | app.migrations.add(UserToken.CreaetUserToken()) 7 | 8 | try app.autoMigrate().wait() 9 | } 10 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Models/API/Request/CreateUserData.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | struct CreateUserData: Content, Validatable { 4 | var name: String 5 | var email: String 6 | var password: String 7 | var username: String 8 | 9 | /// Ensures a valid email is entered as well as a password of a given length (8 currently) 10 | static func validations(_ validations: inout Validations) { 11 | validations.add("email", as: String.self, is: .email) 12 | validations.add("password", as: String.self, is: .count(8...)) 13 | validations.add("name", as: String.self, is: .count(1...)) 14 | validations.add("username", as: String.self, is: .alphanumeric && .count(3...)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Models/API/Request/SignInWithAppleToken.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | struct SignInWithAppleToken: Content { 4 | let token: String 5 | let name: String? 6 | let username: String? 7 | } 8 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Models/API/Response/LoginResponse.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | struct LoginResponse: Content { 4 | let apiToken: UserToken 5 | let streamToken: String 6 | } 7 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Models/API/Response/User+Public.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | extension User { 4 | struct Public: Content { 5 | let id: UUID? 6 | let name: String 7 | let email: String 8 | } 9 | 10 | func toPublic() -> User.Public { 11 | User.Public(id: self.id, name: self.name, email: self.email) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Models/Fluent/App+FieldKeys.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | 3 | extension User { 4 | enum v20220314 { 5 | static let schemaName = "users" 6 | static let id = FieldKey(stringLiteral: "id") 7 | static let name = FieldKey(stringLiteral: "name") 8 | static let siwaID = FieldKey(stringLiteral: "sign_in_with_apple_id") 9 | static let email = FieldKey(stringLiteral: "email") 10 | static let username = FieldKey(stringLiteral: "username") 11 | static let passwordHash = FieldKey(stringLiteral: "password_hash") 12 | } 13 | } 14 | 15 | extension UserToken { 16 | enum v20220314 { 17 | static let schemaName = "user_tokens" 18 | static let id = FieldKey(stringLiteral: "id") 19 | static let value = FieldKey(stringLiteral: "value") 20 | static let userID = FieldKey(stringLiteral: "user_id") 21 | static let expiration = FieldKey(stringLiteral: "expiration") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Models/Fluent/User.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import Fluent 3 | 4 | final class User: Model, Content { 5 | static let schema = v20220314.schemaName 6 | 7 | @ID 8 | var id: UUID? 9 | 10 | @Field(key: v20220314.name) 11 | var name: String 12 | 13 | @Field(key: v20220314.email) 14 | var email: String 15 | 16 | @Field(key: v20220314.email) 17 | var username: String 18 | 19 | @Field(key: v20220314.passwordHash) 20 | var passwordHash: String 21 | 22 | @Field(key: v20220314.siwaID) 23 | var siwaID: String? 24 | 25 | init() { } 26 | 27 | init(id: UUID? = nil, name: String, email: String, username: String, passwordHash: String, siwaID: String?) { 28 | self.id = id 29 | self.name = name 30 | self.email = email 31 | self.username = username 32 | self.passwordHash = passwordHash 33 | self.siwaID = siwaID 34 | } 35 | } 36 | 37 | extension User: ModelAuthenticatable { 38 | static let passwordHashKey = \User.$passwordHash 39 | static var usernameKey = \User.$email 40 | 41 | func verify(password: String) throws -> Bool { 42 | try Bcrypt.verify(password, created: self.passwordHash) 43 | } 44 | } 45 | 46 | extension User: ModelSessionAuthenticatable {} 47 | 48 | extension User { 49 | func generateToken() throws -> UserToken { 50 | try UserToken( 51 | value: [UInt8].random(count: 16).base64, 52 | userID: self.requireID() 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/ExampleApp/Models/Fluent/UserToken.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import Vapor 3 | 4 | final class UserToken: Model, Content { 5 | static let schema = v20220314.schemaName 6 | 7 | @ID 8 | var id: UUID? 9 | 10 | @Field(key: v20220314.value) 11 | var value: String 12 | 13 | @Parent(key: v20220314.userID) 14 | var user: User 15 | 16 | @Field(key: v20220314.expiration) 17 | var expiration: Date 18 | 19 | init() { } 20 | 21 | init(id: UUID? = nil, value: String, userID: User.IDValue, expiration: Date? = nil) { 22 | self.id = id 23 | self.value = value 24 | self.$user.id = userID 25 | if let expiration = expiration { 26 | self.expiration = expiration 27 | } else { 28 | // Expiration is 24 hours by default 29 | self.expiration = Date(timeIntervalSinceNow: 3600 * 24) 30 | } 31 | } 32 | } 33 | 34 | extension UserToken: ModelTokenAuthenticatable { 35 | static let valueKey = \UserToken.$value 36 | static let userKey = \UserToken.$user 37 | 38 | var isValid: Bool { 39 | expiration > Date() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/ExampleApp/app.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | @main 4 | struct ExampleApp { 5 | static func main() async throws { 6 | var env = try Environment.detect() 7 | try LoggingSystem.bootstrap(from: &env) 8 | let app = Application(env) 9 | defer { app.shutdown() } 10 | try configure(app) 11 | try app.run() 12 | } 13 | } -------------------------------------------------------------------------------- /Sources/ExampleApp/configure.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import FluentPostgresDriver 3 | import Vapor 4 | import StreamSDKVapor 5 | 6 | // configures your application 7 | public func configure(_ app: Application) throws { 8 | app.databases.use(.postgres( 9 | hostname: Environment.get("DATABASE_HOST") ?? "localhost", 10 | port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? PostgresConfiguration.ianaPortNumber, 11 | username: Environment.get("DATABASE_USERNAME") ?? "vapor_username", 12 | password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password", 13 | database: Environment.get("DATABASE_NAME") ?? "vapor_database" 14 | ), as: .psql) 15 | 16 | // Run migrations 17 | try migrations(app) 18 | 19 | // register routes 20 | try routes(app) 21 | 22 | guard let streamAccessKey = Environment.get("STREAM_ACCESS_KEY"), let streamAccessSecret = Environment.get("STREAM_ACCESS_SECRET") else { 23 | app.logger.critical("STREAM keys not set") 24 | fatalError("STREAM keys not set") 25 | } 26 | 27 | let streamConfig = StreamConfiguration(accessKey: streamAccessKey, accessSecret: streamAccessSecret) 28 | app.stream.use(streamConfig) 29 | 30 | app.middleware.use(app.sessions.middleware) 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ExampleApp/routes.swift: -------------------------------------------------------------------------------- 1 | import Fluent 2 | import Vapor 3 | 4 | func routes(_ app: Application) throws { 5 | app.get("hc") { req in 6 | "OK" 7 | } 8 | 9 | try app.register(collection: AuthController()) 10 | try app.register(collection: AccountController()) 11 | } 12 | -------------------------------------------------------------------------------- /Sources/StreamSDKVapor/Application+Stream.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | extension Application { 4 | 5 | /// The `StreamWrapper` to allow the Stream SDK to be integrated with Vapor 6 | public var stream: StreamWrapper { 7 | .init(application: self) 8 | } 9 | 10 | 11 | /// A wrapper class containing everything needed to make integrating with Vapor seamless 12 | public struct StreamWrapper { 13 | /// The `StreamClient` used for generating JWTs etc 14 | public var client: StreamClient { 15 | guard let makeClient = self.storage.makeClient else { 16 | fatalError("No stream configured. Configure with app.clients.use(...)") 17 | } 18 | return makeClient(self.application) 19 | } 20 | 21 | final class Storage { 22 | var makeClient: ((Application) -> StreamClient)? 23 | init() { } 24 | } 25 | 26 | struct Key: StorageKey { 27 | typealias Value = Storage 28 | } 29 | 30 | func initialize() { 31 | self.application.storage[Key.self] = .init() 32 | } 33 | 34 | 35 | /// Tell Vapor what configuration to use for the `StreamClient` 36 | /// - Parameter config: the config to use 37 | public func use(_ config: StreamConfiguration) { 38 | self.storage.makeClient = { app in 39 | StreamClient(accessKey: config.accessKey, accessSecret: config.accessSecret, client: app.client, eventLoop: app.eventLoopGroup.next()) 40 | } 41 | } 42 | 43 | /// The central Vapor Application 44 | let application: Application 45 | 46 | var storage: Storage { 47 | guard let storage = self.application.storage[Key.self] else { 48 | let storage = Storage() 49 | self.application.storage[Key.self] = storage 50 | return storage 51 | } 52 | return storage 53 | } 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Sources/StreamSDKVapor/Request+Stream.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | extension Request { 4 | /// A `StreamClient` to use from the `Request` 5 | public var stream: StreamClient { 6 | self.application.stream.client.for(eventLoop: self.eventLoop, client: self.client) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/StreamSDKVapor/StreamClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JWTKit 3 | import Vapor 4 | 5 | 6 | /// A client for interacting with Stream and their services 7 | public struct StreamClient { 8 | let accessKey: String 9 | let accessSecret: String 10 | let client: Client 11 | let eventLoop: EventLoop 12 | 13 | /// Initializes a new `StreamClient` 'for' the given `EventLoop` and `Client`. 14 | /// - Parameters: 15 | /// - eventLoop: <#eventLoop description#> 16 | /// - client: A Vapor `Client` object. 17 | /// - Returns: An initialized StreamClient capable of connecting to the Stream backend to handle all Stream related network calls. 18 | func `for`(eventLoop: EventLoop, client: Client) -> StreamClient { 19 | StreamClient(accessKey: self.accessKey, accessSecret: self.accessSecret, client: client, eventLoop: eventLoop) 20 | } 21 | 22 | 23 | /// Description 24 | /// - Parameters: 25 | /// - accessKey: Access key to be obtained from the Stream dashboard 26 | /// - accessSecret: Access secret to be obtained from the Stream dashboard 27 | /// - client: A Vapor `Client` object. 28 | init(accessKey: String, accessSecret: String, client: Client, eventLoop: EventLoop) { 29 | self.accessKey = accessKey 30 | self.accessSecret = accessSecret 31 | self.client = client 32 | self.eventLoop = eventLoop 33 | } 34 | 35 | 36 | /// Create a JWT to use with client SDKs to interact with Stream 37 | /// - Parameters: 38 | /// - id: The ID of the user for the token 39 | /// - expiresAt: An optional date the token should expire at. Defaults to no expiry date 40 | /// - Returns: A `StreamToken` containing the JWT to provide to client SDKs 41 | public func createToken(id: String, expiresAt: Date? = nil) throws -> StreamToken { 42 | let signer = JWTSigner.hs256(key: accessSecret) 43 | let expiration: ExpirationClaim? 44 | if let expiresAt = expiresAt { 45 | expiration = .init(value: expiresAt) 46 | } else { 47 | expiration = nil 48 | } 49 | let payload = StreamPayload( 50 | expiration: expiration, 51 | userID: id 52 | ) 53 | let jwt = try signer.sign(payload) 54 | return StreamToken(jwt: jwt) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/StreamSDKVapor/StreamConfiguration.swift: -------------------------------------------------------------------------------- 1 | /// A `StreamConfiguration` contains the details necessary to configure the Stream SDK for your Vapor app 2 | public struct StreamConfiguration { 3 | let accessKey: String 4 | let accessSecret: String 5 | 6 | /// Create an instance of `StreamConfiguration` 7 | /// - Parameters: 8 | /// - accessKey: Your Stream App Access Key 9 | /// - accessSecret: Your Stream App Access Secret 10 | public init(accessKey: String, accessSecret: String) { 11 | self.accessKey = accessKey 12 | self.accessSecret = accessSecret 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/StreamSDKVapor/StreamJWTPayload.swift: -------------------------------------------------------------------------------- 1 | import JWT 2 | import Foundation 3 | 4 | struct StreamPayload: JWTPayload { 5 | enum CodingKeys: String, CodingKey { 6 | case expiration = "exp" 7 | case userID = "user_id" 8 | } 9 | 10 | /// The "exp" (expiration time) claim identifies the expiration time on 11 | /// or after which the JWT MUST NOT be accepted for processing. 12 | var expiration: ExpirationClaim? 13 | 14 | /// Custom data. 15 | /// If true, the user is an admin. 16 | var userID: String 17 | 18 | func verify(using signer: JWTSigner) throws { 19 | if let expiration = self.expiration { 20 | return try expiration.verifyNotExpired() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/StreamSDKVapor/StreamToken.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type to hold a JWT to use with Stream SDKs 4 | public struct StreamToken: Codable { 5 | /// The JWT to use with Stream SDKs 6 | public let jwt: String 7 | } 8 | -------------------------------------------------------------------------------- /Tests/ExampleAppTests/ExampleAppTests.swift: -------------------------------------------------------------------------------- 1 | @testable import ExampleApp 2 | import XCTVapor 3 | 4 | final class ExampleAppTests: 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", afterResponse: { 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 run 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: stream-chat:latest 32 | build: 33 | context: . 34 | environment: 35 | <<: *shared_environment 36 | depends_on: 37 | - db 38 | ports: 39 | - '8080:8080' 40 | # user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user. 41 | command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] 42 | migrate: 43 | image: stream-chat:latest 44 | build: 45 | context: . 46 | environment: 47 | <<: *shared_environment 48 | depends_on: 49 | - db 50 | command: ["migrate", "--yes"] 51 | deploy: 52 | replicas: 0 53 | revert: 54 | image: stream-chat:latest 55 | build: 56 | context: . 57 | environment: 58 | <<: *shared_environment 59 | depends_on: 60 | - db 61 | command: ["migrate", "--revert", "--yes"] 62 | deploy: 63 | replicas: 0 64 | db: 65 | image: postgres:14-alpine 66 | volumes: 67 | - db_data:/var/lib/postgresql/data/pgdata 68 | environment: 69 | PGDATA: /var/lib/postgresql/data/pgdata 70 | POSTGRES_USER: vapor_username 71 | POSTGRES_PASSWORD: vapor_password 72 | POSTGRES_DB: vapor_database 73 | ports: 74 | - '5432:5432' 75 | -------------------------------------------------------------------------------- /scripts/localDockerDB.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/swift 2 | import Foundation 3 | 4 | let databaseName = "vapor_database" 5 | let username = "vapor_username" 6 | let password = "vapor_password" 7 | let port = 5432 8 | let containerName = "stream-postgres" 9 | 10 | @discardableResult 11 | func shell(_ args: String...) -> Int32 { 12 | let task = Process() 13 | task.launchPath = "/usr/bin/env" 14 | task.arguments = args 15 | task.launch() 16 | task.waitUntilExit() 17 | 18 | return task.terminationStatus 19 | } 20 | 21 | let args = CommandLine.arguments 22 | 23 | if args.contains("start") { 24 | start() 25 | } else if args.contains("stop") { 26 | stop() 27 | } else if args.contains("reset") { 28 | reset() 29 | } else { 30 | print("Pass an argument, one of: [start, stop, reset]") 31 | } 32 | 33 | func start() { 34 | print("Starting local database in container \(containerName). \nDatabase name is \(databaseName), username is \(username) and password is \(password)") 35 | 36 | let dockerResult = shell("docker", "start", containerName) 37 | 38 | guard dockerResult == 0 else { 39 | print("Starting the Database failed, attempting to create one in Docker 🐳") 40 | create() 41 | exit(1) 42 | } 43 | 44 | print("Database started in Docker 🐳") 45 | } 46 | 47 | func create() { 48 | let dockerResult = shell("docker", "run", "--name", containerName, "-e", "POSTGRES_DB=\(databaseName)", "-e", "POSTGRES_USER=\(username)" , "-e", "POSTGRES_PASSWORD=\(password)", "-p", "\(port):5432", "-d", "postgres") 49 | 50 | guard dockerResult == 0 else { 51 | print("❌ ERROR: Failed to create the database") 52 | exit(1) 53 | } 54 | 55 | print("Database created in Docker 🐳") 56 | } 57 | 58 | func stop() { 59 | let containerName = "stream-postgres" 60 | print("Stopping existing database in container \(containerName)") 61 | 62 | let stopResult = shell("docker", "stop", containerName) 63 | 64 | guard stopResult == 0 else { 65 | print("❌ ERROR: Failed to stop the database") 66 | exit(1) 67 | } 68 | 69 | print("Database stopped in Docker 🐳") 70 | } 71 | 72 | func reset() { 73 | shell("docker", "stop", containerName) 74 | let removeResult = shell("docker", "rm", containerName) 75 | 76 | guard removeResult == 0 else { 77 | print("❌ ERROR: Failed to remove the database container") 78 | exit(1) 79 | } 80 | 81 | print("Database destroyed in Docker 🐳") 82 | } 83 | -------------------------------------------------------------------------------- /vapor.paw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/stream-chat-vapor-swift/10ff8721f882a2714e06e5dd63713d0cf697a929/vapor.paw --------------------------------------------------------------------------------