feat: magic-link auth complete
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
"apollo-server-plugin-base": "^3.7.1",
|
"apollo-server-plugin-base": "^3.7.1",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fp-ts": "^2.13.1",
|
"fp-ts": "^2.13.1",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.0",
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
"@nestjs/schematics": "^9.0.3",
|
"@nestjs/schematics": "^9.0.3",
|
||||||
"@nestjs/testing": "^9.2.1",
|
"@nestjs/testing": "^9.2.1",
|
||||||
"@relmify/jest-fp-ts": "^2.0.2",
|
"@relmify/jest-fp-ts": "^2.0.2",
|
||||||
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^18.11.10",
|
"@types/node": "^18.11.10",
|
||||||
|
|||||||
6228
packages/hoppscotch-backend/pnpm-lock.yaml
generated
Normal file
6228
packages/hoppscotch-backend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
|||||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
import { Body, Controller, Get, HttpStatus, Post, Res } from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { signInMagicDto } from './dto/signin-magic.dto';
|
import { signInMagicDto } from './dto/signin-magic.dto';
|
||||||
import { verifyMagicDto } from './dto/verify-magic.dto';
|
import { verifyMagicDto } from './dto/verify-magic.dto';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@@ -11,4 +12,23 @@ export class AuthController {
|
|||||||
async signIn(@Body() authData: signInMagicDto) {
|
async signIn(@Body() authData: signInMagicDto) {
|
||||||
return this.authService.signIn(authData.email);
|
return this.authService.signIn(authData.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: set expiresOn to cookies
|
||||||
|
@Post('verify')
|
||||||
|
async verify(@Body() data: verifyMagicDto, @Res() res: Response) {
|
||||||
|
const authTokens = await this.authService.verify(data);
|
||||||
|
res.cookie('access_token', authTokens.access_token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
});
|
||||||
|
res.cookie('refresh_token', authTokens.refresh_token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
});
|
||||||
|
res.status(HttpStatus.OK).json({
|
||||||
|
message: 'Successfully logged in',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,21 @@ import { AuthController } from './auth.controller';
|
|||||||
import { UserModule } from 'src/user/user.module';
|
import { UserModule } from 'src/user/user.module';
|
||||||
import { MailerModule } from 'src/mailer/mailer.module';
|
import { MailerModule } from 'src/mailer/mailer.module';
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { JwtModule } from '@nestjs/jwt/dist';
|
||||||
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, UserModule, MailerModule],
|
imports: [
|
||||||
providers: [AuthService],
|
PrismaModule,
|
||||||
|
UserModule,
|
||||||
|
MailerModule,
|
||||||
|
PassportModule,
|
||||||
|
JwtModule.register({
|
||||||
|
secret: process.env.JWT_SECRET,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [AuthService, JwtStrategy],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -6,21 +6,37 @@ import { UserService } from 'src/user/user.service';
|
|||||||
import { verifyMagicDto } from './dto/verify-magic.dto';
|
import { verifyMagicDto } from './dto/verify-magic.dto';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
import { PasswordlessToken } from 'src/types/Passwordless';
|
import { PasswordlessToken } from 'src/types/Passwordless';
|
||||||
import { EmailCodec } from 'src/types/Email';
|
import { EmailCodec } from 'src/types/Email';
|
||||||
import { INVALID_EMAIL } from 'src/errors';
|
import {
|
||||||
|
INVALID_EMAIL,
|
||||||
|
INVALID_MAGIC_LINK_DATA,
|
||||||
|
PASSWORDLESS_DATA_NOT_FOUND,
|
||||||
|
MAGIC_LINK_EXPIRED,
|
||||||
|
TOKEN_EXPIRED,
|
||||||
|
USER_NOT_FOUND,
|
||||||
|
} from 'src/errors';
|
||||||
import { pipe } from 'fp-ts/lib/function';
|
import { pipe } from 'fp-ts/lib/function';
|
||||||
import { validateEmail } from 'src/utils';
|
import { throwErr, validateEmail } from 'src/utils';
|
||||||
|
import {
|
||||||
|
AccessTokenPayload,
|
||||||
|
AuthTokens,
|
||||||
|
RefreshTokenPayload,
|
||||||
|
} from 'src/types/AuthTokens';
|
||||||
|
import { ProviderAccount } from 'src/types/ProviderAccount';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { pass } from 'fp-ts/lib/Writer';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private prismaService: PrismaService,
|
private prismaService: PrismaService,
|
||||||
|
private jwtService: JwtService,
|
||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -32,30 +48,140 @@ export class AuthService {
|
|||||||
const idToken: PasswordlessToken =
|
const idToken: PasswordlessToken =
|
||||||
await this.prismaService.passwordlessVerification.create({
|
await this.prismaService.passwordlessVerification.create({
|
||||||
data: {
|
data: {
|
||||||
expiresOn: expiresOn,
|
|
||||||
deviceIdentifier: salt,
|
deviceIdentifier: salt,
|
||||||
userUid: user.id,
|
userUid: user.id,
|
||||||
|
expiresOn: expiresOn,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return idToken;
|
return idToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async validatePasswordlessTokens(data: verifyMagicDto) {
|
||||||
|
try {
|
||||||
|
const tokens: PasswordlessToken =
|
||||||
|
await this.prismaService.passwordlessVerification.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
passwordless_deviceIdentifier_tokens: {
|
||||||
|
deviceIdentifier: data.deviceIdentifier,
|
||||||
|
token: data.token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return O.some(tokens);
|
||||||
|
} catch (error) {
|
||||||
|
return O.none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UpdateUserRefreshToken(tokenHash: string, userUid: string) {
|
||||||
|
try {
|
||||||
|
const user: User = await this.prismaService.user.update({
|
||||||
|
where: {
|
||||||
|
id: userUid,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
refreshToken: tokenHash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return E.right(user);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateRefreshToken(userUid: string) {
|
||||||
|
const refreshTokenPayload: RefreshTokenPayload = {
|
||||||
|
iss: process.env.APP_DOMAIN,
|
||||||
|
sub: userUid,
|
||||||
|
aud: [process.env.APP_DOMAIN],
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshToken = await this.jwtService.sign(refreshTokenPayload, {
|
||||||
|
expiresIn: process.env.REFRESH_TOKEN_VALIDITY, //7 Days
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshTokenHash = await argon2.hash(refreshToken);
|
||||||
|
|
||||||
|
const updatedUser = await this.UpdateUserRefreshToken(
|
||||||
|
refreshTokenHash,
|
||||||
|
userUid,
|
||||||
|
);
|
||||||
|
if (E.isLeft(updatedUser))
|
||||||
|
throw new HttpException(updatedUser.left, HttpStatus.NOT_FOUND);
|
||||||
|
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateAuthTokens(userUid: string) {
|
||||||
|
const accessTokenPayload: AccessTokenPayload = {
|
||||||
|
iss: process.env.APP_DOMAIN,
|
||||||
|
sub: userUid,
|
||||||
|
aud: [process.env.APP_DOMAIN],
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshToken = await this.generateRefreshToken(userUid);
|
||||||
|
|
||||||
|
return <AuthTokens>{
|
||||||
|
access_token: await this.jwtService.sign(accessTokenPayload, {
|
||||||
|
expiresIn: process.env.ACCESS_TOKEN_VALIDITY, //1 Day
|
||||||
|
}),
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deletePasswordlessVerificationToken(
|
||||||
|
passwordlessTokens: PasswordlessToken,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const deletedPasswordlessToken =
|
||||||
|
await this.prismaService.passwordlessVerification.delete({
|
||||||
|
where: {
|
||||||
|
passwordless_deviceIdentifier_tokens: {
|
||||||
|
deviceIdentifier: passwordlessTokens.deviceIdentifier,
|
||||||
|
token: passwordlessTokens.token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return E.right(deletedPasswordlessToken);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(PASSWORDLESS_DATA_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkIfProviderAccountExists(user: User, profile) {
|
||||||
|
const provider: ProviderAccount =
|
||||||
|
await this.prismaService.account.findUnique({
|
||||||
|
where: {
|
||||||
|
verifyProviderAccount: {
|
||||||
|
provider: profile.provider,
|
||||||
|
providerAccountId: profile.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!provider) return O.none;
|
||||||
|
|
||||||
|
return O.some(provider);
|
||||||
|
}
|
||||||
|
|
||||||
async signIn(email: string) {
|
async signIn(email: string) {
|
||||||
if (!validateEmail(email)) return E.left(INVALID_EMAIL);
|
if (!validateEmail(email))
|
||||||
|
throw new HttpException(INVALID_EMAIL, HttpStatus.BAD_REQUEST);
|
||||||
|
|
||||||
let user: User;
|
let user: User;
|
||||||
const queriedUser = await this.usersService.findUserByEmail(email);
|
const queriedUser = await this.usersService.findUserByEmail(email);
|
||||||
|
|
||||||
if (O.isNone(queriedUser)) {
|
if (O.isNone(queriedUser)) {
|
||||||
user = await this.usersService.createUser(email);
|
user = await this.usersService.createUserMagic(email);
|
||||||
} else {
|
} else {
|
||||||
user = queriedUser.value;
|
user = queriedUser.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatedTokens = await this.generatePasswordlessTokens(user);
|
const generatedTokens = await this.generatePasswordlessTokens(user);
|
||||||
|
|
||||||
this.mailerService.sendMail(email, {
|
await this.mailerService.sendAuthEmail(email, {
|
||||||
template: 'code-your-own',
|
template: 'code-your-own',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: email,
|
inviteeEmail: email,
|
||||||
@@ -65,4 +191,29 @@ export class AuthService {
|
|||||||
|
|
||||||
return { deviceIdentifier: generatedTokens.deviceIdentifier };
|
return { deviceIdentifier: generatedTokens.deviceIdentifier };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async verify(data: verifyMagicDto) {
|
||||||
|
const passwordlessTokens = await this.validatePasswordlessTokens(data);
|
||||||
|
if (O.isNone(passwordlessTokens))
|
||||||
|
throw new HttpException(INVALID_MAGIC_LINK_DATA, HttpStatus.NOT_FOUND);
|
||||||
|
|
||||||
|
const currentTime = DateTime.now().toISOTime();
|
||||||
|
|
||||||
|
if (currentTime > passwordlessTokens.value.expiresOn.toISOString()) {
|
||||||
|
throw new HttpException(MAGIC_LINK_EXPIRED, HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
const tokens = await this.generateAuthTokens(
|
||||||
|
passwordlessTokens.value.userUid,
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletedPasswordlessToken =
|
||||||
|
await this.deletePasswordlessVerificationToken(passwordlessTokens.value);
|
||||||
|
if (E.isLeft(deletedPasswordlessToken))
|
||||||
|
throw new HttpException(
|
||||||
|
deletedPasswordlessToken.left,
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export class verifyMagicDto {
|
export class verifyMagicDto {
|
||||||
identifier: string;
|
deviceIdentifier: string;
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
ForbiddenException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AccessTokenPayload } from 'src/types/AuthTokens';
|
||||||
|
import { UserService } from 'src/user/user.service';
|
||||||
|
import { AuthService } from '../auth.service';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import * as O from 'fp-ts/Option';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
|
constructor(
|
||||||
|
private usersService: UserService,
|
||||||
|
private authService: AuthService,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||||
|
(request: Request) => {
|
||||||
|
const ATCookie = request.cookies['access_token'];
|
||||||
|
if (!ATCookie) {
|
||||||
|
throw new ForbiddenException('No cookies present');
|
||||||
|
}
|
||||||
|
return ATCookie;
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
secretOrKey: process.env.JWT_SECRET,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: AccessTokenPayload) {
|
||||||
|
if (!payload) throw new ForbiddenException('Access token malformed');
|
||||||
|
|
||||||
|
const user = await this.usersService.findUserById(payload.sub);
|
||||||
|
|
||||||
|
if (O.isNone(user)) {
|
||||||
|
throw new UnauthorizedException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = {
|
||||||
|
provider: 'email',
|
||||||
|
id: user.value.email,
|
||||||
|
};
|
||||||
|
|
||||||
|
const providerAccountExists =
|
||||||
|
await this.authService.checkIfProviderAccountExists(user.value, profile);
|
||||||
|
|
||||||
|
if (!providerAccountExists)
|
||||||
|
await this.usersService.createProviderAccount(
|
||||||
|
user.value,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
profile,
|
||||||
|
);
|
||||||
|
|
||||||
|
return user.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
import { User } from '../user/user.model';
|
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
|
||||||
export const GqlUser = createParamDecorator<any, any, User>(
|
export const GqlUser = createParamDecorator(
|
||||||
(_data: any, context: ExecutionContext) => {
|
(data: unknown, context: ExecutionContext) => {
|
||||||
const { user } = GqlExecutionContext.create(context).getContext<{
|
const ctx = GqlExecutionContext.create(context);
|
||||||
user: User;
|
return ctx.getContext().req.user;
|
||||||
}>();
|
|
||||||
if (!user)
|
|
||||||
throw new Error(
|
|
||||||
'@GqlUser decorator use with null user. Make sure the resolve has the @GqlAuthGuard present.',
|
|
||||||
);
|
|
||||||
|
|
||||||
return user;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -209,3 +209,28 @@ export const BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES =
|
|||||||
*/
|
*/
|
||||||
export const BUG_TEAM_ENV_GUARD_NO_ENV_ID =
|
export const BUG_TEAM_ENV_GUARD_NO_ENV_ID =
|
||||||
'bug/team_env/guard_no_env_id' as const;
|
'bug/team_env/guard_no_env_id' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data sent to the verify route are invalid
|
||||||
|
* (AuthService)
|
||||||
|
*/
|
||||||
|
export const INVALID_MAGIC_LINK_DATA = 'auth/magic_link_invalid_data' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Could not find PasswordlessVerification entry in the db
|
||||||
|
* (AuthService)
|
||||||
|
*/
|
||||||
|
export const PASSWORDLESS_DATA_NOT_FOUND =
|
||||||
|
'auth/passwordless_token_data_not_found' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth Tokens expired
|
||||||
|
* (AuthService)
|
||||||
|
*/
|
||||||
|
export const TOKEN_EXPIRED = 'auth/token_expired' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PasswordlessVerification Tokens expired i.e. magic-link expired
|
||||||
|
* (AuthService)
|
||||||
|
*/
|
||||||
|
export const MAGIC_LINK_EXPIRED = 'auth/magic_link_expired' as const;
|
||||||
|
|||||||
@@ -1,44 +1,11 @@
|
|||||||
import { CanActivate, Injectable, ExecutionContext } from '@nestjs/common';
|
import { Injectable, ExecutionContext } from '@nestjs/common';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import { User } from '../user/user.model';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { IncomingHttpHeaders } from 'http2';
|
|
||||||
import { AUTH_FAIL } from 'src/errors';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GqlAuthGuard implements CanActivate {
|
export class GqlAuthGuard extends AuthGuard('jwt') {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
getRequest(context: ExecutionContext) {
|
||||||
constructor() {}
|
const ctx = GqlExecutionContext.create(context);
|
||||||
|
return ctx.getContext().req;
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const ctx = GqlExecutionContext.create(context).getContext<{
|
|
||||||
reqHeaders: IncomingHttpHeaders;
|
|
||||||
user: User | null;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
if (
|
|
||||||
ctx.reqHeaders.authorization &&
|
|
||||||
ctx.reqHeaders.authorization.startsWith('Bearer ')
|
|
||||||
) {
|
|
||||||
const idToken = ctx.reqHeaders.authorization.split(' ')[1];
|
|
||||||
|
|
||||||
const authUser: User = {
|
|
||||||
id: 'aabb22ccdd',
|
|
||||||
name: 'exampleUser',
|
|
||||||
image: 'http://example.com/avatar',
|
|
||||||
email: 'me@example.com',
|
|
||||||
isAdmin: false,
|
|
||||||
createdOn: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.user = authUser;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(AUTH_FAIL);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,20 @@ export class MailerService implements OnModuleInit {
|
|||||||
() => EMAIL_FAILED,
|
() => EMAIL_FAILED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendAuthEmail(to: string, mailDesc: UserMagicLinkMailDescription) {
|
||||||
|
try {
|
||||||
|
const res = await this.client.sendEmailWithTemplate({
|
||||||
|
To: to,
|
||||||
|
From:
|
||||||
|
process.env.POSTMARK_SENDER_EMAIL ||
|
||||||
|
throwErr('No Postmark Sender Email defined'),
|
||||||
|
TemplateAlias: mailDesc.template,
|
||||||
|
TemplateModel: mailDesc.variables,
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
return throwErr(EMAIL_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { json } from 'express';
|
import { json } from 'express';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||||
@@ -28,6 +29,7 @@ async function bootstrap() {
|
|||||||
origin: true,
|
origin: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
app.use(cookieParser());
|
||||||
await app.listen(process.env.PORT || 3170);
|
await app.listen(process.env.PORT || 3170);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
19
packages/hoppscotch-backend/src/types/AuthTokens.ts
Normal file
19
packages/hoppscotch-backend/src/types/AuthTokens.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// refer to https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-claims#registered-claims
|
||||||
|
export interface AccessTokenPayload {
|
||||||
|
iss: string; // iss:issuer
|
||||||
|
sub: string; // sub:subject
|
||||||
|
aud: [string]; // aud:audience
|
||||||
|
iat?: number; // iat:issued at time
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshTokenPayload {
|
||||||
|
iss: string;
|
||||||
|
sub: string;
|
||||||
|
aud: [string];
|
||||||
|
iat?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthTokens {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
}
|
||||||
13
packages/hoppscotch-backend/src/types/ProviderAccount.ts
Normal file
13
packages/hoppscotch-backend/src/types/ProviderAccount.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { User } from 'src/user/user.model';
|
||||||
|
|
||||||
|
export interface ProviderAccount {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
user?: User;
|
||||||
|
provider: string;
|
||||||
|
providerAccountId: string;
|
||||||
|
providerRefreshToken?: string;
|
||||||
|
providerAccessToken?: string;
|
||||||
|
providerScope?: string;
|
||||||
|
loggedIn: Date;
|
||||||
|
}
|
||||||
@@ -20,8 +20,21 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUser(email: string) {
|
async findUserById(userUid: string) {
|
||||||
const createdUser = await this.prisma.user.create({
|
try {
|
||||||
|
const user: User = await this.prisma.user.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: userUid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return O.some(user);
|
||||||
|
} catch (error) {
|
||||||
|
return O.none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUserMagic(email: string) {
|
||||||
|
const createdUser: User = await this.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email: email,
|
email: email,
|
||||||
accounts: {
|
accounts: {
|
||||||
|
|||||||
@@ -116,6 +116,6 @@ export const taskEitherValidateArraySeq = <A, B>(
|
|||||||
*/
|
*/
|
||||||
export const validateEmail = (email: string) => {
|
export const validateEmail = (email: string) => {
|
||||||
return new RegExp(
|
return new RegExp(
|
||||||
"([!#-'*+/-9=?A-Z^-~-]+(.[!#-'*+/-9=?A-Z^-~-]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([!#-'*+/-9=?A-Z^-~-]+(.[!#-'*+/-9=?A-Z^-~-]+)*|[[\t -Z^-~]*])",
|
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
||||||
).test(email);
|
).test(email);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user