Skip to content

Commit

Permalink
Supports logging in with single provider only
Browse files Browse the repository at this point in the history
  • Loading branch information
simonbs committed Jun 26, 2024
1 parent fe2e3e7 commit e4f11f9
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 34 deletions.
23 changes: 7 additions & 16 deletions src/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ import {
} from "@/features/auth/domain"
import {
DbGuestRepository,
DbUserRepository,
EmailGuestInviter
} from "./features/admin/data"
import {
IGuestInviter,
IGuestRepository
IGuestRepository,
IUserRepository
} from "./features/admin/domain"

const {
Expand Down Expand Up @@ -95,10 +97,12 @@ const oauthTokenRepository = new CompositeOAuthTokenRepository({
]
})

const logInHandler = new LogInHandler({ oauthTokenRepository })
const userRepository: IUserRepository = new DbUserRepository({ db })

export const guestRepository: IGuestRepository = new DbGuestRepository({ db })

const logInHandler = new LogInHandler({ userRepository, guestRepository })

// Must be a verified email in AWS SES.
const fromEmail = FROM_EMAIL || "Shape Docs <[email protected]>"

Expand Down Expand Up @@ -137,20 +141,7 @@ export const auth = NextAuth({
},
callbacks: {
async signIn({ user, account, email }) {
if (email && email.verificationRequest && user.email) { // in verification request flow
const guest = await guestRepository.findByEmail(user.email)
if (guest == undefined) {
return false // email not invited
}
}
if (!user.id) {
return false
}
if (account) {
return await logInHandler.handleLogIn(user.id, account)
} else {
return await logInHandler.handleLogIn(user.id)
}
return await logInHandler.handleLogIn(user, account, email)
},
async session({ session, user }) {
session.user.id = user.id
Expand Down
54 changes: 54 additions & 0 deletions src/features/admin/data/DbUserRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { IDB } from "@/common"
import { User, IUserRepository } from "../domain"

type UserRow = {
readonly id: number
readonly name: string | null
readonly email: string | null
readonly image: string | null
readonly account_provider: string | null
}

export default class DbUserRepository implements IUserRepository {
private readonly db: IDB

constructor(config: { db: IDB }) {
this.db = config.db
}

async findByEmail(email: string): Promise<User | undefined> {
const sql = `
SELECT u.id, u.name, u.email, u.image, a.provider as account_provider
FROM users u
LEFT JOIN accounts a ON u.id = a."userId"
WHERE u.email = $1
`
const result = await this.db.query<UserRow>(sql, [email])
const users = this.groupUserRows(result.rows)
if (users.length == 0) {
return undefined
}
return users[0]
}

private groupUserRows(rows: UserRow[]): User[] {
const userMap = new Map<number, User>()
for (const row of rows) {
let user = userMap.get(row.id)
if (!user) {
user = {
id: row.id,
name: row.name,
email: row.email,
image: row.image,
accounts: []
}
userMap.set(row.id, user)
}
if (row.account_provider) {
user.accounts.push({ provider: row.account_provider })
}
}
return Array.from(userMap.values())
}
}
1 change: 1 addition & 0 deletions src/features/admin/data/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as DbGuestRepository } from "./DbGuestRepository"
export { default as DbUserRepository } from "./DbUserRepository"
export { default as EmailGuestInviter } from "./EmailGuestInviter"
5 changes: 5 additions & 0 deletions src/features/admin/domain/Account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type Account = {
readonly provider: string
}

export default Account
5 changes: 5 additions & 0 deletions src/features/admin/domain/IUserRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import User from "./User"

export default interface IUserRepository {
findByEmail(email: string): Promise<User | undefined>
}
11 changes: 11 additions & 0 deletions src/features/admin/domain/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Account from "./Account"

type User = {
readonly id: number
readonly name: string | null
readonly email: string | null
readonly image: string | null
readonly accounts: Account[]
}

export default User
3 changes: 3 additions & 0 deletions src/features/admin/domain/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export type { default as Account } from "./Account"
export type { default as Guest } from "./Guest"
export type { default as IGuestRepository } from "./IGuestRepository"
export type { default as IGuestInviter } from "./IGuestInviter"
export type { default as IUserRepository } from "./IUserRepository"
export type { default as User } from "./User"
11 changes: 10 additions & 1 deletion src/features/auth/domain/log-in/ILogInHandler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
export interface IUser {
readonly id?: string
readonly email?: string | null
}

export interface IAccount {
readonly provider: string
readonly providerAccountId: string
readonly access_token?: string
readonly refresh_token?: string
}

export interface IEmail {
readonly verificationRequest?: boolean
}

export default interface ILogInHandler {
handleLogIn(userId: string, account?: IAccount): Promise<boolean>
handleLogIn(user: IUser, account: IAccount | null, email?: IEmail): Promise<boolean | string>
}
47 changes: 31 additions & 16 deletions src/features/auth/domain/log-in/LogInHandler.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,57 @@
import { ILogInHandler, IAccount } from "."
import { IOAuthTokenRepository } from ".."
import { ILogInHandler, IUser, IAccount, IEmail } from "."
import { IGuestRepository, IUserRepository } from "@/features/admin/domain"

export default class LogInHandler implements ILogInHandler {
private readonly oauthTokenRepository: IOAuthTokenRepository
private readonly userRepository: IUserRepository
private readonly guestRepository: IGuestRepository

constructor(config: { oauthTokenRepository: IOAuthTokenRepository }) {
this.oauthTokenRepository = config.oauthTokenRepository
constructor(config: {
userRepository: IUserRepository,
guestRepository: IGuestRepository
}) {
this.userRepository = config.userRepository
this.guestRepository = config.guestRepository
}

async handleLogIn(userId: string, account?: IAccount): Promise<boolean> {
async handleLogIn(user: IUser, account: IAccount | null, email?: IEmail) {
if (!account) {
return false
}
if (account.provider === "github") {
return await this.handleLogInForGitHubUser(userId, account)
return await this.handleLogInForGitHubUser(account)
} else if (account.provider === "nodemailer") {
return true
return await this.handleLogInForGuestUser(user)
} else {
console.error("Unhandled account provider: " + account.provider)
return false
}
}

private async handleLogInForGitHubUser(userId: string, account: IAccount): Promise<boolean> {
private async handleLogInForGitHubUser(account: IAccount) {
if (!account.access_token) {
return false
}
if (!account.refresh_token) {
return false
}
try {
await this.oauthTokenRepository.set(userId, {
accessToken: account.access_token,
refreshToken: account.refresh_token
})
return true
} catch {
return true
}

private async handleLogInForGuestUser(user: IUser) {
if (!user.email) {
return false
}
const existingUser = await this.userRepository.findByEmail(user.email)
if (existingUser && existingUser.accounts.length > 0) {
// The user is already authenticated with an identity provider,
// so we'll ask them to use that instead.
return "/api/auth/signin?error=OAuthAccountNotLinked"
}
const guest = await this.guestRepository.findByEmail(user.email)
if (!guest) {
// The e-mail address has not been invited as a guest.
return false
}
return true
}
}
2 changes: 1 addition & 1 deletion src/features/auth/domain/log-in/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { default as ILogInHandler, IAccount } from "./ILogInHandler"
export type { default as ILogInHandler, IUser, IAccount, IEmail } from "./ILogInHandler"
export { default as LogInHandler } from "./LogInHandler"

0 comments on commit e4f11f9

Please sign in to comment.