refactor(server): auth (#7994)

This commit is contained in:
forehalo
2024-09-03 09:03:39 +00:00
parent 821de0a3bb
commit 8b0afd6eeb
39 changed files with 639 additions and 775 deletions
@@ -1,4 +1,12 @@
import { Controller, Get, Query, Req, Res } from '@nestjs/common';
import {
Body,
Controller,
HttpCode,
HttpStatus,
Post,
Req,
Res,
} from '@nestjs/common';
import { ConnectedAccount, PrismaClient } from '@prisma/client';
import type { Request, Response } from 'express';
@@ -11,34 +19,34 @@ import {
OauthStateExpired,
UnknownOauthProvider,
URLHelper,
WrongSignInMethod,
} from '../../fundamentals';
import { OAuthProviderName } from './config';
import { OAuthAccount, Tokens } from './providers/def';
import { OAuthProviderFactory } from './register';
import { OAuthService } from './service';
@Controller('/oauth')
@Controller('/api/oauth')
export class OAuthController {
constructor(
private readonly auth: AuthService,
private readonly oauth: OAuthService,
private readonly user: UserService,
private readonly providerFactory: OAuthProviderFactory,
private readonly url: URLHelper,
private readonly providerFactory: OAuthProviderFactory,
private readonly db: PrismaClient
) {}
@Public()
@Get('/login')
async login(
@Res() res: Response,
@Query('provider') unknownProviderName: string,
@Query('redirect_uri') redirectUri?: string
@Post('/preflight')
@HttpCode(HttpStatus.OK)
async preflight(
@Body('provider') unknownProviderName?: string,
@Body('redirect_uri') redirectUri: string = this.url.home
) {
if (!unknownProviderName) {
throw new MissingOauthQueryParameter({ name: 'provider' });
}
// @ts-expect-error safe
const providerName = OAuthProviderName[unknownProviderName];
const provider = this.providerFactory.get(providerName);
@@ -48,20 +56,23 @@ export class OAuthController {
}
const state = await this.oauth.saveOAuthState({
redirectUri: redirectUri ?? this.url.home,
provider: providerName,
redirectUri,
});
return res.redirect(provider.getAuthUrl(state));
return {
url: provider.getAuthUrl(state),
};
}
@Public()
@Get('/callback')
@Post('/callback')
@HttpCode(HttpStatus.OK)
async callback(
@Req() req: Request,
@Res() res: Response,
@Query('code') code?: string,
@Query('state') stateStr?: string
@Body('code') code?: string,
@Body('state') stateStr?: string
) {
if (!code) {
throw new MissingOauthQueryParameter({ name: 'code' });
@@ -93,43 +104,18 @@ export class OAuthController {
const tokens = await provider.getToken(code);
const externAccount = await provider.getUser(tokens.accessToken);
const user = req.user;
const user = await this.loginFromOauth(
state.provider,
externAccount,
tokens
);
try {
if (!user) {
// if user not found, login
const user = await this.loginFromOauth(
state.provider,
externAccount,
tokens
);
const session = await this.auth.createUserSession(
user,
req.cookies[AuthService.sessionCookieName]
);
res.cookie(AuthService.sessionCookieName, session.sessionId, {
expires: session.expiresAt ?? void 0, // expiredAt is `string | null`
...this.auth.cookieOptions,
});
} else {
// if user is found, connect the account to this user
await this.connectAccountFromOauth(
user,
state.provider,
externAccount,
tokens
);
}
} catch (e: any) {
return res.redirect(
this.url.link('/signIn', {
redirect_uri: state.redirectUri,
error: e.message,
})
);
}
this.url.safeRedirect(res, state.redirectUri);
await this.auth.setCookies(req, res, user.id);
res.send({
id: user.id,
/* @deprecated */
redirectUri: state.redirectUri,
});
}
private async loginFromOauth(
@@ -154,37 +140,27 @@ export class OAuthController {
return connectedUser.user;
}
let user = await this.user.findUserByEmail(externalAccount.email);
const user = await this.user.fulfillUser(externalAccount.email, {
emailVerifiedAt: new Date(),
registered: true,
avatarUrl: externalAccount.avatarUrl,
});
if (user) {
// we can't directly connect the external account with given email in sign in scenario for safety concern.
// let user manually connect in account sessions instead.
if (user.registered) {
throw new WrongSignInMethod();
}
await this.db.connectedAccount.create({
data: {
userId: user.id,
provider,
providerAccountId: externalAccount.id,
...tokens,
},
});
return user;
} else {
user = await this.createUserWithConnectedAccount(
await this.db.connectedAccount.create({
data: {
userId: user.id,
provider,
externalAccount,
tokens
);
}
providerAccountId: externalAccount.id,
...tokens,
},
});
return user;
}
updateConnectedAccount(connectedUser: ConnectedAccount, tokens: Tokens) {
private async updateConnectedAccount(
connectedUser: ConnectedAccount,
tokens: Tokens
) {
return this.db.connectedAccount.update({
where: {
id: connectedUser.id,
@@ -193,27 +169,12 @@ export class OAuthController {
});
}
async createUserWithConnectedAccount(
provider: OAuthProviderName,
externalAccount: OAuthAccount,
tokens: Tokens
) {
return this.user.createUser({
email: externalAccount.email,
name: externalAccount.email.split('@')[0],
avatarUrl: externalAccount.avatarUrl,
emailVerifiedAt: new Date(),
connectedAccounts: {
create: {
provider,
providerAccountId: externalAccount.id,
...tokens,
},
},
});
}
private async connectAccountFromOauth(
/**
* we currently don't support connect oauth account to existing user
* keep it incase we need it in the future
*/
// @ts-expect-error allow unused
private async _connectAccount(
user: { id: string },
provider: OAuthProviderName,
externalAccount: OAuthAccount,