feat(cloud): add find-workspace and email verification endpoints (#2020)

* feat: add find-workspace and email verification endpoints
* sync
This commit is contained in:
Philip Okugbe
2026-03-14 13:36:30 +00:00
committed by GitHub
parent d0ed6865cb
commit 97c459be67
25 changed files with 479 additions and 13 deletions
@@ -1,3 +1,4 @@
export enum UserTokenType {
FORGOT_PASSWORD = 'forgot-password',
EMAIL_VERIFICATION = 'email-verification',
}
+32
View File
@@ -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);