feat: rewrite auth (#364)
* feat: wip rewrite auth * feat: restructure folders and fix stories and tests * feat: remove auth provider and fix tests
This commit is contained in:
@ -5,13 +5,12 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { GoogleAuthController } from './controllers/google-auth.controller';
|
||||
import { GoogleStrategy } from './strategies/google.auth.strategy';
|
||||
import { TokenController } from './controllers/token.controller';
|
||||
import { PrismaService } from 'src/database/prisma.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { AuthController } from './controllers/auth.controller';
|
||||
import { PasswordAuthController } from './controllers/password-auth.controller';
|
||||
import { VerifyAuthController } from './controllers/verify-auth.controller';
|
||||
|
||||
import { TokenService } from './services/token.service';
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
const jwtModule = JwtModule.registerAsync({
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
@ -28,18 +27,14 @@ const jwtModule = JwtModule.registerAsync({
|
||||
|
||||
@Module({
|
||||
imports: [jwtModule, ConfigModule.forRoot({}), UserModule],
|
||||
controllers: [
|
||||
GoogleAuthController,
|
||||
PasswordAuthController,
|
||||
TokenController,
|
||||
AuthController,
|
||||
],
|
||||
controllers: [GoogleAuthController, VerifyAuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
TokenService,
|
||||
JwtAuthStrategy,
|
||||
GoogleStrategy,
|
||||
PrismaService,
|
||||
AuthResolver,
|
||||
],
|
||||
exports: [jwtModule],
|
||||
})
|
||||
|
||||
30
server/src/core/auth/auth.resolver.spec.ts
Normal file
30
server/src/core/auth/auth.resolver.spec.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
import { TokenService } from './services/token.service';
|
||||
import { AuthService } from './services/auth.service';
|
||||
|
||||
describe('AuthResolver', () => {
|
||||
let resolver: AuthResolver;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AuthResolver,
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TokenService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
resolver = module.get<AuthResolver>(AuthResolver);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(resolver).toBeDefined();
|
||||
});
|
||||
});
|
||||
49
server/src/core/auth/auth.resolver.ts
Normal file
49
server/src/core/auth/auth.resolver.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import { AuthTokens } from './dto/token.entity';
|
||||
import { TokenService } from './services/token.service';
|
||||
import { RefreshTokenInput } from './dto/refresh-token.input';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Verify } from './dto/verify.entity';
|
||||
import { VerifyInput } from './dto/verify.input';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { LoginToken } from './dto/login-token.entity';
|
||||
import { ChallengeInput } from './dto/challenge.input';
|
||||
|
||||
@Resolver()
|
||||
export class AuthResolver {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private tokenService: TokenService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => LoginToken)
|
||||
async challenge(@Args() challengeInput: ChallengeInput): Promise<LoginToken> {
|
||||
const user = await this.authService.challenge(challengeInput);
|
||||
const loginToken = await this.tokenService.generateLoginToken(user.email);
|
||||
|
||||
return { loginToken };
|
||||
}
|
||||
|
||||
@Mutation(() => Verify)
|
||||
async verify(@Args() verifyInput: VerifyInput): Promise<Verify> {
|
||||
const email = await this.tokenService.verifyLoginToken(
|
||||
verifyInput.loginToken,
|
||||
);
|
||||
const result = await this.authService.verify(email);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Mutation(() => AuthTokens)
|
||||
async renewToken(@Args() args: RefreshTokenInput): Promise<AuthTokens> {
|
||||
if (!args.refreshToken) {
|
||||
throw new BadRequestException('Refresh token is mendatory');
|
||||
}
|
||||
|
||||
const tokens = await this.tokenService.generateTokensFromRefreshToken(
|
||||
args.refreshToken,
|
||||
);
|
||||
|
||||
return { tokens: tokens };
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PasswordAuthController } from './password-auth.controller';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { TokenService } from '../services/token.service';
|
||||
|
||||
describe('PasswordAuthController', () => {
|
||||
let controller: PasswordAuthController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PasswordAuthController],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TokenService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PasswordAuthController>(PasswordAuthController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -1,23 +0,0 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ChallengeInput } from '../dto/challenge.input';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { LoginTokenEntity } from '../dto/login-token.entity';
|
||||
import { TokenService } from '../services/token.service';
|
||||
|
||||
@Controller('auth/password')
|
||||
export class PasswordAuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly tokenService: TokenService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
async challenge(
|
||||
@Body() challengeInput: ChallengeInput,
|
||||
): Promise<LoginTokenEntity> {
|
||||
const user = await this.authService.challenge(challengeInput);
|
||||
const loginToken = await this.tokenService.generateLoginToken(user.email);
|
||||
|
||||
return { loginToken };
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
import { BadRequestException, Body, Controller, Post } from '@nestjs/common';
|
||||
import { RefreshTokenInput } from '../dto/refresh-token.input';
|
||||
import { TokenService } from '../services/token.service';
|
||||
|
||||
@Controller('auth/token')
|
||||
export class TokenController {
|
||||
constructor(private tokenService: TokenService) {}
|
||||
|
||||
@Post()
|
||||
async generateAccessToken(@Body() body: RefreshTokenInput) {
|
||||
if (!body.refreshToken) {
|
||||
throw new BadRequestException('Refresh token is mendatory');
|
||||
}
|
||||
|
||||
const tokens = await this.tokenService.generateTokensFromRefreshToken(
|
||||
body.refreshToken,
|
||||
);
|
||||
|
||||
return { tokens: tokens };
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,14 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { VerifyAuthController } from './verify-auth.controller';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { TokenService } from '../services/token.service';
|
||||
|
||||
describe('AuthController', () => {
|
||||
let controller: AuthController;
|
||||
describe('VerifyAuthController', () => {
|
||||
let controller: VerifyAuthController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AuthController],
|
||||
controllers: [VerifyAuthController],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthService,
|
||||
@ -21,7 +21,7 @@ describe('AuthController', () => {
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AuthController>(AuthController);
|
||||
controller = module.get<VerifyAuthController>(VerifyAuthController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@ -1,18 +1,18 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { VerifyInput } from '../dto/verify.input';
|
||||
import { VerifyEntity } from '../dto/verify.entity';
|
||||
import { Verify } from '../dto/verify.entity';
|
||||
import { TokenService } from '../services/token.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@Controller('auth/verify')
|
||||
export class VerifyAuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly tokenService: TokenService,
|
||||
) {}
|
||||
|
||||
@Post('verify')
|
||||
async verify(@Body() verifyInput: VerifyInput): Promise<VerifyEntity> {
|
||||
@Post()
|
||||
async verify(@Body() verifyInput: VerifyInput): Promise<Verify> {
|
||||
const email = await this.tokenService.verifyLoginToken(
|
||||
verifyInput.loginToken,
|
||||
);
|
||||
@ -1,10 +1,14 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class ChallengeInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { TokenEntity } from './token.entity';
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { AuthToken } from './token.entity';
|
||||
|
||||
export class LoginTokenEntity {
|
||||
loginToken: TokenEntity;
|
||||
@ObjectType()
|
||||
export class LoginToken {
|
||||
@Field(() => AuthToken)
|
||||
loginToken: AuthToken;
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class RefreshTokenInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
refreshToken: string;
|
||||
|
||||
@ -6,18 +6,23 @@ import {
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { PASSWORD_REGEX } from '../auth.util';
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
@ArgsType()
|
||||
export class RegisterInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@Matches(PASSWORD_REGEX, { message: 'password too weak' })
|
||||
password: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
displayName: string;
|
||||
|
||||
@ -1,4 +1,25 @@
|
||||
export class TokenEntity {
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class AuthToken {
|
||||
@Field(() => String)
|
||||
token: string;
|
||||
|
||||
@Field(() => Date)
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AuthTokenPair {
|
||||
@Field(() => AuthToken)
|
||||
accessToken: AuthToken;
|
||||
|
||||
@Field(() => AuthToken)
|
||||
refreshToken: AuthToken;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AuthTokens {
|
||||
@Field(() => AuthTokenPair)
|
||||
tokens: AuthTokenPair;
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { TokenEntity } from './token.entity';
|
||||
import { User } from '@prisma/client';
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { AuthTokens } from './token.entity';
|
||||
import { User } from 'src/core/@generated/user/user.model';
|
||||
|
||||
export class VerifyEntity {
|
||||
user: Omit<User, 'passwordHash'>;
|
||||
|
||||
tokens: {
|
||||
accessToken: TokenEntity;
|
||||
refreshToken: TokenEntity;
|
||||
};
|
||||
@ObjectType()
|
||||
export class Verify extends AuthTokens {
|
||||
@Field(() => User)
|
||||
user: User;
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class VerifyInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
loginToken: string;
|
||||
|
||||
@ -9,7 +9,7 @@ import { UserService } from 'src/core/user/user.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { RegisterInput } from '../dto/register.input';
|
||||
import { PASSWORD_REGEX, compareHash, hashPassword } from '../auth.util';
|
||||
import { VerifyEntity } from '../dto/verify.entity';
|
||||
import { Verify } from '../dto/verify.entity';
|
||||
import { TokenService } from './token.service';
|
||||
|
||||
export type UserPayload = {
|
||||
@ -73,17 +73,17 @@ export class AuthService {
|
||||
return user;
|
||||
}
|
||||
|
||||
async verify(email: string): Promise<VerifyEntity> {
|
||||
const data = await this.userService.findUnique({
|
||||
async verify(email: string): Promise<Verify> {
|
||||
const user = await this.userService.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
assert(data, "This user doesn't exist", NotFoundException);
|
||||
assert(user, "This user doesn't exist", NotFoundException);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { passwordHash: _, ...user } = data;
|
||||
// passwordHash is hidden for security reasons
|
||||
user.passwordHash = '';
|
||||
|
||||
const accessToken = await this.tokenService.generateAccessToken(user.id);
|
||||
const refreshToken = await this.tokenService.generateRefreshToken(user.id);
|
||||
|
||||
@ -13,7 +13,7 @@ import { PrismaService } from 'src/database/prisma.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { TokenEntity } from '../dto/token.entity';
|
||||
import { AuthToken } from '../dto/token.entity';
|
||||
import { TokenExpiredError } from 'jsonwebtoken';
|
||||
|
||||
@Injectable()
|
||||
@ -24,7 +24,7 @@ export class TokenService {
|
||||
private readonly prismaService: PrismaService,
|
||||
) {}
|
||||
|
||||
async generateAccessToken(userId: string): Promise<TokenEntity> {
|
||||
async generateAccessToken(userId: string): Promise<AuthToken> {
|
||||
const expiresIn = this.configService.get<string>('ACCESS_TOKEN_EXPIRES_IN');
|
||||
assert(expiresIn, '', InternalServerErrorException);
|
||||
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||
@ -55,7 +55,7 @@ export class TokenService {
|
||||
};
|
||||
}
|
||||
|
||||
async generateRefreshToken(userId: string): Promise<TokenEntity> {
|
||||
async generateRefreshToken(userId: string): Promise<AuthToken> {
|
||||
const secret = this.configService.get('REFRESH_TOKEN_SECRET');
|
||||
const expiresIn = this.configService.get<string>(
|
||||
'REFRESH_TOKEN_EXPIRES_IN',
|
||||
@ -86,7 +86,7 @@ export class TokenService {
|
||||
};
|
||||
}
|
||||
|
||||
async generateLoginToken(email: string): Promise<TokenEntity> {
|
||||
async generateLoginToken(email: string): Promise<AuthToken> {
|
||||
const secret = this.configService.get('LOGIN_TOKEN_SECRET');
|
||||
const expiresIn = this.configService.get<string>('LOGIN_TOKEN_EXPIRES_IN');
|
||||
assert(expiresIn, '', InternalServerErrorException);
|
||||
@ -163,8 +163,8 @@ export class TokenService {
|
||||
}
|
||||
|
||||
async generateTokensFromRefreshToken(token: string): Promise<{
|
||||
accessToken: TokenEntity;
|
||||
refreshToken: TokenEntity;
|
||||
accessToken: AuthToken;
|
||||
refreshToken: AuthToken;
|
||||
}> {
|
||||
const {
|
||||
user,
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WorkspaceService } from './services/workspace.service';
|
||||
import { WorkspaceMemberService } from './services/workspace-member.service';
|
||||
import { WorkspaceMemberResolver } from './resolvers/workspace-member.resolver';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
WorkspaceService,
|
||||
WorkspaceMemberService,
|
||||
WorkspaceMemberResolver,
|
||||
],
|
||||
providers: [WorkspaceService, WorkspaceMemberService],
|
||||
exports: [WorkspaceService, WorkspaceMemberService],
|
||||
})
|
||||
export class WorkspaceModule {}
|
||||
|
||||
Reference in New Issue
Block a user