mirror of
https://github.com/docmost/docmost.git
synced 2025-11-24 08:21:14 +10:00
feat: cloud and ee (#805)
* stripe init git submodules for enterprise modules * * Cloud billing UI - WIP * Proxy websockets in dev mode * Separate workspace login and creation for cloud * Other fixes * feat: billing (cloud) * * add domain service * prepare links from workspace hostname * WIP * Add exchange token generation * Validate JWT token type during verification * domain service * add SkipTransform decorator * * updates (server) * add new packages * new sso migration file * WIP * Fix hostname generation * WIP * WIP * Reduce input error font-size * set max password length * jwt package * license page - WIP * * License management UI * Move license key store to db * add reflector * SSO enforcement * * Add default plan * Add usePlan hook * * Fix auth container margin in mobile * Redirect login and home to select page in cloud * update .gitignore * Default to yearly * * Trial messaging * Handle ended trials * Don't set to readonly on collab disconnect (Cloud) * Refine trial (UI) * Fix bug caused by using jotai optics atom in AppHeader component * configurable database maximum pool * Close SSO form on save * wip * sync * Only show sign-in in cloud * exclude base api part from workspaceId check * close db connection beforeApplicationShutdown * Add health/live endpoint * clear cookie on hostname change * reset currentUser atom * Change text * return 401 if workspace does not match * feat: show user workspace list in cloud login page * sync * Add home path * Prefetch to speed up queries * * Add robots.txt * Disallow login and forgot password routes * wildcard user-agent * Fix space query cache * fix * fix * use space uuid for recent pages * prefetch billing plans * enhance license page * sync
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
@ -24,6 +24,7 @@ import { PasswordResetDto } from './dto/password-reset.dto';
|
||||
import { VerifyUserTokenDto } from './dto/verify-user-token.dto';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { addDays } from 'date-fns';
|
||||
import { validateSsoEnforcement } from './auth.util';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@ -35,14 +36,13 @@ export class AuthController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('login')
|
||||
async login(
|
||||
@Req() req,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Res({ passthrough: true }) res: FastifyReply,
|
||||
@Body() loginInput: LoginDto,
|
||||
) {
|
||||
const authToken = await this.authService.login(
|
||||
loginInput,
|
||||
req.raw.workspaceId,
|
||||
);
|
||||
validateSsoEnforcement(workspace);
|
||||
|
||||
const authToken = await this.authService.login(loginInput, workspace.id);
|
||||
this.setAuthCookie(res, authToken);
|
||||
}
|
||||
|
||||
@ -53,10 +53,11 @@ export class AuthController {
|
||||
@Res({ passthrough: true }) res: FastifyReply,
|
||||
@Body() createAdminUserDto: CreateAdminUserDto,
|
||||
) {
|
||||
if (this.environmentService.isCloud()) throw new NotFoundException();
|
||||
const { workspace, authToken } =
|
||||
await this.authService.setup(createAdminUserDto);
|
||||
|
||||
const authToken = await this.authService.setup(createAdminUserDto);
|
||||
this.setAuthCookie(res, authToken);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ -76,7 +77,8 @@ export class AuthController {
|
||||
@Body() forgotPasswordDto: ForgotPasswordDto,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
return this.authService.forgotPassword(forgotPasswordDto, workspace.id);
|
||||
validateSsoEnforcement(workspace);
|
||||
return this.authService.forgotPassword(forgotPasswordDto, workspace);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@ -10,5 +10,6 @@ import { TokenModule } from './token.module';
|
||||
imports: [TokenModule, WorkspaceModule],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, SignupService, JwtStrategy],
|
||||
exports: [SignupService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
8
apps/server/src/core/auth/auth.util.ts
Normal file
8
apps/server/src/core/auth/auth.util.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Workspace } from '@docmost/db/types/entity.types';
|
||||
|
||||
export function validateSsoEnforcement(workspace: Workspace) {
|
||||
if (workspace.enforceSso) {
|
||||
throw new BadRequestException('This workspace has enforced SSO login.');
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ import {
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import {Transform, TransformFnParams} from "class-transformer";
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsOptional()
|
||||
@ -22,6 +22,7 @@ export class CreateUserDto {
|
||||
|
||||
@IsNotEmpty()
|
||||
@MinLength(8)
|
||||
@MaxLength(70)
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export enum JwtType {
|
||||
ACCESS = 'access',
|
||||
COLLAB = 'collab',
|
||||
EXCHANGE = 'exchange',
|
||||
}
|
||||
export type JwtPayload = {
|
||||
sub: string;
|
||||
@ -14,3 +15,9 @@ export type JwtCollabPayload = {
|
||||
workspaceId: string;
|
||||
type: 'collab';
|
||||
};
|
||||
|
||||
export type JwtExchangePayload = {
|
||||
sub: string;
|
||||
workspaceId: string;
|
||||
type: 'exchange';
|
||||
};
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
import { CanActivate, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class SetupGuard implements CanActivate {
|
||||
constructor(private workspaceRepo: WorkspaceRepo) {}
|
||||
constructor(
|
||||
private workspaceRepo: WorkspaceRepo,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async canActivate(): Promise<boolean> {
|
||||
if (this.environmentService.isCloud()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const workspaceCount = await this.workspaceRepo.count();
|
||||
if (workspaceCount > 0) {
|
||||
throw new ForbiddenException('Workspace setup already completed.');
|
||||
|
||||
@ -22,13 +22,13 @@ import { ForgotPasswordDto } from '../dto/forgot-password.dto';
|
||||
import ForgotPasswordEmail from '@docmost/transactional/emails/forgot-password-email';
|
||||
import { UserTokenRepo } from '@docmost/db/repos/user-token/user-token.repo';
|
||||
import { PasswordResetDto } from '../dto/password-reset.dto';
|
||||
import { UserToken } from '@docmost/db/types/entity.types';
|
||||
import { UserToken, Workspace } from '@docmost/db/types/entity.types';
|
||||
import { UserTokenType } from '../auth.constants';
|
||||
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { executeTx } from '@docmost/db/utils';
|
||||
import { VerifyUserTokenDto } from '../dto/verify-user-token.dto';
|
||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||
import { DomainService } from '../../../integrations/environment/domain.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -38,7 +38,7 @@ export class AuthService {
|
||||
private userRepo: UserRepo,
|
||||
private userTokenRepo: UserTokenRepo,
|
||||
private mailService: MailService,
|
||||
private environmentService: EnvironmentService,
|
||||
private domainService: DomainService,
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
) {}
|
||||
|
||||
@ -46,7 +46,9 @@ export class AuthService {
|
||||
const user = await this.userRepo.findByEmail(
|
||||
loginDto.email,
|
||||
workspaceId,
|
||||
true,
|
||||
{
|
||||
includePassword: true
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
@ -68,8 +70,11 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async setup(createAdminUserDto: CreateAdminUserDto) {
|
||||
const user = await this.signupService.initialSetup(createAdminUserDto);
|
||||
return this.tokenService.generateAccessToken(user);
|
||||
const { workspace, user } =
|
||||
await this.signupService.initialSetup(createAdminUserDto);
|
||||
|
||||
const authToken = await this.tokenService.generateAccessToken(user);
|
||||
return { workspace, authToken };
|
||||
}
|
||||
|
||||
async changePassword(
|
||||
@ -113,11 +118,11 @@ export class AuthService {
|
||||
|
||||
async forgotPassword(
|
||||
forgotPasswordDto: ForgotPasswordDto,
|
||||
workspaceId: string,
|
||||
workspace: Workspace,
|
||||
): Promise<void> {
|
||||
const user = await this.userRepo.findByEmail(
|
||||
forgotPasswordDto.email,
|
||||
workspaceId,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
@ -125,7 +130,8 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const token = nanoIdGen(16);
|
||||
const resetLink = `${this.environmentService.getAppUrl()}/password-reset?token=${token}`;
|
||||
|
||||
const resetLink = `${this.domainService.getUrl(workspace.hostname)}/password-reset?token=${token}`;
|
||||
|
||||
await this.userTokenRepo.insertUserToken({
|
||||
token: token,
|
||||
@ -199,7 +205,7 @@ export class AuthService {
|
||||
userTokenDto: VerifyUserTokenDto,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const userToken = await this.userTokenRepo.findById(
|
||||
const userToken: UserToken = await this.userTokenRepo.findById(
|
||||
userTokenDto.token,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
@ -7,7 +7,7 @@ import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { executeTx } from '@docmost/db/utils';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { User } from '@docmost/db/types/entity.types';
|
||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||
import { UserRole } from '../../../common/helpers/types/permission';
|
||||
|
||||
@ -32,7 +32,7 @@ export class SignupService {
|
||||
|
||||
if (userCheck) {
|
||||
throw new BadRequestException(
|
||||
'You already have an account on this workspace',
|
||||
'An account with this email already exists in this workspace',
|
||||
);
|
||||
}
|
||||
|
||||
@ -72,11 +72,14 @@ export class SignupService {
|
||||
createAdminUserDto: CreateAdminUserDto,
|
||||
trx?: KyselyTransaction,
|
||||
) {
|
||||
return await executeTx(
|
||||
let user: User,
|
||||
workspace: Workspace = null;
|
||||
|
||||
await executeTx(
|
||||
this.db,
|
||||
async (trx) => {
|
||||
// create user
|
||||
const user = await this.userRepo.insertUser(
|
||||
user = await this.userRepo.insertUser(
|
||||
{
|
||||
name: createAdminUserDto.name,
|
||||
email: createAdminUserDto.email,
|
||||
@ -92,7 +95,7 @@ export class SignupService {
|
||||
name: createAdminUserDto.workspaceName,
|
||||
};
|
||||
|
||||
const workspace = await this.workspaceService.create(
|
||||
workspace = await this.workspaceService.create(
|
||||
user,
|
||||
workspaceData,
|
||||
trx,
|
||||
@ -103,5 +106,7 @@ export class SignupService {
|
||||
},
|
||||
trx,
|
||||
);
|
||||
|
||||
return { user, workspace };
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
import { JwtCollabPayload, JwtPayload, JwtType } from '../dto/jwt-payload';
|
||||
import {
|
||||
JwtCollabPayload,
|
||||
JwtExchangePayload,
|
||||
JwtPayload,
|
||||
JwtType,
|
||||
} from '../dto/jwt-payload';
|
||||
import { User } from '@docmost/db/types/entity.types';
|
||||
|
||||
@Injectable()
|
||||
@ -34,9 +39,29 @@ export class TokenService {
|
||||
return this.jwtService.sign(payload, { expiresIn });
|
||||
}
|
||||
|
||||
async verifyJwt(token: string) {
|
||||
return this.jwtService.verifyAsync(token, {
|
||||
async generateExchangeToken(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const payload: JwtExchangePayload = {
|
||||
sub: userId,
|
||||
workspaceId: workspaceId,
|
||||
type: JwtType.EXCHANGE,
|
||||
};
|
||||
return this.jwtService.sign(payload, { expiresIn: '10s' });
|
||||
}
|
||||
|
||||
async verifyJwt(token: string, tokenType: string) {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: this.environmentService.getAppSecret(),
|
||||
});
|
||||
|
||||
if (payload.type !== tokenType) {
|
||||
throw new UnauthorizedException(
|
||||
'Invalid JWT token. Token type does not match.',
|
||||
);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
// CLOUD ENV
|
||||
if (this.environmentService.isCloud()) {
|
||||
if (req.raw.workspaceId && req.raw.workspaceId !== payload.workspaceId) {
|
||||
throw new BadRequestException('Workspace does not match');
|
||||
throw new UnauthorizedException('Workspace does not match');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user