mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-19 03:01:10 +10:00
Refactoring
* Refactor workspace membership system * Create setup endpoint * Use Passport.js * Several updates and fixes
This commit is contained in:
@ -1,27 +1,47 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Post,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { CreateUserDto } from '../user/dto/create-user.dto';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { SetupGuard } from './guards/setup.guard';
|
||||
import { EnvironmentService } from '../../environment/environment.service';
|
||||
import { CreateAdminUserDto } from './dto/create-admin-user.dto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('login')
|
||||
async login(@Body() loginInput: LoginDto) {
|
||||
return await this.authService.login(loginInput);
|
||||
async login(@Req() req, @Body() loginInput: LoginDto) {
|
||||
return this.authService.login(loginInput, req.raw.workspaceId);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('register')
|
||||
async register(@Body() createUserDto: CreateUserDto) {
|
||||
return await this.authService.register(createUserDto);
|
||||
async register(@Req() req, @Body() createUserDto: CreateUserDto) {
|
||||
return this.authService.register(createUserDto, req.raw.workspaceId);
|
||||
}
|
||||
|
||||
@UseGuards(SetupGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('setup')
|
||||
async setupWorkspace(
|
||||
@Req() req,
|
||||
@Body() createAdminUserDto: CreateAdminUserDto,
|
||||
) {
|
||||
if (this.environmentService.isCloud()) throw new NotFoundException();
|
||||
return this.authService.setup(createAdminUserDto);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,13 +4,17 @@ import { AuthService } from './services/auth.service';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { EnvironmentService } from '../../environment/environment.service';
|
||||
import { TokenService } from './services/token.service';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
import { SignupService } from './services/signup.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { SpaceModule } from '../space/space.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.registerAsync({
|
||||
useFactory: async (environmentService: EnvironmentService) => {
|
||||
return {
|
||||
global: true,
|
||||
secret: environmentService.getJwtSecret(),
|
||||
signOptions: {
|
||||
expiresIn: environmentService.getJwtTokenExpiresIn(),
|
||||
@ -18,10 +22,13 @@ import { TokenService } from './services/token.service';
|
||||
};
|
||||
},
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
} as any),
|
||||
UserModule,
|
||||
WorkspaceModule,
|
||||
SpaceModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, TokenService],
|
||||
providers: [AuthService, SignupService, TokenService, JwtStrategy],
|
||||
exports: [TokenService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
8
apps/server/src/core/auth/auth.utils.ts
Normal file
8
apps/server/src/core/auth/auth.utils.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
export async function comparePasswordHash(
|
||||
plainPassword: string,
|
||||
passwordHash: string,
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(plainPassword, passwordHash);
|
||||
}
|
||||
15
apps/server/src/core/auth/dto/create-admin-user.dto.ts
Normal file
15
apps/server/src/core/auth/dto/create-admin-user.dto.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
|
||||
export class CreateAdminUserDto extends CreateUserDto {
|
||||
@IsNotEmpty()
|
||||
@MinLength(3)
|
||||
@MaxLength(35)
|
||||
name: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@MinLength(4)
|
||||
@MaxLength(35)
|
||||
@IsString()
|
||||
workspaceName: string;
|
||||
}
|
||||
24
apps/server/src/core/auth/dto/create-user.dto.ts
Normal file
24
apps/server/src/core/auth/dto/create-user.dto.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import {
|
||||
IsEmail,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString, MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsOptional()
|
||||
@MinLength(3)
|
||||
@MaxLength(35)
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@MinLength(8)
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
16
apps/server/src/core/auth/dto/jwt-payload.ts
Normal file
16
apps/server/src/core/auth/dto/jwt-payload.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export enum JwtType {
|
||||
ACCESS = 'access',
|
||||
REFRESH = 'refresh',
|
||||
}
|
||||
export type JwtPayload = {
|
||||
sub: string;
|
||||
email: string;
|
||||
workspaceId: string;
|
||||
type: 'access';
|
||||
};
|
||||
|
||||
export type JwtRefreshPayload = {
|
||||
sub: string;
|
||||
workspaceId: string;
|
||||
type: 'refresh';
|
||||
};
|
||||
@ -1,55 +0,0 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { TokenService } from '../services/token.service';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { IS_PUBLIC_KEY } from '../../../decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class JwtGuard implements CanActivate {
|
||||
constructor(
|
||||
private tokenService: TokenService,
|
||||
private userService: UserService,
|
||||
private reflector: Reflector,
|
||||
) {}
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token: string = await this.tokenService.extractTokenFromHeader(
|
||||
request,
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('Invalid jwt token');
|
||||
}
|
||||
|
||||
let payload;
|
||||
|
||||
try {
|
||||
payload = await this.tokenService.verifyJwt(token);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Could not verify jwt token');
|
||||
}
|
||||
|
||||
try {
|
||||
//fetch user and current workspace data from db
|
||||
request['user'] = await this.userService.getUserInstance(payload.sub);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Failed to fetch auth user');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
14
apps/server/src/core/auth/guards/setup.guard.ts
Normal file
14
apps/server/src/core/auth/guards/setup.guard.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { CanActivate, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { WorkspaceRepository } from '../../workspace/repositories/workspace.repository';
|
||||
|
||||
@Injectable()
|
||||
export class SetupGuard implements CanActivate {
|
||||
constructor(private workspaceRepository: WorkspaceRepository) {}
|
||||
async canActivate(): Promise<boolean> {
|
||||
const workspaceCount = await this.workspaceRepository.count();
|
||||
if (workspaceCount > 0) {
|
||||
throw new ForbiddenException('Workspace setup already completed.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,38 +1,57 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { LoginDto } from '../dto/login.dto';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CreateUserDto } from '../../user/dto/create-user.dto';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { TokenService } from './token.service';
|
||||
import { TokensDto } from '../dto/tokens.dto';
|
||||
import { UserRepository } from '../../user/repositories/user.repository';
|
||||
import { comparePasswordHash } from '../auth.utils';
|
||||
import { SignupService } from './signup.service';
|
||||
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private signupService: SignupService,
|
||||
private tokenService: TokenService,
|
||||
private userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
async login(loginDto: LoginDto) {
|
||||
const user: User = await this.userService.findByEmail(loginDto.email);
|
||||
const invalidCredentialsMessage = 'email or password does not match';
|
||||
async login(loginDto: LoginDto, workspaceId: string) {
|
||||
const user = await this.userRepository.findOneByEmail(
|
||||
loginDto.email,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (
|
||||
!user ||
|
||||
!(await this.userService.compareHash(loginDto.password, user.password))
|
||||
!(await comparePasswordHash(loginDto.password, user.password))
|
||||
) {
|
||||
throw new UnauthorizedException(invalidCredentialsMessage);
|
||||
throw new UnauthorizedException('email or password does not match');
|
||||
}
|
||||
|
||||
user.lastLoginAt = new Date();
|
||||
await this.userRepository.save(user);
|
||||
|
||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||
return { tokens };
|
||||
}
|
||||
|
||||
async register(createUserDto: CreateUserDto, workspaceId: string) {
|
||||
const user: User = await this.signupService.signup(
|
||||
createUserDto,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||
|
||||
return { tokens };
|
||||
}
|
||||
|
||||
async register(createUserDto: CreateUserDto) {
|
||||
const user: User = await this.userService.create(createUserDto);
|
||||
async setup(createAdminUserDto: CreateAdminUserDto) {
|
||||
const user: User = await this.signupService.firstSetup(createAdminUserDto);
|
||||
|
||||
const tokens: TokensDto = await this.tokenService.generateTokens(user);
|
||||
|
||||
|
||||
121
apps/server/src/core/auth/services/signup.service.ts
Normal file
121
apps/server/src/core/auth/services/signup.service.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { transactionWrapper } from '../../../helpers/db.helper';
|
||||
import { UserRepository } from '../../user/repositories/user.repository';
|
||||
import { WorkspaceRepository } from '../../workspace/repositories/workspace.repository';
|
||||
import { WorkspaceService } from '../../workspace/services/workspace.service';
|
||||
import { CreateWorkspaceDto } from '../../workspace/dto/create-workspace.dto';
|
||||
import { Workspace } from '../../workspace/entities/workspace.entity';
|
||||
import { SpaceService } from '../../space/space.service';
|
||||
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class SignupService {
|
||||
constructor(
|
||||
private userRepository: UserRepository,
|
||||
private workspaceRepository: WorkspaceRepository,
|
||||
private workspaceService: WorkspaceService,
|
||||
private spaceService: SpaceService,
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
prepareUser(createUserDto: CreateUserDto): User {
|
||||
const user = new User();
|
||||
user.name = createUserDto.name || createUserDto.email.split('@')[0];
|
||||
user.email = createUserDto.email.toLowerCase();
|
||||
user.password = createUserDto.password;
|
||||
user.locale = 'en';
|
||||
user.lastLoginAt = new Date();
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUser(
|
||||
createUserDto: CreateUserDto,
|
||||
manager?: EntityManager,
|
||||
): Promise<User> {
|
||||
return await transactionWrapper(
|
||||
async (transactionManager: EntityManager) => {
|
||||
let user = this.prepareUser(createUserDto);
|
||||
user = await transactionManager.save(user);
|
||||
|
||||
return user;
|
||||
},
|
||||
this.dataSource,
|
||||
manager,
|
||||
);
|
||||
}
|
||||
|
||||
async signup(
|
||||
createUserDto: CreateUserDto,
|
||||
workspaceId: string,
|
||||
manager?: EntityManager,
|
||||
): Promise<User> {
|
||||
const userCheck = await this.userRepository.findOneByEmail(
|
||||
createUserDto.email,
|
||||
workspaceId,
|
||||
);
|
||||
if (userCheck) {
|
||||
throw new BadRequestException('You have an account on this workspace');
|
||||
}
|
||||
|
||||
return await transactionWrapper(
|
||||
async (manager: EntityManager) => {
|
||||
// create user
|
||||
const user = await this.createUser(createUserDto, manager);
|
||||
|
||||
// add user to workspace
|
||||
await this.workspaceService.addUserToWorkspace(
|
||||
user,
|
||||
workspaceId,
|
||||
undefined,
|
||||
manager,
|
||||
);
|
||||
return user;
|
||||
},
|
||||
this.dataSource,
|
||||
manager,
|
||||
);
|
||||
}
|
||||
|
||||
async createWorkspace(
|
||||
user: User,
|
||||
workspaceName,
|
||||
manager?: EntityManager,
|
||||
): Promise<Workspace> {
|
||||
return await transactionWrapper(
|
||||
async (manager: EntityManager) => {
|
||||
// for cloud
|
||||
const workspaceData: CreateWorkspaceDto = {
|
||||
name: workspaceName,
|
||||
// hostname: '', // generate
|
||||
};
|
||||
|
||||
return await this.workspaceService.create(user, workspaceData, manager);
|
||||
},
|
||||
this.dataSource,
|
||||
manager,
|
||||
);
|
||||
}
|
||||
|
||||
async firstSetup(
|
||||
createAdminUserDto: CreateAdminUserDto,
|
||||
manager?: EntityManager,
|
||||
): Promise<User> {
|
||||
return await transactionWrapper(
|
||||
async (manager: EntityManager) => {
|
||||
// create user
|
||||
const user = await this.createUser(createAdminUserDto, manager);
|
||||
await this.createWorkspace(
|
||||
user,
|
||||
createAdminUserDto.workspaceName,
|
||||
manager,
|
||||
);
|
||||
return user;
|
||||
},
|
||||
this.dataSource,
|
||||
manager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { EnvironmentService } from '../../../environment/environment.service';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { TokensDto } from '../dto/tokens.dto';
|
||||
|
||||
export type JwtPayload = { sub: string; email: string };
|
||||
import { JwtPayload, JwtRefreshPayload, JwtType } from '../dto/jwt-payload';
|
||||
|
||||
@Injectable()
|
||||
export class TokenService {
|
||||
@ -13,31 +11,37 @@ export class TokenService {
|
||||
private jwtService: JwtService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
async generateJwt(user: User): Promise<string> {
|
||||
|
||||
async generateAccessToken(user: User): Promise<string> {
|
||||
const payload: JwtPayload = {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
workspaceId: user.workspaceId,
|
||||
type: JwtType.ACCESS,
|
||||
};
|
||||
return await this.jwtService.signAsync(payload);
|
||||
return this.jwtService.sign(payload);
|
||||
}
|
||||
|
||||
async generateRefreshToken(userId: string, workspaceId): Promise<string> {
|
||||
const payload: JwtRefreshPayload = {
|
||||
sub: userId,
|
||||
workspaceId,
|
||||
type: JwtType.REFRESH,
|
||||
};
|
||||
const expiresIn = '30d'; // todo: fix
|
||||
return this.jwtService.sign(payload, { expiresIn });
|
||||
}
|
||||
|
||||
async generateTokens(user: User): Promise<TokensDto> {
|
||||
return {
|
||||
accessToken: await this.generateJwt(user),
|
||||
refreshToken: null,
|
||||
accessToken: await this.generateAccessToken(user),
|
||||
refreshToken: await this.generateRefreshToken(user.id, user.workspaceId),
|
||||
};
|
||||
}
|
||||
|
||||
async verifyJwt(token: string) {
|
||||
return await this.jwtService.verifyAsync(token, {
|
||||
return this.jwtService.verifyAsync(token, {
|
||||
secret: this.environmentService.getJwtSecret(),
|
||||
});
|
||||
}
|
||||
|
||||
async extractTokenFromHeader(
|
||||
request: FastifyRequest,
|
||||
): Promise<string | undefined> {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
64
apps/server/src/core/auth/strategies/jwt.strategy.ts
Normal file
64
apps/server/src/core/auth/strategies/jwt.strategy.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { EnvironmentService } from '../../../environment/environment.service';
|
||||
import { JwtPayload, JwtType } from '../dto/jwt-payload';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { UserRepository } from '../../user/repositories/user.repository';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { WorkspaceRepository } from '../../workspace/repositories/workspace.repository';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private userService: UserService,
|
||||
private userRepository: UserRepository,
|
||||
private workspaceRepository: WorkspaceRepository,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: environmentService.getJwtSecret(),
|
||||
passReqToCallback: true,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(req, payload: JwtPayload) {
|
||||
// CLOUD ENV
|
||||
if (this.environmentService.isCloud()) {
|
||||
if (req.raw.workspaceId && req.raw.workspaceId !== payload.workspaceId) {
|
||||
throw new BadRequestException('Workspace does not match');
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload.workspaceId || payload.type !== JwtType.ACCESS) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findById(
|
||||
payload.workspaceId,
|
||||
);
|
||||
|
||||
if (!workspace) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id: payload.sub,
|
||||
workspaceId: payload.workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return { user, workspace };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user