implement new invitation system

* fix comments on the frontend
* move jwt token service to its own module
* other fixes and updates
This commit is contained in:
Philipinho
2024-05-14 22:55:11 +01:00
parent 525990d6e5
commit eefe63d1cd
75 changed files with 10965 additions and 7846 deletions

View File

@ -1,34 +0,0 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('page_ordering')
.addColumn('id', 'uuid', (col) =>
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
)
.addColumn('entity_id', 'uuid', (col) => col.notNull())
.addColumn('entity_type', 'varchar', (col) => col.notNull()) // can be page or space
.addColumn('children_ids', sql`uuid[]`, (col) => col.notNull())
.addColumn('space_id', 'uuid', (col) =>
col.references('spaces.id').onDelete('cascade').notNull(),
)
.addColumn('workspace_id', 'uuid', (col) =>
col.references('workspaces.id').onDelete('cascade').notNull(),
)
.addColumn('created_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)
.addColumn('updated_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)
.addColumn('deleted_at', 'timestamptz', (col) => col)
.addUniqueConstraint('page_ordering_entity_id_entity_type_unique', [
'entity_id',
'entity_type',
])
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('page_ordering').execute();
}

View File

@ -0,0 +1,43 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('workspace_invitations')
.addColumn('token', 'varchar', (col) => col)
.addColumn('group_ids', sql`uuid[]`, (col) => col)
.execute();
await db.schema
.alterTable('workspace_invitations')
.dropColumn('status')
.execute();
await db.schema
.alterTable('workspace_invitations')
.addUniqueConstraint('invitation_email_workspace_id_unique', [
'email',
'workspace_id',
])
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('workspace_invitations')
.dropColumn('token')
.execute();
await db.schema
.alterTable('workspace_invitations')
.dropColumn('group_ids')
.execute();
await db.schema
.alterTable('workspace_invitations')
.addColumn('status', 'varchar', (col) => col)
.execute();
await db.schema
.alterTable('workspace_invitations')
.dropConstraint('invitation_email_workspace_id_unique')
.execute();
}

View File

@ -0,0 +1,14 @@
import { type Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('users')
.addColumn('invited_by_id', 'uuid', (col) =>
col.references('users.id').onDelete('set null'),
)
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.alterTable('users').dropColumn('invited_by_id').execute();
}

View File

@ -1,14 +1,24 @@
import { Injectable } from '@nestjs/common';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
import { dbOrTx } from '@docmost/db/utils';
import { dbOrTx, executeTx } from '@docmost/db/utils';
import { GroupUser, InsertableGroupUser } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '../../pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination';
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
import { UserRepo } from '@docmost/db/repos/user/user.repo';
@Injectable()
export class GroupUserRepo {
constructor(@InjectKysely() private readonly db: KyselyDB) {}
constructor(
@InjectKysely() private readonly db: KyselyDB,
private readonly groupRepo: GroupRepo,
private readonly userRepo: UserRepo,
) {}
async getGroupUserById(
userId: string,
@ -62,6 +72,78 @@ export class GroupUserRepo {
return result;
}
async addUserToGroup(
userId: string,
groupId: string,
workspaceId: string,
trx?: KyselyTransaction,
): Promise<void> {
await executeTx(
this.db,
async (trx) => {
const group = await this.groupRepo.findById(groupId, workspaceId, {
trx,
});
if (!group) {
throw new NotFoundException('Group not found');
}
const user = await this.userRepo.findById(userId, workspaceId, {
trx: trx,
});
if (!user) {
throw new NotFoundException('User not found');
}
const groupUserExists = await this.getGroupUserById(
userId,
groupId,
trx,
);
if (groupUserExists) {
throw new BadRequestException(
'User is already a member of this group',
);
}
await this.insertGroupUser(
{
userId,
groupId,
},
trx,
);
},
trx,
);
}
async addUserToDefaultGroup(
userId: string,
workspaceId: string,
trx?: KyselyTransaction,
): Promise<void> {
await executeTx(
this.db,
async (trx) => {
const defaultGroup = await this.groupRepo.getDefaultGroup(
workspaceId,
trx,
);
await this.insertGroupUser(
{
userId,
groupId: defaultGroup.id,
},
trx,
);
},
trx,
);
}
async delete(userId: string, groupId: string): Promise<void> {
await this.db
.deleteFrom('groupUsers')

View File

@ -11,6 +11,7 @@ import { ExpressionBuilder, sql } from 'kysely';
import { PaginationOptions } from '../../pagination/pagination-options';
import { DB } from '@docmost/db/types/db';
import { executeWithPagination } from '@docmost/db/pagination/pagination';
import { DefaultGroup } from '../../../core/group/dto/create-group.dto';
@Injectable()
export class GroupRepo {
@ -19,9 +20,10 @@ export class GroupRepo {
async findById(
groupId: string,
workspaceId: string,
opts?: { includeMemberCount: boolean },
opts?: { includeMemberCount?: boolean; trx?: KyselyTransaction },
): Promise<Group> {
return await this.db
const db = dbOrTx(this.db, opts?.trx);
return db
.selectFrom('groups')
.selectAll('groups')
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
@ -33,9 +35,10 @@ export class GroupRepo {
async findByName(
groupName: string,
workspaceId: string,
opts?: { includeMemberCount: boolean },
opts?: { includeMemberCount?: boolean; trx?: KyselyTransaction },
): Promise<Group> {
return await this.db
const db = dbOrTx(this.db, opts?.trx);
return db
.selectFrom('groups')
.selectAll('groups')
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
@ -85,6 +88,21 @@ export class GroupRepo {
);
}
async createDefaultGroup(
workspaceId: string,
opts?: { userId?: string; trx?: KyselyTransaction },
): Promise<Group> {
const { userId, trx } = opts;
const insertableGroup: InsertableGroup = {
name: DefaultGroup.EVERYONE,
isDefault: true,
creatorId: userId,
workspaceId: workspaceId,
};
return this.insertGroup(insertableGroup, trx);
}
async getGroupsPaginated(workspaceId: string, pagination: PaginationOptions) {
let query = this.db
.selectFrom('groups')

View File

@ -93,7 +93,7 @@ export class UserRepo {
trx?: KyselyTransaction,
): Promise<User> {
const user: InsertableUser = {
name: insertableUser.name || insertableUser.email.split('@')[0],
name: insertableUser.name || insertableUser.email.toLowerCase(),
email: insertableUser.email.toLowerCase(),
password: await hashPassword(insertableUser.password),
locale: 'en',

View File

@ -1,15 +1,10 @@
import type { ColumnType } from 'kysely';
import type { ColumnType } from "kysely";
export type Generated<T> =
T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export type Int8 = ColumnType<
string,
bigint | number | string,
bigint | number | string
>;
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
export type Json = JsonValue;
@ -151,6 +146,7 @@ export interface Users {
email: string;
emailVerifiedAt: Timestamp | null;
id: Generated<string>;
invitedById: string | null;
lastActiveAt: Timestamp | null;
lastLoginAt: Timestamp | null;
locale: string | null;
@ -167,10 +163,11 @@ export interface Users {
export interface WorkspaceInvitations {
createdAt: Generated<Timestamp>;
email: string;
groupIds: string[] | null;
id: Generated<string>;
invitedById: string | null;
role: string;
status: string | null;
token: string | null;
updatedAt: Generated<Timestamp>;
workspaceId: string;
}