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:
Philip Okugbe
2025-03-06 13:38:37 +00:00
committed by GitHub
parent 91596be70e
commit b81c9ee10c
148 changed files with 8947 additions and 3458 deletions
@@ -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;
}
}