Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add name and avatar to user, refactoring #11

Merged
merged 1 commit into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/App/Auth/FirebaseAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import FirebaseJWTMiddleware
protocol JWTUser {
var userID: String { get }
var email: String? { get }
var name: String? { get }
var picture: String? { get }
}

extension FirebaseJWTPayload : JWTUser {
Expand Down
98 changes: 41 additions & 57 deletions Sources/App/Controllers/OrganizationController.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
import Foundation
import Fluent
import Vapor
import FirebaseJWTMiddleware
import MixpanelVapor

extension Request {
func organization(minRole: ProfileOrganizationRole.Role) async throws -> Organization {
guard let organizationId = self.parameters.get("organizationID").flatMap({ UUID(uuidString: $0) }) else {
throw Abort(.badRequest)
}

let profile = try await self.profile

guard let profileId = profile.id else {
throw Abort(.internalServerError)
}

guard let membership = try await ProfileOrganizationRole
.query(on: self.db)
.join(Organization.self, on: \ProfileOrganizationRole.$organization.$id == \Organization.$id)
.join(Profile.self, on: \ProfileOrganizationRole.$profile.$id == \Profile.$id)
.filter(Profile.self, \.$id == profileId)
.filter(Organization.self, \.$id == organizationId)
.first() else {

throw Abort(.unauthorized)
}

guard membership.role >= minRole else {
throw Abort(.unauthorized)
}

try await membership.$organization.load(on: self.db)

return membership.organization
}
}

enum OrganizationRoleDTO: String, Content {
case admin
Expand Down Expand Up @@ -97,36 +128,13 @@ struct OrganizationController: RouteCollection {
try await organizationRole.$profile.load(on: req.db)
}

await req.mixpanel.track(name: "organization_created", request: req, params: ["email": profile.email, "organization_id": "\(organizationId)"])
await req.trackAnalyticsEvent(name: "organization_created", params: ["email": profile.email, "organization_id": "\(organizationId)"])

return try organization.toDTO()
}

func patch(req: Request) async throws -> OrganizationDTO {
let profile = try await req.profile

guard let organizationId = req.parameters.get("organizationID").flatMap({ UUID(uuidString: $0) }) else {
throw Abort(.badRequest)
}

guard let profileId = profile.id else {
throw Abort(.internalServerError)
}

guard let role = try await ProfileOrganizationRole
.query(on: req.db)
.join(Organization.self, on: \ProfileOrganizationRole.$organization.$id == \Organization.$id)
.join(Profile.self, on: \ProfileOrganizationRole.$profile.$id == \Profile.$id)
.filter(Profile.self, \.$id == profileId)
.filter(Organization.self, \.$id == organizationId)
.filter(\.$role == .admin)
.with(\.$organization)
.first() else {

throw Abort(.unauthorized)
}

let organization = role.organization
let organization = try await req.organization(minRole: .admin)

struct OrganizationUpdateDTO: Content, Validatable {
var name: String?
Expand All @@ -149,41 +157,17 @@ struct OrganizationController: RouteCollection {
try await organizationRole.$profile.load(on: req.db)
}

await req.mixpanel.track(name: "organization_updated", request: req, params: ["email": profile.email, "organization_id": "\(organizationId)"])
let organizationId = try organization.requireID()
await req.trackAnalyticsEvent(name: "organization_updated", params: ["organization_id": "\(organizationId)"])

return try organization.toDTO()
}

func delete(req: Request) async throws -> HTTPStatus {
print("xxx delete organization")
let profile = try await req.profile

guard let organizationId = req.parameters.get("organizationID").flatMap({ UUID(uuidString: $0) }) else {
throw Abort(.badRequest)
}

try await profile.$organizationRoles.load(on: req.db)

guard let profileId = profile.id else {
throw Abort(.internalServerError)
}

guard let role = try await ProfileOrganizationRole
.query(on: req.db)
.join(Organization.self, on: \ProfileOrganizationRole.$organization.$id == \Organization.$id)
.join(Profile.self, on: \ProfileOrganizationRole.$profile.$id == \Profile.$id)
.filter(Profile.self, \.$id == profileId)
.filter(Organization.self, \.$id == organizationId)
.filter(\.$role == .admin)
.with(\.$organization)
.first() else {

throw Abort(.unauthorized)
}

try await role.organization.delete(on: req.db)

await req.mixpanel.track(name: "organization_deleted", request: req, params: ["email": profile.email, "organization_id": "aaa"])
let organization = try await req.organization(minRole: .admin)
let organizationName = organization.name
try await organization.delete(on: req.db)
await req.trackAnalyticsEvent(name: "organization_deleted", params: ["organization_name": organizationName])

return .noContent
}
Expand Down
13 changes: 6 additions & 7 deletions Sources/App/Controllers/ProfileController.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Fluent
import Vapor
import MixpanelVapor

extension Request {
var profile: Profile {
Expand Down Expand Up @@ -62,14 +61,14 @@ struct ProfileController: RouteCollection {
throw Abort(.badRequest, reason: "Firebase user email does not match profile email.")
}

await req.mixpanel.track(name: "profile_created", request: req, params: ["email": email])
await req.trackAnalyticsEvent(name: "profile_created")

return try profile.toDTO()
} else {
guard let email = token.email else {
throw Abort(.badRequest, reason: "Firebase user does not have an email address.")
}
let profile = Profile(firebaseUserId: token.userID, email: email)
let profile = Profile(firebaseUserId: token.userID, email: email, name: token.name, avatarUrl: token.picture)
try await profile.save(on: req.db)
return try profile.toDTO()
}
Expand All @@ -87,10 +86,10 @@ struct ProfileController: RouteCollection {
if let isSubscribedToNewsletter = update.isSubscribedToNewsletter {
if isSubscribedToNewsletter && profile.subscribedToNewsletterAt == nil {
profile.subscribedToNewsletterAt = Date()
await req.mixpanel.track(name: "profile_subscribed_to_newsletter", request: req, params: ["email": profile.email])
await req.trackAnalyticsEvent(name: "profile_subscribed_to_newsletter")
} else if profile.subscribedToNewsletterAt != nil {
profile.subscribedToNewsletterAt = nil
await req.mixpanel.track(name: "profile_unsubscribed_from_newsletter", request: req, params: ["email": profile.email])
await req.trackAnalyticsEvent(name: "profile_unsubscribed_from_newsletter")
}
}

Expand All @@ -102,8 +101,8 @@ struct ProfileController: RouteCollection {
func delete(req: Request) async throws -> HTTPStatus {
// TODO: delete org if it's the last admin member
let profile = try await req.profile
try await req.profile.delete(on: req.db)
await req.mixpanel.track(name: "profile_deleted", request: req, params: ["email": profile.email])
try await profile.delete(on: req.db)
await req.trackAnalyticsEvent(name: "profile_deleted", params: ["email": profile.email])
return .noContent
}
}
2 changes: 2 additions & 0 deletions Sources/App/Migrations/CreateProfile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ struct CreateProfile: AsyncMigration {
.id()
.field("firebase_user_id", .string, .required)
.field("email", .string, .required)
.field("name", .string)
.field("avatar_url", .string)
.field("subscribed_to_newsletter_at", .date)
.field("created_at", .datetime)
.field("updated_at", .datetime)
Expand Down
10 changes: 9 additions & 1 deletion Sources/App/Models/Profile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ final class Profile: Model, Content {

@OptionalField(key: "subscribed_to_newsletter_at")
var subscribedToNewsletterAt: Date?

@OptionalField(key: "name")
var name: String?

@OptionalField(key: "avatar_url")
var avatarUrl: String?

@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
Expand All @@ -30,10 +36,12 @@ final class Profile: Model, Content {

init() { }

init(id: UUID? = nil, firebaseUserId: String, email: String, subscribedToNewsletterAt: Date? = nil) {
init(id: UUID? = nil, firebaseUserId: String, email: String, name: String?, avatarUrl: String?, subscribedToNewsletterAt: Date? = nil) {
self.id = id
self.firebaseUserId = firebaseUserId
self.email = email
self.name = name
self.avatarUrl = avatarUrl
self.subscribedToNewsletterAt = subscribedToNewsletterAt
}
}
10 changes: 8 additions & 2 deletions Sources/App/Models/ProfileOrganizationRole.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import Vapor

final class ProfileOrganizationRole: Model {

enum Role: String, Codable {
enum Role: String, Codable, Comparable {

static func < (lhs: ProfileOrganizationRole.Role, rhs: ProfileOrganizationRole.Role) -> Bool {
let order: [Role] = [.lurker, .editor, .admin]
return order.firstIndex(of: lhs)! < order.firstIndex(of: rhs)!
}

case admin
case editor
case lurker
case lurker
}

static let schema = "organization+profile"
Expand Down
39 changes: 39 additions & 0 deletions Sources/App/Utils/Analytics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// File.swift
//
//
// Created by Petr Pavlik on 29.01.2024.
//

import Foundation
import Vapor
import MixpanelVapor

extension Request {
func trackAnalyticsEvent(name: String, params: [String: any Content] = [:]) async {

guard application.environment.isRelease else {
return
}

var params = params
if let profile = try? await profile {

if let profileId = profile.id {
params["$user_id"] = profileId.uuidString
}

params["$email"] = profile.email

if let name = profile.name {
params["$name"] = name
}
if let avatarUrl = profile.avatarUrl {
params["$avatar"] = avatarUrl
}
}

// Log to a destination of your choice
await mixpanel.track(name: name, request: self, params: params)
}
}
59 changes: 59 additions & 0 deletions Sources/App/Utils/Email.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// File.swift
//
//
// Created by Petr Pavlik on 29.01.2024.
//

import Foundation
import Vapor
import VaporSMTPKit
import SMTPKitten

extension Application {
func sendEmail(subject: String, message: String, to email: String) async throws {
guard try Environment.detect() != .testing else {
return
}

// Following logic uses an email integrated through STMP to send your transactional emails
// You can replace this with email provider of your choice, like Amazon SES or resend.com

guard let smtpHostName = Environment.process.SMTP_HOSTNAME else {
throw Abort(.internalServerError, reason: "SMTP_HOSTNAME env variable not defined")
}

guard let smtpEmail = Environment.process.SMTP_EMAIL else {
throw Abort(.internalServerError, reason: "SMTP_EMAIL env variable not defined")
}

guard let smtpPassword = Environment.process.SMTP_PASSWORD else {
throw Abort(.internalServerError, reason: "SMTP_PASSWORD env variable not defined")
}

let credentials = SMTPCredentials(
hostname: smtpHostName,
ssl: .startTLS(configuration: .default),
email: smtpEmail,
password: smtpPassword
)

let email = Mail(
from: .init(name: "[name] from [company]", email: smtpEmail),
to: [
MailUser(name: nil, email: email)
],
subject: subject,
contentType: .plain, // supports html
text: message
)

try await sendMail(email, withCredentials: credentials).get()
}
}

extension Request {
func sendEmail(subject: String, message: String, to: String) async throws {
try await self.application.sendEmail(subject: subject, message: message, to: to)
}
}
47 changes: 0 additions & 47 deletions Sources/App/configure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,6 @@ import NIOSSL
import Fluent
import FluentPostgresDriver
import Vapor
import VaporSMTPKit
import SMTPKitten

extension Application {
func sendEmail(subject: String, message: String, to email: String) async throws {
guard try Environment.detect() != .testing else {
return
}

guard let smtpHostName = Environment.process.SMTP_HOSTNAME else {
throw Abort(.internalServerError, reason: "SMTP_HOSTNAME env variable not defined")
}

guard let smtpEmail = Environment.process.SMTP_EMAIL else {
throw Abort(.internalServerError, reason: "SMTP_EMAIL env variable not defined")
}

guard let smtpPassword = Environment.process.SMTP_PASSWORD else {
throw Abort(.internalServerError, reason: "SMTP_PASSWORD env variable not defined")
}

let credentials = SMTPCredentials(
hostname: smtpHostName,
ssl: .startTLS(configuration: .default),
email: smtpEmail,
password: smtpPassword
)

let email = Mail(
from: .init(name: "[name] from [company]", email: smtpEmail),
to: [
MailUser(name: nil, email: email)
],
subject: subject,
contentType: .plain, // supports html
text: message
)

try await sendMail(email, withCredentials: credentials).get()
}
}

extension Request {
func sendEmail(subject: String, message: String, to: String) async throws {
try await self.application.sendEmail(subject: subject, message: message, to: to)
}
}

// configures your application
public func configure(_ app: Application) async throws {
Expand Down
Loading