diff --git a/.env.testing b/.env.testing new file mode 100644 index 0000000..10bf805 --- /dev/null +++ b/.env.testing @@ -0,0 +1,9 @@ +# Please don't molest this sample firebase project. I'm exposing these credentials for your convenience, and I +# will disable the project it it's causing me a headache. You should create your own +# firebase project (it's free) and generate your own credentials. +FIREBASE_PROJECT_ID=vapor-saas-backend-template +TEST_FIREBASE_WEB_API_KEY=AIzaSyAesbeFK6uCEvYZNCJZN0bs-Ab9Ya01Egg +TEST_FIREBASE_USER_EMAIL=saastemplatetestuser1@indiepitcher.com +TEST_FIREBASE_USER_PASSWORD=testtest +TEST_FIREBASE_USER_2_EMAIL=saastemplatetestuser2@indiepitcher.com +TEST_FIREBASE_USER_2_PASSWORD=testtest \ No newline at end of file diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index d863861..a245db2 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -35,11 +35,4 @@ jobs: - name: Build run: swift build -v - name: Run tests - env: - TEST_FIREBASE_WEB_API_KEY: ${{ secrets.TEST_FIREBASE_WEB_API_KEY }} - TEST_FIREBASE_USER_EMAIL: ${{ secrets.TEST_FIREBASE_USER_EMAIL }} - TEST_FIREBASE_USER_PASSWORD: ${{ secrets.TEST_FIREBASE_USER_PASSWORD }} - TEST_FIREBASE_USER_2_EMAIL: ${{ secrets.TEST_FIREBASE_USER_2_EMAIL }} - TEST_FIREBASE_USER_2_PASSWORD: ${{ secrets.TEST_FIREBASE_USER_2_PASSWORD }} - FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }} run: swift test -v diff --git a/.gitignore b/.gitignore index 42aade1..6848618 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ DerivedData/ db.sqlite .swiftpm .env -/.env.testing diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..51db687 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + "configurations": [ + { + "type": "lldb", + "request": "launch", + "sourceLanguages": [ + "swift" + ], + "args": [], + "cwd": "${workspaceFolder:vapor-saas-backend-template}", + "name": "Debug App", + "program": "${workspaceFolder:vapor-saas-backend-template}/.build/debug/App", + "preLaunchTask": "swift: Build Debug App" + }, + { + "type": "lldb", + "request": "launch", + "sourceLanguages": [ + "swift" + ], + "args": [], + "cwd": "${workspaceFolder:vapor-saas-backend-template}", + "name": "Release App", + "program": "${workspaceFolder:vapor-saas-backend-template}/.build/release/App", + "preLaunchTask": "swift: Build Release App" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fb7cd16..88fc547 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,8 @@ FROM swift:5.9-jammy as build # Install OS updates RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get -q update \ - && apt-get -q dist-upgrade -y\ - && rm -rf /var/lib/apt/lists/* + && apt-get -q dist-upgrade -y \ + && apt-get install -y libjemalloc-dev # Set up a build area WORKDIR /build @@ -23,11 +23,11 @@ RUN swift package resolve --skip-update \ # Copy entire repo into container COPY . . -# Build everything, with optimizations -RUN swift build -c release --static-swift-stdlib \ - # Workaround for https://github.com/apple/swift/pull/68669 - # This can be removed as soon as 5.9.1 is released, but is harmless if left in. - -Xlinker -u -Xlinker _swift_backtrace_isThunkFunction +# Build everything, with optimizations, with static linking, and using jemalloc +# N.B.: The static version of jemalloc is incompatible with the static Swift runtime. +RUN swift build -c release \ + --static-swift-stdlib \ + -Xlinker -ljemalloc # Switch to the staging area WORKDIR /staging @@ -35,6 +35,9 @@ WORKDIR /staging # Copy main executable to staging area RUN cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ +# Copy static swift backtracer binary to staging area +RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ + # Copy resources bundled by SPM to staging area RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; @@ -46,13 +49,14 @@ RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w # ================================ # Run image # ================================ -FROM swift:5.9-jammy-slim +FROM ubuntu:jammy # Make sure all system packages are up to date, and install only essential packages. RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get -q update \ && apt-get -q dist-upgrade -y \ && apt-get -q install -y \ + libjemalloc2 \ ca-certificates \ tzdata \ # If your app or its dependencies import FoundationNetworking, also install `libcurl4`. @@ -71,7 +75,7 @@ WORKDIR /app COPY --from=build --chown=vapor:vapor /staging /app # Provide configuration needed by the built-in crash reporter and some sensible default behaviors. -ENV SWIFT_ROOT=/usr SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no +ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static # Ensure all further commands run as the vapor user USER vapor:vapor @@ -81,4 +85,4 @@ EXPOSE 8080 # Start the Vapor service when the image is run, default to listening on 8080 in production environment ENTRYPOINT ["./App"] -CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] +CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..83159d8 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,338 @@ +{ + "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "291438696abdd48d2a83b52465c176efbd94512b", + "version" : "1.20.1" + } + }, + { + "identity" : "async-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/async-kit.git", + "state" : { + "revision" : "7ece208cd401687641c88367a00e3ea2b04311f1", + "version" : "1.19.0" + } + }, + { + "identity" : "console-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/console-kit.git", + "state" : { + "revision" : "a31f44ebfbd15a2cc0fda705279676773ac16355", + "version" : "4.14.1" + } + }, + { + "identity" : "cwlcatchexception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlCatchException.git", + "state" : { + "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", + "version" : "2.1.2" + } + }, + { + "identity" : "cwlpreconditiontesting", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state" : { + "revision" : "dc9af4781f2afdd1e68e90f80b8603be73ea7abc", + "version" : "2.2.0" + } + }, + { + "identity" : "fluent", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent.git", + "state" : { + "revision" : "a586a5d4164f23a0ee4e02e1f467b9bbef0c9f1c", + "version" : "4.9.0" + } + }, + { + "identity" : "fluent-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-kit.git", + "state" : { + "revision" : "6cef8533c9ab87865de58fa3c6e6317e3e09857a", + "version" : "1.45.1" + } + }, + { + "identity" : "fluent-postgres-driver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-postgres-driver.git", + "state" : { + "revision" : "a538fc647f82d915eb84e0a12ca9b08c513e57c4", + "version" : "2.8.0" + } + }, + { + "identity" : "jwt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt.git", + "state" : { + "revision" : "d65f32bfd08fae5910d603028be5dec4f35b3482", + "version" : "4.2.2" + } + }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit.git", + "state" : { + "revision" : "cd0fe3af36764e876182137c3132a6d8459e1867", + "version" : "4.13.1" + } + }, + { + "identity" : "mixpanelvapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/petrpavlik/MixpanelVapor.git", + "state" : { + "revision" : "1413f86c8cd28fe846b0f5819316ea00d35816fd", + "version" : "0.3.0" + } + }, + { + "identity" : "multipart-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/multipart-kit.git", + "state" : { + "revision" : "12ee56f25bd3fc4c2d09c2aa16e69de61dc786e8", + "version" : "4.6.0" + } + }, + { + "identity" : "nimble", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Nimble.git", + "state" : { + "revision" : "c1f3dd66222d5e7a1a20afc237f7e7bc432c564f", + "version" : "13.2.0" + } + }, + { + "identity" : "postgres-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/postgres-kit.git", + "state" : { + "revision" : "80ab7737dac4fccd4a8ad38743828dcb71ba7ac8", + "version" : "2.12.2" + } + }, + { + "identity" : "postgres-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/postgres-nio.git", + "state" : { + "revision" : "fa3137d39bca84843739db1c5a3db2d7f4ae65e6", + "version" : "1.20.0" + } + }, + { + "identity" : "routing-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/routing-kit.git", + "state" : { + "revision" : "2a92a7eac411a82fb3a03731be5e76773ebe1b3e", + "version" : "4.9.0" + } + }, + { + "identity" : "smtpkitten", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Joannis/SMTPKitten.git", + "state" : { + "revision" : "84699b87957068ca2eb69ecd5c2ed7f3ff4a9604", + "version" : "0.1.9" + } + }, + { + "identity" : "sql-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/sql-kit.git", + "state" : { + "revision" : "b2f128cb62a3abfbb1e3b2893ff3ee69e70f4f0f", + "version" : "3.28.0" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "cc76b894169a3c86b71bac10c78a4db6beb7a9ad", + "version" : "3.2.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "12358d55a3824bd5fed310b999ea8cf83a9a1a65", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" + } + }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "971ba26378ab69c43737ee7ba967a896cb74c0d1", + "version" : "2.4.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", + "version" : "2.62.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", + "version" : "1.20.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806", + "version" : "1.29.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", + "version" : "2.25.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", + "version" : "1.20.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-sentry", + "kind" : "remoteSourceControl", + "location" : "https://github.com/petrpavlik/swift-sentry.git", + "state" : { + "branch" : "main", + "revision" : "2bc05a18f20fcf124520f078b57a58e6c6f077c5" + } + }, + { + "identity" : "uaparserswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/malcommac/UAParserSwift.git", + "state" : { + "revision" : "a8057c7cff60db2cdf31d0a0a68ce430754ab64e", + "version" : "1.2.1" + } + }, + { + "identity" : "vapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/vapor.git", + "state" : { + "revision" : "0680f9f6bfab7100cd585b3186740ee7860c983e", + "version" : "4.91.1" + } + }, + { + "identity" : "vapor-firebase-jwt-middleware", + "kind" : "remoteSourceControl", + "location" : "https://github.com/emvakar/vapor-firebase-jwt-middleware.git", + "state" : { + "branch" : "master", + "revision" : "9dfe7a9ea9a2ccdd41a0eaab0c7bea3c54f24e45" + } + }, + { + "identity" : "vaporsmtpkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Joannis/VaporSMTPKit.git", + "state" : { + "revision" : "0260448395c5c8706abaad1f77d5501126497429", + "version" : "1.0.4" + } + }, + { + "identity" : "websocket-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/websocket-kit.git", + "state" : { + "revision" : "53fe0639a98903858d0196b699720decb42aee7b", + "version" : "2.14.0" + } + } + ], + "version" : 2 +} diff --git a/Sources/App/Auth/FirebaseAuth.swift b/Sources/App/Auth/FirebaseAuth.swift new file mode 100644 index 0000000..1c8022a --- /dev/null +++ b/Sources/App/Auth/FirebaseAuth.swift @@ -0,0 +1,27 @@ +// +// File.swift +// +// +// Created by Petr Pavlik on 21.01.2024. +// + +import Vapor +import FirebaseJWTMiddleware + +protocol JWTUser { + var userID: String { get } + var email: String? { get } +} + +extension FirebaseJWTPayload : JWTUser { + +} + +extension Request { + var jwtUser: some JWTUser { + get async throws { + // This is where you can swap Firebase Auth for another JWT-based provider (Amazon Congnito, Clerk, ...) + try await self.firebaseJwt.asyncVerify() + } + } +} diff --git a/Sources/App/Controllers/ProfileController.swift b/Sources/App/Controllers/ProfileController.swift index 96a3d33..84a0c8e 100644 --- a/Sources/App/Controllers/ProfileController.swift +++ b/Sources/App/Controllers/ProfileController.swift @@ -1,12 +1,11 @@ import Fluent import Vapor -import FirebaseJWTMiddleware import MixpanelVapor extension Request { var profile: Profile { get async throws { - let token = try await self.firebaseJwt.asyncVerify() + let token = try await self.jwtUser if let profile = try await Profile.query(on: self.db).filter(\.$firebaseUserId == token.userID).first() { return profile } else { @@ -29,19 +28,11 @@ struct ProfileLiteDTO: Content { extension Profile { func toDTO() throws -> ProfileDTO { - guard let id else { - throw Abort(.internalServerError, reason: "missing profile id") - } - - return .init(id: id, email: email, isSubscribedToNewsletter: subscribedToNewsletterAt != nil) + .init(id: try requireID(), email: email, isSubscribedToNewsletter: subscribedToNewsletterAt != nil) } func toLiteDTO() throws -> ProfileLiteDTO { - guard let id else { - throw Abort(.internalServerError, reason: "missing profile id") - } - - return .init(id: id, email: email) + .init(id: try requireID(), email: email) } } @@ -59,7 +50,7 @@ struct ProfileController: RouteCollection { } func create(req: Request) async throws -> ProfileDTO { - let token = try await req.firebaseJwt.asyncVerify() + let token = try await req.jwtUser if let profile = try await Profile.query(on: req.db).filter(\.$firebaseUserId == token.userID).first() { guard let email = token.email else { diff --git a/Sources/App/entrypoint.swift b/Sources/App/entrypoint.swift index a834d20..28fad2c 100644 --- a/Sources/App/entrypoint.swift +++ b/Sources/App/entrypoint.swift @@ -1,26 +1,7 @@ import Vapor -import Dispatch import Logging import SwiftSentry -/// This extension is temporary and can be removed once Vapor gets this support. -private extension Vapor.Application { - static let baseExecutionQueue = DispatchQueue(label: "vapor.codes.entrypoint") - - func runFromAsyncMainEntrypoint() async throws { - try await withCheckedThrowingContinuation { continuation in - Vapor.Application.baseExecutionQueue.async { [self] in - do { - try self.run() - continuation.resume() - } catch { - continuation.resume(throwing: error) - } - } - } - } -} - @main enum Entrypoint { static func main() async throws { @@ -55,6 +36,6 @@ enum Entrypoint { app.logger.report(error: error) throw error } - try await app.runFromAsyncMainEntrypoint() + try await app.execute() } }