* feat: wip upload module * feat: local storage and serve local images * feat: protect against injections * feat: server local and s3 files * fix: use storage location when serving local files * feat: cross field env validation
207 lines
5.5 KiB
TypeScript
207 lines
5.5 KiB
TypeScript
import {
|
|
ForbiddenException,
|
|
Injectable,
|
|
InternalServerErrorException,
|
|
NotFoundException,
|
|
UnauthorizedException,
|
|
UnprocessableEntityException,
|
|
} from '@nestjs/common';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { JwtPayload } from '../strategies/jwt.auth.strategy';
|
|
import { PrismaService } from 'src/database/prisma.service';
|
|
import { assert } from 'src/utils/assert';
|
|
import { addMilliseconds } from 'date-fns';
|
|
import ms from 'ms';
|
|
import { AuthToken } from '../dto/token.entity';
|
|
import { TokenExpiredError } from 'jsonwebtoken';
|
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
|
|
|
@Injectable()
|
|
export class TokenService {
|
|
constructor(
|
|
private readonly jwtService: JwtService,
|
|
private readonly environmentService: EnvironmentService,
|
|
private readonly prismaService: PrismaService,
|
|
) {}
|
|
|
|
async generateAccessToken(userId: string): Promise<AuthToken> {
|
|
const expiresIn = this.environmentService.getAccessTokenExpiresIn();
|
|
assert(expiresIn, '', InternalServerErrorException);
|
|
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
|
|
|
const user = await this.prismaService.user.findUnique({
|
|
where: { id: userId },
|
|
include: {
|
|
workspaceMember: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
throw new NotFoundException('User is not found');
|
|
}
|
|
|
|
if (!user.workspaceMember) {
|
|
throw new ForbiddenException('User is not associated to a workspace');
|
|
}
|
|
|
|
const jwtPayload: JwtPayload = {
|
|
sub: user.id,
|
|
workspaceId: user.workspaceMember.workspaceId,
|
|
};
|
|
|
|
return {
|
|
token: this.jwtService.sign(jwtPayload),
|
|
expiresAt,
|
|
};
|
|
}
|
|
|
|
async generateRefreshToken(userId: string): Promise<AuthToken> {
|
|
const secret = this.environmentService.getRefreshTokenSecret();
|
|
const expiresIn = this.environmentService.getRefreshTokenExpiresIn();
|
|
assert(expiresIn, '', InternalServerErrorException);
|
|
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
|
|
|
const refreshTokenPayload = {
|
|
userId,
|
|
expiresAt,
|
|
};
|
|
const jwtPayload = {
|
|
sub: userId,
|
|
};
|
|
|
|
const refreshToken = await this.prismaService.refreshToken.create({
|
|
data: refreshTokenPayload,
|
|
});
|
|
|
|
return {
|
|
token: this.jwtService.sign(jwtPayload, {
|
|
secret,
|
|
expiresIn,
|
|
// Jwtid will be used to link RefreshToken entity to this token
|
|
jwtid: refreshToken.id,
|
|
}),
|
|
expiresAt,
|
|
};
|
|
}
|
|
|
|
async generateLoginToken(email: string): Promise<AuthToken> {
|
|
const secret = this.environmentService.getLoginTokenSecret();
|
|
const expiresIn = this.environmentService.getLoginTokenExpiresIn();
|
|
assert(expiresIn, '', InternalServerErrorException);
|
|
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
|
const jwtPayload = {
|
|
sub: email,
|
|
};
|
|
|
|
return {
|
|
token: this.jwtService.sign(jwtPayload, {
|
|
secret,
|
|
expiresIn,
|
|
}),
|
|
expiresAt,
|
|
};
|
|
}
|
|
|
|
async verifyLoginToken(loginToken: string): Promise<string> {
|
|
const loginTokenSecret = this.environmentService.getLoginTokenSecret();
|
|
|
|
const payload = await this.verifyJwt(loginToken, loginTokenSecret);
|
|
|
|
return payload.sub;
|
|
}
|
|
|
|
async verifyRefreshToken(refreshToken: string) {
|
|
const secret = this.environmentService.getRefreshTokenSecret();
|
|
const jwtPayload = await this.verifyJwt(refreshToken, secret);
|
|
|
|
assert(
|
|
jwtPayload.jti && jwtPayload.sub,
|
|
'This refresh token is malformed',
|
|
UnprocessableEntityException,
|
|
);
|
|
|
|
const token = await this.prismaService.refreshToken.findUnique({
|
|
where: { id: jwtPayload.jti },
|
|
});
|
|
|
|
assert(token, "This refresh token doesn't exist", NotFoundException);
|
|
|
|
const user = await this.prismaService.user.findUnique({
|
|
where: {
|
|
id: jwtPayload.sub,
|
|
},
|
|
include: {
|
|
refreshTokens: true,
|
|
},
|
|
});
|
|
|
|
assert(user, 'User not found', NotFoundException);
|
|
|
|
if (token.isRevoked) {
|
|
// Revoke all user refresh tokens
|
|
await this.prismaService.refreshToken.updateMany({
|
|
where: {
|
|
id: {
|
|
in: user.refreshTokens.map(({ id }) => id),
|
|
},
|
|
},
|
|
data: {
|
|
isRevoked: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
assert(
|
|
!token.isRevoked,
|
|
'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.',
|
|
ForbiddenException,
|
|
);
|
|
|
|
return { user, token };
|
|
}
|
|
|
|
async generateTokensFromRefreshToken(token: string): Promise<{
|
|
accessToken: AuthToken;
|
|
refreshToken: AuthToken;
|
|
}> {
|
|
const {
|
|
user,
|
|
token: { id },
|
|
} = await this.verifyRefreshToken(token);
|
|
|
|
// Revoke old refresh token
|
|
await this.prismaService.refreshToken.update({
|
|
where: {
|
|
id,
|
|
},
|
|
data: {
|
|
isRevoked: true,
|
|
},
|
|
});
|
|
|
|
const accessToken = await this.generateAccessToken(user.id);
|
|
const refreshToken = await this.generateRefreshToken(user.id);
|
|
|
|
return {
|
|
accessToken,
|
|
refreshToken,
|
|
};
|
|
}
|
|
|
|
computeRedirectURI(loginToken: string): string {
|
|
return `${this.environmentService.getFrontAuthCallbackUrl()}?loginToken=${loginToken}`;
|
|
}
|
|
|
|
async verifyJwt(token: string, secret?: string) {
|
|
try {
|
|
return this.jwtService.verify(token, secret ? { secret } : undefined);
|
|
} catch (error) {
|
|
if (error instanceof TokenExpiredError) {
|
|
throw new UnauthorizedException('Token has expired.');
|
|
} else {
|
|
throw new UnprocessableEntityException();
|
|
}
|
|
}
|
|
}
|
|
}
|