Skip to content

Commit

Permalink
Add name and avatar to user, refactoring (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
petrpavlik authored Jan 29, 2024
1 parent 7013899 commit d0acfd1
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 114 deletions.
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

0 comments on commit d0acfd1

Please sign in to comment.