Skip to content

Commit

Permalink
refactor(server): auth
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo authored and darkskygit committed Aug 29, 2024
1 parent f1bb1fc commit dca6bc0
Show file tree
Hide file tree
Showing 39 changed files with 632 additions and 767 deletions.
7 changes: 0 additions & 7 deletions .github/helm/affine/templates/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,6 @@ spec:
name: affine-graphql
port:
number: {{ .Values.graphql.service.port }}
- path: /oauth
pathType: Prefix
backend:
service:
name: affine-graphql
port:
number: {{ .Values.graphql.service.port }}
- path: /
pathType: Prefix
backend:
Expand Down
13 changes: 7 additions & 6 deletions packages/backend/server/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ model ConnectedAccount {
}

model Session {
id String @id @default(uuid()) @db.VarChar
expiresAt DateTime? @map("expires_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
id String @id @default(uuid()) @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
userSessions UserSession[]
// @deprecated use [UserSession.expiresAt]
deprecated_expiresAt DateTime? @map("expires_at") @db.Timestamptz(3)
@@map("multiple_users_sessions")
}

Expand Down Expand Up @@ -243,7 +244,7 @@ model Snapshot {
updatedAt DateTime @map("updated_at") @db.Timestamptz(3)
// @deprecated use updatedAt only
seq Int? @default(0) @db.Integer
seq Int? @default(0) @db.Integer
// we need to clear all hanging updates and snapshots before enable the foreign key on workspaceId
// workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
Expand Down Expand Up @@ -276,7 +277,7 @@ model Update {
createdAt DateTime @map("created_at") @db.Timestamptz(3)
// @deprecated use createdAt only
seq Int? @db.Integer
seq Int? @db.Integer
@@id([workspaceId, id, createdAt])
@@map("updates")
Expand Down
161 changes: 100 additions & 61 deletions packages/backend/server/src/core/auth/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,33 @@ import {
EarlyAccessRequired,
EmailTokenNotFound,
InternalServerError,
InvalidEmail,
InvalidEmailToken,
SignUpForbidden,
Throttle,
URLHelper,
} from '../../fundamentals';
import { UserService } from '../user';
import { validators } from '../utils/validators';
import { CurrentUser } from './current-user';
import { Public } from './guard';
import { AuthService, parseAuthUserSeqNum } from './service';
import { AuthService } from './service';
import { CurrentUser, Session } from './session';
import { TokenService, TokenType } from './token';

class SignInCredential {
email!: string;
interface PreflightResponse {
registered: boolean;
hasPassword: boolean;
}

interface SignInCredential {
email: string;
password?: string;
callbackUrl?: string;
}

class MagicLinkCredential {
email!: string;
token!: string;
interface MagicLinkCredential {
email: string;
token: string;
}

@Throttle('strict')
Expand All @@ -51,14 +58,44 @@ export class AuthController {
private readonly config: Config
) {}

@Public()
@Post('/preflight')
async preflight(
@Body() params?: { email: string }
): Promise<PreflightResponse> {
if (!params?.email) {
throw new InvalidEmail();
}
validators.assertValidEmail(params.email);

const user = await this.user.findUserWithHashedPasswordByEmail(
params.email
);

if (!user) {
return {
registered: false,
hasPassword: false,
};
}

return {
registered: user.registered,
hasPassword: !!user.password,
};
}

@Public()
@Post('/sign-in')
@Header('content-type', 'application/json')
async signIn(
@Req() req: Request,
@Res() res: Response,
@Body() credential: SignInCredential,
@Query('redirect_uri') redirectUri = this.url.home
/**
* @deprecated
*/
@Query('redirect_uri') redirectUri?: string
) {
validators.assertValidEmail(credential.email);
const canSignIn = await this.auth.canSignIn(credential.email);
Expand All @@ -67,80 +104,83 @@ export class AuthController {
}

if (credential.password) {
const user = await this.auth.signIn(
await this.passwordSignIn(
req,
res,
credential.email,
credential.password
);

await this.auth.setCookie(req, res, user);
res.status(HttpStatus.OK).send(user);
} else {
// send email magic link
const user = await this.user.findUserByEmail(credential.email);
if (!user) {
const allowSignup = await this.config.runtime.fetch('auth/allowSignup');
if (!allowSignup) {
throw new SignUpForbidden();
}
}

const result = await this.sendSignInEmail(
{ email: credential.email, signUp: !user },
await this.sendMagicLink(
req,
res,
credential.email,
credential.callbackUrl,
redirectUri
);
}
}

if (result.rejected.length) {
throw new InternalServerError('Failed to send sign-in email.');
}
async passwordSignIn(
req: Request,
res: Response,
email: string,
password: string
) {
const user = await this.auth.signIn(email, password);

res.status(HttpStatus.OK).send({
email: credential.email,
});
}
await this.auth.setCookies(req, res, user.id);
res.status(HttpStatus.OK).send(user);
}

async sendSignInEmail(
{ email, signUp }: { email: string; signUp: boolean },
redirectUri: string
async sendMagicLink(
_req: Request,
res: Response,
email: string,
callbackUrl = '/magic-link',

redirectUrl = this.url.home
) {
// send email magic link
const user = await this.user.findUserByEmail(email);
if (!user) {
const allowSignup = await this.config.runtime.fetch('auth/allowSignup');
if (!allowSignup) {
throw new SignUpForbidden();
}
}

const token = await this.token.createToken(TokenType.SignIn, email);

const magicLink = this.url.link('/magic-link', {
const magicLink = this.url.link(callbackUrl, {
token,
email,
redirect_uri: redirectUri,
redirect_uri: redirectUrl,
});

const result = await this.auth.sendSignInEmail(email, magicLink, signUp);
const result = await this.auth.sendSignInEmail(email, magicLink, !user);

if (result.rejected.length) {
throw new InternalServerError('Failed to send sign-in email.');
}

return result;
res.status(HttpStatus.OK).send({
email: email,
});
}

@Get('/sign-out')
async signOut(
@Req() req: Request,
@Res() res: Response,
@Query('redirect_uri') redirectUri?: string
@Session() session: Session,
@Body() { all }: { all: boolean }
) {
const session = await this.auth.signOut(
req.cookies[AuthService.sessionCookieName],
parseAuthUserSeqNum(req.headers[AuthService.authUserSeqHeaderName])
await this.auth.signOut(
session.sessionId,
all ? undefined : session.userId
);

if (session) {
res.cookie(AuthService.sessionCookieName, session.id, {
expires: session.expiresAt ?? void 0, // expiredAt is `string | null`
...this.auth.cookieOptions,
});
} else {
res.clearCookie(AuthService.sessionCookieName);
}

if (redirectUri) {
return this.url.safeRedirect(res, redirectUri);
} else {
return res.send(null);
}
res.status(HttpStatus.OK).send({});
}

@Public()
Expand All @@ -156,11 +196,11 @@ export class AuthController {

validators.assertValidEmail(email);

const valid = await this.token.verifyToken(TokenType.SignIn, token, {
const tokenRecord = await this.token.verifyToken(TokenType.SignIn, token, {
credential: email,
});

if (!valid) {
if (!tokenRecord) {
throw new InvalidEmailToken();
}

Expand All @@ -169,9 +209,8 @@ export class AuthController {
registered: true,
});

await this.auth.setCookie(req, res, user);

res.send({ id: user.id, email: user.email, name: user.name });
await this.auth.setCookies(req, res, user.id);
res.send({ id: user.id });
}

@Throttle('default', { limit: 1200 })
Expand Down
Loading

0 comments on commit dca6bc0

Please sign in to comment.