feat: add user invite management (#338)

* feat: add invite management page

* refactor: improve existing translations

* test: add test for invite router

* feat: update mysql schema to match sqlite schema

* fix: format issues

* fix: deepsource issues

* fix: lint issues

* chore: address pull request feedback
This commit is contained in:
Meier Lukas
2024-04-29 12:09:34 +02:00
committed by GitHub
parent 16e42d654d
commit 621f6c81ae
20 changed files with 1506 additions and 59 deletions

View File

@@ -1,6 +1,7 @@
import { appRouter as innerAppRouter } from "./router/app";
import { boardRouter } from "./router/board";
import { integrationRouter } from "./router/integration";
import { inviteRouter } from "./router/invite";
import { locationRouter } from "./router/location";
import { logRouter } from "./router/log";
import { userRouter } from "./router/user";
@@ -9,6 +10,7 @@ import { createTRPCRouter } from "./trpc";
export const appRouter = createTRPCRouter({
user: userRouter,
invite: inviteRouter,
integration: integrationRouter,
board: boardRouter,
app: innerAppRouter,

View File

@@ -0,0 +1,70 @@
import { randomBytes } from "crypto";
import { TRPCError } from "@trpc/server";
import { asc, createId, eq } from "@homarr/db";
import { invites } from "@homarr/db/schema/sqlite";
import { z } from "@homarr/validation";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const inviteRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
const dbInvites = await ctx.db.query.invites.findMany({
orderBy: asc(invites.expirationDate),
columns: {
token: false,
},
with: {
creator: {
columns: {
id: true,
name: true,
},
},
},
});
return dbInvites;
}),
createInvite: protectedProcedure
.input(
z.object({
expirationDate: z.date(),
}),
)
.mutation(async ({ ctx, input }) => {
const id = createId();
const token = randomBytes(20).toString("hex");
await ctx.db.insert(invites).values({
id,
expirationDate: input.expirationDate,
creatorId: ctx.session.user.id,
token,
});
return {
id,
token,
};
}),
deleteInvite: protectedProcedure
.input(
z.object({
id: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
const dbInvite = await ctx.db.query.invites.findFirst({
where: eq(invites.id, input.id),
});
if (!dbInvite) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Invite not found",
});
}
await ctx.db.delete(invites).where(eq(invites.id, input.id));
}),
});

View File

