-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Supports logging in with single provider only
- Loading branch information
Showing
10 changed files
with
128 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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]>" | ||
|
||
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
type Account = { | ||
readonly provider: string | ||
} | ||
|
||
export default Account |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |