mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat(cloud): add find-workspace and email verification endpoints (#2020)
* feat: add find-workspace and email verification endpoints * sync
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export enum UserTokenType {
|
||||
FORGOT_PASSWORD = 'forgot-password',
|
||||
EMAIL_VERIFICATION = 'email-verification',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Workspace } from '@docmost/db/types/entity.types';
|
||||
import { createHmac } from 'node:crypto';
|
||||
|
||||
export function computeEmailSignature(
|
||||
email: string,
|
||||
workspaceId: string,
|
||||
appSecret: string,
|
||||
): string {
|
||||
return createHmac('sha256', appSecret)
|
||||
.update(`${email.toLowerCase()}:${workspaceId}`)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function throwIfEmailNotVerified(opts: {
|
||||
isCloud: boolean;
|
||||
emailVerifiedAt: Date | null;
|
||||
email: string;
|
||||
workspaceId: string;
|
||||
appSecret: string;
|
||||
}): void {
|
||||
if (!opts.isCloud || opts.emailVerifiedAt) return;
|
||||
|
||||
const emailSignature = computeEmailSignature(
|
||||
opts.email,
|
||||
opts.workspaceId,
|
||||
opts.appSecret,
|
||||
);
|
||||
throw new BadRequestException({
|
||||
message:
|
||||
'Please verify your email address. Check your inbox for the verification link.',
|
||||
emailSignature,
|
||||
});
|
||||
}
|
||||
|
||||
export function validateSsoEnforcement(workspace: Workspace) {
|
||||
if (workspace.enforceSso) {
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
} from 'class-validator';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import { NoUrls } from '../../../common/validators/no-urls.validator';
|
||||
|
||||
export class CreateAdminUserDto extends CreateUserDto {
|
||||
@IsNotEmpty()
|
||||
@MinLength(1)
|
||||
@MaxLength(50)
|
||||
@NoUrls()
|
||||
@Transform(({ value }: TransformFnParams) => value?.trim())
|
||||
name: string;
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import { NoUrls } from '../../../common/validators/no-urls.validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsOptional()
|
||||
@MinLength(1)
|
||||
@MaxLength(50)
|
||||
@IsString()
|
||||
@NoUrls()
|
||||
@Transform(({ value }: TransformFnParams) => value?.trim())
|
||||
name: string;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
isUserDisabled,
|
||||
nanoIdGen,
|
||||
} from '../../../common/helpers';
|
||||
import { throwIfEmailNotVerified } from '../auth.util';
|
||||
import { ChangePasswordDto } from '../dto/change-password.dto';
|
||||
import { MailService } from '../../../integrations/mail/mail.service';
|
||||
import ChangePasswordEmail from '@docmost/transactional/emails/change-password-email';
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
AUDIT_SERVICE,
|
||||
IAuditService,
|
||||
} from '../../../integrations/audit/audit.service';
|
||||
import { EnvironmentService } from '../../../integrations/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -46,6 +48,7 @@ export class AuthService {
|
||||
private userTokenRepo: UserTokenRepo,
|
||||
private mailService: MailService,
|
||||
private domainService: DomainService,
|
||||
private environmentService: EnvironmentService,
|
||||
@InjectKysely() private readonly db: KyselyDB,
|
||||
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
|
||||
) {}
|
||||
@@ -69,6 +72,14 @@ export class AuthService {
|
||||
throw new UnauthorizedException(errorMessage);
|
||||
}
|
||||
|
||||
throwIfEmailNotVerified({
|
||||
isCloud: this.environmentService.isCloud(),
|
||||
emailVerifiedAt: user.emailVerifiedAt,
|
||||
email: user.email,
|
||||
workspaceId,
|
||||
appSecret: this.environmentService.getAppSecret(),
|
||||
});
|
||||
|
||||
user.lastLoginAt = new Date();
|
||||
await this.userRepo.updateLastLogin(user.id, workspaceId);
|
||||
|
||||
@@ -247,6 +258,14 @@ export class AuthService {
|
||||
template: emailTemplate,
|
||||
});
|
||||
|
||||
if (this.environmentService.isCloud() && !user.emailVerifiedAt) {
|
||||
await this.userRepo.updateUser(
|
||||
{ emailVerifiedAt: new Date() },
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user has MFA enabled or workspace enforces MFA
|
||||
const userHasMfa = user?.['mfa']?.isEnabled || false;
|
||||
const workspaceEnforcesMfa = workspace.enforceMfa || false;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { UserRole } from '../../../common/helpers/types/permission';
|
||||
import { NoUrls } from '../../../common/validators/no-urls.validator';
|
||||
|
||||
export class InviteUserDto {
|
||||
@IsArray()
|
||||
@@ -44,6 +45,7 @@ export class AcceptInviteDto extends InvitationIdDto {
|
||||
@MinLength(2)
|
||||
@MaxLength(60)
|
||||
@IsString()
|
||||
@NoUrls()
|
||||
name: string;
|
||||
|
||||
@MinLength(8)
|
||||
|
||||
@@ -244,7 +244,7 @@ export class WorkspaceService {
|
||||
await this.billingQueue.add(
|
||||
QueueJob.WELCOME_EMAIL,
|
||||
{ userId: user.id },
|
||||
{ delay: 60 * 1000 }, // 1m
|
||||
{ delay: 30 * 60 * 1000 }, // 30m
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
|
||||
Reference in New Issue
Block a user