@@ -0,0 +1,190 @@
import { describe, expect, test, vi } from "vitest";
import type { Session } from "@homarr/auth";
import { createId } from "@homarr/db";
import { invites, users } from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
import { inviteRouter } from "../invite";
const defaultSession = {
user: {
id: createId(),
},
expires: new Date().toISOString(),
};
// Mock the auth module to return an empty session
vi.mock("@homarr/auth", async () => {
const mod = await import("@homarr/auth/security");
return { ...mod, auth: () => ({}) as Session };
});
describe("all should return all existing invites without sensitive informations", () => {
test("invites should not contain sensitive informations", async () => {
// Arrange
const db = createDb();
const caller = inviteRouter.createCaller({
db,
session: defaultSession,
});
const userId = createId();
await db.insert(users).values({
id: userId,
name: "someone",
});
const inviteId = createId();
await db.insert(invites).values({
id: inviteId,
creatorId: userId,
expirationDate: new Date(2022, 5, 1),
token: "token",
});
// Act
const result = await caller.getAll();
// Assert
expect(result.length).toBe(1);
expect(result[0]?.id).toBe(inviteId);
expect(result[0]?.expirationDate).toEqual(new Date(2022, 5, 1));
expect(result[0]?.creator.id).toBe(userId);
expect(result[0]?.creator.name).toBe("someone");
expect("token" in result[0]!).toBe(false);
});
test("invites should be sorted ascending by expiration date", async () => {
// Arrange
const db = createDb();
const caller = inviteRouter.createCaller({
db,
session: defaultSession,
});
const userId = createId();
await db.insert(users).values({
id: userId,
name: "someone",
});
const inviteId = createId();
await db.insert(invites).values({
id: inviteId,
creatorId: userId,
expirationDate: new Date(2022, 5, 1),
token: "token",
});
await db.insert(invites).values({
id: createId(),
creatorId: userId,
expirationDate: new Date(2022, 5, 2),
token: "token2",
});
// Act
const result = await caller.getAll();
// Assert
expect(result.length).toBe(2);
expect(result[0]?.expirationDate.getDate()).toBe(1);
expect(result[1]?.expirationDate.getDate()).toBe(2);
});
});
describe("create should create a new invite expiring on the specified date with a token and id returned to generate url", () => {
test("creation should work with a date in the future, but less than 6 months.", async () => {
// Arrange
const db = createDb();
const caller = inviteRouter.createCaller({
db,
session: defaultSession,
});
await db.insert(users).values({
id: defaultSession.user.id,
});
const expirationDate = new Date(2024, 5, 1); // TODO: add mock date
// Act
const result = await caller.createInvite({
expirationDate,
});
// Assert
expect(result.id.length).toBeGreaterThan(10);
expect(result.token.length).toBeGreaterThan(20);
const createdInvite = await db.query.invites.findFirst();
expect(createdInvite).toBeDefined();
expect(createdInvite?.id).toBe(result.id);
expect(createdInvite?.token).toBe(result.token);
expect(createdInvite?.expirationDate).toEqual(expirationDate);
expect(createdInvite?.creatorId).toBe(defaultSession.user.id);
});
});
describe("delete should remove invite by id", () => {
test("deletion should remove present invite", async () => {
// Arrange
const db = createDb();
const caller = inviteRouter.createCaller({
db,
session: defaultSession,
});
const userId = createId();
await db.insert(users).values({
id: userId,
});
const inviteId = createId();
await db.insert(invites).values([
{
id: createId(),
creatorId: userId,
expirationDate: new Date(2023, 1, 1),
token: "first-token",
},
{
id: inviteId,
creatorId: userId,
expirationDate: new Date(2023, 1, 1),
token: "second-token",
},
]);
// Act
await caller.deleteInvite({ id: inviteId });
// Assert
const dbInvites = await db.query.invites.findMany();
expect(dbInvites.length).toBe(1);
expect(dbInvites[0]?.id).not.toBe(inviteId);
});
test("deletion should throw with NOT_FOUND code when specified invite not present", async () => {
// Arrange
const db = createDb();
const caller = inviteRouter.createCaller({
db,
session: defaultSession,
});
const userId = createId();
await db.insert(users).values({
id: userId,
});
await db.insert(invites).values({
id: createId(),
creatorId: userId,
expirationDate: new Date(2023, 1, 1),
token: "first-token",
});
// Act
const act = async () => await caller.deleteInvite({ id: createId() });
// Assert
await expect(act()).rejects.toThrow("not found");
});
});

View File

