mirror of
https://github.com/docmost/docmost.git
synced 2025-11-23 16:31:09 +10:00
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:
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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',
|
||||
|
||||
19
apps/server/src/kysely/types/db.d.ts
vendored
19
apps/server/src/kysely/types/db.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user