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

View File

@ -7,25 +7,63 @@ import {
UpdatableWorkspace,
Workspace,
} from '@docmost/db/types/entity.types';
import { sql } from 'kysely';
import { ExpressionBuilder, sql } from 'kysely';
import { DB, Workspaces } from '@docmost/db/types/db';
@Injectable()
export class WorkspaceRepo {
public baseFields: Array<keyof Workspaces> = [
'id',
'name',
'description',
'logo',
'hostname',
'customDomain',
'settings',
'defaultRole',
'emailDomains',
'defaultSpaceId',
'createdAt',
'updatedAt',
'deletedAt',
'stripeCustomerId',
'status',
'billingEmail',
'trialEndAt',
'enforceSso',
'plan',
];
constructor(@InjectKysely() private readonly db: KyselyDB) {}
async findById(
workspaceId: string,
opts?: {
withLock?: boolean;
withMemberCount?: boolean;
withLicenseKey?: boolean;
trx?: KyselyTransaction;
},
): Promise<Workspace> {
const db = dbOrTx(this.db, opts?.trx);
return db
let query = db
.selectFrom('workspaces')
.selectAll()
.where('id', '=', workspaceId)
.executeTakeFirst();
.select(this.baseFields)
.where('id', '=', workspaceId);
if (opts?.withMemberCount) {
query = query.select(this.withMemberCount);
}
if (opts?.withLicenseKey) {
query = query.select('licenseKey');
}
if (opts?.withLock && opts?.trx) {
query = query.forUpdate();
}
return query.executeTakeFirst();
}
async findFirst(): Promise<Workspace> {
@ -45,17 +83,34 @@ export class WorkspaceRepo {
.executeTakeFirst();
}
async hostnameExists(
hostname: string,
trx?: KyselyTransaction,
): Promise<boolean> {
if (hostname?.length < 1) return false;
const db = dbOrTx(this.db, trx);
let { count } = await db
.selectFrom('workspaces')
.select((eb) => eb.fn.count('id').as('count'))
.where(sql`LOWER(hostname)`, '=', sql`LOWER(${hostname})`)
.executeTakeFirst();
count = count as number;
return count != 0;
}
async updateWorkspace(
updatableWorkspace: UpdatableWorkspace,
workspaceId: string,
trx?: KyselyTransaction,
) {
): Promise<Workspace> {
const db = dbOrTx(this.db, trx);
return db
.updateTable('workspaces')
.set({ ...updatableWorkspace, updatedAt: new Date() })
.where('id', '=', workspaceId)
.execute();
.returning(this.baseFields)
.executeTakeFirst();
}
async insertWorkspace(
@ -66,7 +121,7 @@ export class WorkspaceRepo {
return db
.insertInto('workspaces')
.values(insertableWorkspace)
.returningAll()
.returning(this.baseFields)
.executeTakeFirst();
}
@ -77,4 +132,28 @@ export class WorkspaceRepo {
.executeTakeFirst();
return count as number;
}
withMemberCount(eb: ExpressionBuilder<DB, 'workspaces'>) {
return eb
.selectFrom('users')
.select((eb) => eb.fn.countAll().as('count'))
.where('users.deactivatedAt', 'is', null)
.where('users.deletedAt', 'is', null)
.whereRef('users.workspaceId', '=', 'workspaces.id')
.as('memberCount');
}
async getActiveUserCount(workspaceId: string): Promise<number> {
const users = await this.db
.selectFrom('users')
.select(['id', 'deactivatedAt', 'deletedAt'])
.where('workspaceId', '=', workspaceId)
.execute();
const activeUsers = users.filter(
(user) => user.deletedAt === null && user.deactivatedAt === null,
);
return activeUsers.length;
}
}