From 97fcea2469b7b6aff5ca8f7e55bbc0f5f21c6b73 Mon Sep 17 00:00:00 2001 From: Jared Henderson Date: Tue, 6 Feb 2024 15:19:49 -0500 Subject: [PATCH 1/3] api: add optional gclid to admin model --- api/Sources/Api/Configure/migrations.swift | 1 + .../Migrations/019_AddAdminGclid.swift | 23 +++++++++++++++++++ api/Sources/Api/Models/Admin/Admin.swift | 5 +++- api/Sources/Api/Models/Models+Duet.swift | 1 + api/Sources/Api/Models/Models+DuetSQL.swift | 3 +++ 5 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 api/Sources/Api/Database/Migrations/019_AddAdminGclid.swift diff --git a/api/Sources/Api/Configure/migrations.swift b/api/Sources/Api/Configure/migrations.swift index b3908cea..6a8ef764 100644 --- a/api/Sources/Api/Configure/migrations.swift +++ b/api/Sources/Api/Configure/migrations.swift @@ -22,5 +22,6 @@ extension Configure { app.migrations.add(ReworkPayments()) app.migrations.add(AddUserShowSuspensionActivity()) app.migrations.add(EliminateNetworkDecisionsTable()) + app.migrations.add(AddAdminGclid()) } } diff --git a/api/Sources/Api/Database/Migrations/019_AddAdminGclid.swift b/api/Sources/Api/Database/Migrations/019_AddAdminGclid.swift new file mode 100644 index 00000000..db454e21 --- /dev/null +++ b/api/Sources/Api/Database/Migrations/019_AddAdminGclid.swift @@ -0,0 +1,23 @@ +import FluentSQL +import Gertie + +struct AddAdminGclid: GertieMigration { + func up(sql: SQLDatabase) async throws { + try await sql.addColumn( + Admin.M19.gclid, + on: Admin.M1.self, + type: .varchar(128), + nullable: true + ) + } + + func down(sql: SQLDatabase) async throws { + try await sql.dropColumn(Admin.M19.gclid, on: Admin.M1.self) + } +} + +extension Admin { + enum M19 { + static let gclid = FieldKey("gclid") + } +} diff --git a/api/Sources/Api/Models/Admin/Admin.swift b/api/Sources/Api/Models/Admin/Admin.swift index e3c198c9..05408618 100644 --- a/api/Sources/Api/Models/Admin/Admin.swift +++ b/api/Sources/Api/Models/Admin/Admin.swift @@ -8,6 +8,7 @@ final class Admin: Codable { var subscriptionId: SubscriptionId? var subscriptionStatus: SubscriptionStatus var subscriptionStatusExpiration: Date + var gclid: String? var createdAt = Date() var updatedAt = Date() var deletedAt: Date? @@ -25,7 +26,8 @@ final class Admin: Codable { password: String, subscriptionStatus: SubscriptionStatus = .pendingEmailVerification, subscriptionStatusExpiration: Date = Date().advanced(by: .days(7)), - subscriptionId: SubscriptionId? = nil + subscriptionId: SubscriptionId? = nil, + gclid: String? = nil ) { self.id = id self.email = email @@ -33,6 +35,7 @@ final class Admin: Codable { self.subscriptionId = subscriptionId self.subscriptionStatus = subscriptionStatus self.subscriptionStatusExpiration = subscriptionStatusExpiration + self.gclid = gclid } } diff --git a/api/Sources/Api/Models/Models+Duet.swift b/api/Sources/Api/Models/Models+Duet.swift index 126e8bab..9f963024 100644 --- a/api/Sources/Api/Models/Models+Duet.swift +++ b/api/Sources/Api/Models/Models+Duet.swift @@ -110,6 +110,7 @@ extension Admin { case subscriptionId case subscriptionStatus case subscriptionStatusExpiration + case gclid case createdAt case updatedAt } diff --git a/api/Sources/Api/Models/Models+DuetSQL.swift b/api/Sources/Api/Models/Models+DuetSQL.swift index 034377aa..5c5fe8d9 100644 --- a/api/Sources/Api/Models/Models+DuetSQL.swift +++ b/api/Sources/Api/Models/Models+DuetSQL.swift @@ -19,6 +19,8 @@ extension Admin: Model { return .enum(subscriptionStatus) case .subscriptionStatusExpiration: return .date(subscriptionStatusExpiration) + case .gclid: + return .string(gclid) case .createdAt: return .date(createdAt) case .updatedAt: @@ -34,6 +36,7 @@ extension Admin: Model { .subscriptionId: .string(subscriptionId?.rawValue), .subscriptionStatus: .enum(subscriptionStatus), .subscriptionStatusExpiration: .date(subscriptionStatusExpiration), + .gclid: .string(gclid), .createdAt: .currentTimestamp, .updatedAt: .currentTimestamp, ] From 895cd61c0355d46d7401201c59f996d0cbb59d6f Mon Sep 17 00:00:00 2001 From: Jared Henderson Date: Tue, 6 Feb 2024 15:35:56 -0500 Subject: [PATCH 2/3] api: capture signup with google click id (gclid) --- .../Api/PairQL/Dashboard/Pairs/Signup.swift | 4 +++- .../UnauthedResolverTests.swift | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/api/Sources/Api/PairQL/Dashboard/Pairs/Signup.swift b/api/Sources/Api/PairQL/Dashboard/Pairs/Signup.swift index 966528d8..47763bc4 100644 --- a/api/Sources/Api/PairQL/Dashboard/Pairs/Signup.swift +++ b/api/Sources/Api/PairQL/Dashboard/Pairs/Signup.swift @@ -10,6 +10,7 @@ struct Signup: Pair { struct Input: PairInput { var email: String var password: String + var gclid: String? } } @@ -42,7 +43,8 @@ extension Signup: Resolver { email: .init(rawValue: email), password: Env.mode == .test ? input.password : try Bcrypt.hash(input.password), subscriptionStatus: .pendingEmailVerification, - subscriptionStatusExpiration: Current.date().advanced(by: .days(7)) + subscriptionStatusExpiration: Current.date().advanced(by: .days(7)), + gclid: input.gclid )) try await sendVerificationEmail(to: admin, in: context) diff --git a/api/Tests/ApiTests/DashboardPairResolvers/UnauthedResolverTests.swift b/api/Tests/ApiTests/DashboardPairResolvers/UnauthedResolverTests.swift index 8398f41b..bd6271c1 100644 --- a/api/Tests/ApiTests/DashboardPairResolvers/UnauthedResolverTests.swift +++ b/api/Tests/ApiTests/DashboardPairResolvers/UnauthedResolverTests.swift @@ -30,18 +30,30 @@ final class DasboardUnauthedResolverTests: ApiTestCase { let input = Signup.Input(email: email, password: "pass") let output = try await Signup.resolve(with: input, in: context) - let user = try await Current.db.query(Admin.self) + let admin = try await Current.db.query(Admin.self) .where(.email == email) .first() expect(output).toEqual(.success) - expect(user.subscriptionStatus).toEqual(.pendingEmailVerification) - expect(user.subscriptionStatusExpiration).toEqual(.epoch.advanced(by: .days(7))) + expect(admin.subscriptionStatus).toEqual(.pendingEmailVerification) + expect(admin.subscriptionStatusExpiration).toEqual(.epoch.advanced(by: .days(7))) expect(sent.postmarkEmails.count).toEqual(1) expect(sent.postmarkEmails[0].to).toEqual(email) expect(sent.postmarkEmails[0].html).toContain("verify your email address") } + func testInitiateSignupWithGclid() async throws { + let email = "signup".random + "@example.com" + let input = Signup.Input(email: email, password: "pass", gclid: "gclid-123") + _ = try await Signup.resolve(with: input, in: context) + + let admin = try await Current.db.query(Admin.self) + .where(.email == email) + .first() + + expect(admin.gclid).toEqual("gclid-123") + } + func testLoginFromMagicLink() async throws { let admin = try await Current.db.create(Admin.random) let token = await Current.ephemeral.createAdminIdToken(admin.id) From 6cfb13a595a7b5a38039cbfdd9c57cf26661313c Mon Sep 17 00:00:00 2001 From: Jared Henderson Date: Tue, 6 Feb 2024 15:40:21 -0500 Subject: [PATCH 3/3] api: add from name to automated 'noreply' emails --- .../Dashboard/Pairs/SendPasswordResetEmail.swift | 4 ++-- api/Sources/Api/PairQL/Dashboard/Pairs/Signup.swift | 4 ++-- .../Api/Services/Jobs/SubscriptionEmails.swift | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/Sources/Api/PairQL/Dashboard/Pairs/SendPasswordResetEmail.swift b/api/Sources/Api/PairQL/Dashboard/Pairs/SendPasswordResetEmail.swift index 13da1cff..507a5c3b 100644 --- a/api/Sources/Api/PairQL/Dashboard/Pairs/SendPasswordResetEmail.swift +++ b/api/Sources/Api/PairQL/Dashboard/Pairs/SendPasswordResetEmail.swift @@ -35,7 +35,7 @@ extension SendPasswordResetEmail: Resolver { private func reset(_ email: String, _ dashboardUrl: String, _ token: UUID) -> XPostmark.Email { .init( to: email, - from: "noreply@gertrude.app", + from: "Gertrude App ", subject: "Gertrude password reset".withEmailSubjectDisambiguator, html: """ You can reset your Gertrude account password by clicking \ @@ -47,7 +47,7 @@ private func reset(_ email: String, _ dashboardUrl: String, _ token: UUID) -> XP private func notFound(_ email: String) -> XPostmark.Email { .init( to: email, - from: "noreply@gertrude.app", + from: "Gertrude App ", subject: "Gertrude app password reset".withEmailSubjectDisambiguator, html: """ A password reset was requested for this email address, \ diff --git a/api/Sources/Api/PairQL/Dashboard/Pairs/Signup.swift b/api/Sources/Api/PairQL/Dashboard/Pairs/Signup.swift index 47763bc4..aa4a1497 100644 --- a/api/Sources/Api/PairQL/Dashboard/Pairs/Signup.swift +++ b/api/Sources/Api/PairQL/Dashboard/Pairs/Signup.swift @@ -66,7 +66,7 @@ func sendVerificationEmail(to admin: Admin, in context: Context) async throws { private func accountExists(with email: String) -> XPostmark.Email { .init( to: email, - from: "noreply@gertrude.app", + from: "Gertrude App ", subject: "Gertrude Signup Request".withEmailSubjectDisambiguator, html: """ We received a request to initiate a signup for the Gertrude app, \ @@ -80,7 +80,7 @@ private func accountExists(with email: String) -> XPostmark.Email { private func verify(_ email: String, _ dashboardUrl: String, _ token: UUID) -> XPostmark.Email { .init( to: email, - from: "noreply@gertrude.app", + from: "Gertrude App ", subject: "Action Required: Confirm your email".withEmailSubjectDisambiguator, html: """ Please verify your email address by clicking \ diff --git a/api/Sources/Api/Services/Jobs/SubscriptionEmails.swift b/api/Sources/Api/Services/Jobs/SubscriptionEmails.swift index 99bfae43..e1aea97f 100644 --- a/api/Sources/Api/Services/Jobs/SubscriptionEmails.swift +++ b/api/Sources/Api/Services/Jobs/SubscriptionEmails.swift @@ -21,7 +21,7 @@ enum SubscriptionEmails { private static func trialEndingSoon(_ address: String) -> XPostmark.Email { .init( to: address, - from: "noreply@gertrude.app", + from: "Gertrude App ", replyTo: "jared@netrivet.com", subject: "[action required] Gertrude trial ending soon".withEmailSubjectDisambiguator, html: """ @@ -48,7 +48,7 @@ enum SubscriptionEmails { private static func trialEndedToOverdue(_ address: String) -> XPostmark.Email { .init( to: address, - from: "noreply@gertrude.app", + from: "Gertrude App ", replyTo: "jared@netrivet.com", subject: "[action required] Gertrude trial ended".withEmailSubjectDisambiguator, html: """ @@ -85,7 +85,7 @@ enum SubscriptionEmails { private static func overdueToUnpaid(_ address: String) -> XPostmark.Email { .init( to: address, - from: "noreply@gertrude.app", + from: "Gertrude App ", replyTo: "jared@netrivet.com", subject: "[action required] Gertrude account disabled".withEmailSubjectDisambiguator, html: """ @@ -121,7 +121,7 @@ enum SubscriptionEmails { private static func paidToOverdue(_ address: String) -> XPostmark.Email { .init( to: address, - from: "noreply@gertrude.app", + from: "Gertrude App ", replyTo: "jared@netrivet.com", subject: "[action required] Gertrude payment failed".withEmailSubjectDisambiguator, html: """ @@ -161,7 +161,7 @@ enum SubscriptionEmails { private static func unpaidToPendingDelete(_ address: String) -> XPostmark.Email { .init( to: address, - from: "noreply@gertrude.app", + from: "Gertrude App ", replyTo: "jared@netrivet.com", subject: "[action required] Gertrude account will be deleted".withEmailSubjectDisambiguator, html: """ @@ -189,7 +189,7 @@ enum SubscriptionEmails { private static func deleteEmailUnverified(_ address: String) -> XPostmark.Email { .init( to: address, - from: "noreply@gertrude.app", + from: "Gertrude App ", replyTo: "jared@netrivet.com", subject: "Gertrude unverified account deleted".withEmailSubjectDisambiguator, html: """