@@ -0,0 +1,9 @@
CREATE TABLE `invite` (
`id` text PRIMARY KEY NOT NULL,
`token` text NOT NULL,
`expiration_date` integer NOT NULL,
`creator_id` text NOT NULL,
FOREIGN KEY (`creator_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `invite_token_unique` ON `invite` (`token`);

View File

@@ -0,0 +1,846 @@
{
"version": "5",
"dialect": "sqlite",
"id": "c0a91279-dffa-4567-8cd2-d9d2d1a2e77c",
"prevId": "7c2291ee-febd-4b90-994c-85e6ef27102d",
"tables": {
"account": {
"name": "account",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"token_type": {
"name": "token_type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"session_state": {
"name": "session_state",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"userId_idx": {
"name": "userId_idx",
"columns": ["userId"],
"isUnique": false
}
},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"account_provider_providerAccountId_pk": {
"columns": ["provider", "providerAccountId"],
"name": "account_provider_providerAccountId_pk"
}
},
"uniqueConstraints": {}
},
"app": {
"name": "app",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"icon_url": {
"name": "icon_url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"href": {
"name": "href",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"boardPermission": {
"name": "boardPermission",
"columns": {
"board_id": {
"name": "board_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"permission": {
"name": "permission",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"boardPermission_board_id_board_id_fk": {
"name": "boardPermission_board_id_board_id_fk",
"tableFrom": "boardPermission",
"tableTo": "board",
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"boardPermission_user_id_user_id_fk": {
"name": "boardPermission_user_id_user_id_fk",
"tableFrom": "boardPermission",
"tableTo": "user",
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"boardPermission_board_id_user_id_permission_pk": {
"columns": ["board_id", "permission", "user_id"],
"name": "boardPermission_board_id_user_id_permission_pk"
}
},
"uniqueConstraints": {}
},
"board": {
"name": "board",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_public": {
"name": "is_public",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"creator_id": {
"name": "creator_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"page_title": {
"name": "page_title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"meta_title": {
"name": "meta_title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"logo_image_url": {
"name": "logo_image_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"favicon_image_url": {
"name": "favicon_image_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"background_image_url": {
"name": "background_image_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"background_image_attachment": {
"name": "background_image_attachment",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'fixed'"
},
"background_image_repeat": {
"name": "background_image_repeat",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'no-repeat'"
},
"background_image_size": {
"name": "background_image_size",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'cover'"
},
"primary_color": {
"name": "primary_color",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#fa5252'"
},
"secondary_color": {
"name": "secondary_color",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#fd7e14'"
},
"opacity": {
"name": "opacity",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 100
},
"custom_css": {
"name": "custom_css",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"column_count": {
"name": "column_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 10
}
},
"indexes": {
"board_name_unique": {
"name": "board_name_unique",
"columns": ["name"],
"isUnique": true
}
},
"foreignKeys": {
"board_creator_id_user_id_fk": {
"name": "board_creator_id_user_id_fk",
"tableFrom": "board",
"tableTo": "user",
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"integration_item": {
"name": "integration_item",
"columns": {
"item_id": {
"name": "item_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"integration_id": {
"name": "integration_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"integration_item_item_id_item_id_fk": {
"name": "integration_item_item_id_item_id_fk",
"tableFrom": "integration_item",
"tableTo": "item",
"columnsFrom": ["item_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"integration_item_integration_id_integration_id_fk": {
"name": "integration_item_integration_id_integration_id_fk",
"tableFrom": "integration_item",
"tableTo": "integration",
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integration_item_item_id_integration_id_pk": {
"columns": ["integration_id", "item_id"],
"name": "integration_item_item_id_integration_id_pk"
}
},
"uniqueConstraints": {}
},
"integrationSecret": {
"name": "integrationSecret",
"columns": {
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"integration_id": {
"name": "integration_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"integration_secret__kind_idx": {
"name": "integration_secret__kind_idx",
"columns": ["kind"],
"isUnique": false
},
"integration_secret__updated_at_idx": {
"name": "integration_secret__updated_at_idx",
"columns": ["updated_at"],
"isUnique": false
}
},
"foreignKeys": {
"integrationSecret_integration_id_integration_id_fk": {
"name": "integrationSecret_integration_id_integration_id_fk",
"tableFrom": "integrationSecret",
"tableTo": "integration",
"columnsFrom": ["integration_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"integrationSecret_integration_id_kind_pk": {
"columns": ["integration_id", "kind"],
"name": "integrationSecret_integration_id_kind_pk"
}
},
"uniqueConstraints": {}
},
"integration": {
"name": "integration",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"integration__kind_idx": {
"name": "integration__kind_idx",
"columns": ["kind"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"invite": {
"name": "invite",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expiration_date": {
"name": "expiration_date",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"creator_id": {
"name": "creator_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"invite_token_unique": {
"name": "invite_token_unique",
"columns": ["token"],
"isUnique": true
}
},
"foreignKeys": {
"invite_creator_id_user_id_fk": {
"name": "invite_creator_id_user_id_fk",
"tableFrom": "invite",
"tableTo": "user",
"columnsFrom": ["creator_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"item": {
"name": "item",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"section_id": {
"name": "section_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"x_offset": {
"name": "x_offset",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"y_offset": {
"name": "y_offset",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"width": {
"name": "width",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"height": {
"name": "height",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"options": {
"name": "options",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'{\"json\": {}}'"
}
},
"indexes": {},
"foreignKeys": {
"item_section_id_section_id_fk": {
"name": "item_section_id_section_id_fk",
"tableFrom": "item",
"tableTo": "section",
"columnsFrom": ["section_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"section": {
"name": "section",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"board_id": {
"name": "board_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"position": {
"name": "position",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"section_board_id_board_id_fk": {
"name": "section_board_id_board_id_fk",
"tableFrom": "section",
"tableTo": "board",
"columnsFrom": ["board_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"session": {
"name": "session",
"columns": {
"sessionToken": {
"name": "sessionToken",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires": {
"name": "expires",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_id_idx": {
"name": "user_id_idx",
"columns": ["userId"],
"isUnique": false
}
},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"emailVerified": {
"name": "emailVerified",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"salt": {
"name": "salt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"verificationToken": {
"name": "verificationToken",
"columns": {
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires": {
"name": "expires",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"columns": ["identifier", "token"],
"name": "verificationToken_identifier_token_pk"
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1710878250235,
"tag": "0000_productive_changeling",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1712777046680,
"tag": "0001_sparkling_zaran",
"breakpoints": true
}
]
}

View File

@@ -92,6 +92,15 @@ export const verificationTokens = mysqlTable(
}),
);
export const invites = mysqlTable("invite", {
id: varchar("id", { length: 256 }).notNull().primaryKey(),
token: varchar("token", { length: 512 }).notNull().unique(),
expirationDate: timestamp("expiration_date").notNull(),
creatorId: varchar("creator_id", { length: 256 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
});
export const integrations = mysqlTable(
"integration",
{
@@ -236,6 +245,14 @@ export const userRelations = relations(users, ({ many }) => ({
accounts: many(accounts),
boards: many(boards),
boardPermissions: many(boardPermissions),
invites: many(invites),
}));
export const inviteRelations = relations(invites, ({ one }) => ({
creator: one(users, {
fields: [invites.creatorId],
references: [users.id],
}),
}));
export const sessionRelations = relations(sessions, ({ one }) => ({

View File

@@ -89,6 +89,17 @@ export const verificationTokens = sqliteTable(
}),
);
export const invites = sqliteTable("invite", {
id: text("id").notNull().primaryKey(),
token: text("token").notNull().unique(),
expirationDate: int("expiration_date", {
mode: "timestamp",
}).notNull(),
creatorId: text("creator_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
});
export const integrations = sqliteTable(
"integration",
{
@@ -231,6 +242,14 @@ export const userRelations = relations(users, ({ many }) => ({
accounts: many(accounts),
boards: many(boards),
boardPermissions: many(boardPermissions),
invites: many(invites),
}));
export const inviteRelations = relations(invites, ({ one }) => ({
creator: one(users, {
fields: [invites.creatorId],
references: [users.id],
}),
}));
export const sessionRelations = relations(sessions, ({ one }) => ({

View File

@@ -13,6 +13,9 @@ export default {
},
},
field: {
email: {
label: "E-Mail",
},
username: {
label: "Username",
},
@@ -866,14 +869,6 @@ export default {
section: {
profile: {
title: "Profile",
form: {
username: {
label: "Username",
},
email: {
label: "E-Mail",
},
},
},
preferences: {
title: "Preferences",
@@ -882,11 +877,6 @@ export default {
title: "Security",
changePassword: {
title: "Change password",
form: {
password: {
label: "Password",
},
},
message: {
passwordUpdated: "Updated password",
},
@@ -911,25 +901,9 @@ export default {
step: {
personalInformation: {
label: "Personal information",
field: {
username: {
label: "Username",
},
email: {
label: "E-Mail",
},
},
},
security: {
label: "Security",
field: {
password: {
label: "Password",
},
confirmPassword: {
label: "Confirm password",
},
},
},
permissions: {
label: "Permissions",
@@ -942,9 +916,45 @@ export default {
title: "User created",
},
},
buttons: {
action: {
createAnother: "Create another user",
return: "Return to the user list",
back: "Return to the user list",
},
},
invite: {
title: "Manager user invites",
action: {
new: {
title: "New invite",
description:
"After the expiration, an invite will no longer be valid and the recipient of the invite won't be able to create an account.",
},
copy: {
title: "Copy invite",
description:
"Your invitation has been generated. After this modal closes, <b>you'll not be able to copy this link anymore</b>. If you do no longer wish to invite said person, you can delete this invitation any time.",
link: "Invitation link",
button: "Copy & close",
},
delete: {
title: "Delete invite",
description:
"Are you sure, that you want to delete this invitation? Users with this link will no longer be able to create an account using that link.",
},
},
field: {
id: {
label: "ID",
},
creator: {
label: "Creator",
},
expirationDate: {
label: "Expiration date",
},
token: {
label: "Token",
},
},
},
},