diff --git a/drizzle/0001_brave_mimic.sql b/drizzle/0001_brave_mimic.sql new file mode 100644 index 00000000000..e32518ada10 --- /dev/null +++ b/drizzle/0001_brave_mimic.sql @@ -0,0 +1,10 @@ +CREATE TABLE `migrate_token` ( + `id` text PRIMARY KEY NOT NULL, + `token` text NOT NULL, + `boards` integer NOT NULL, + `users` integer NOT NULL, + `integrations` integer NOT NULL, + `expires` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `migrate_token_token_unique` ON `migrate_token` (`token`); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000000..e47b5af15a6 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,527 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "9c8971c9-6d33-4d14-b318-b19ff9fbb88f", + "prevId": "32c1bc91-e69f-4e1d-b53c-9c43f2e6c9d3", + "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" + ] + } + }, + "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 + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_created_by_id_user_id_fk": { + "name": "invite_created_by_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "migrate_token": { + "name": "migrate_token", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "boards": { + "name": "boards", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "users": { + "name": "users", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integrations": { + "name": "integrations", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "migrate_token_token_unique": { + "name": "migrate_token_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "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_setting": { + "name": "user_setting", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color_scheme": { + "name": "color_scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'environment'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "default_board": { + "name": "default_board", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'monday'" + }, + "search_template": { + "name": "search_template", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https://google.com/search?q=%s'" + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "disable_ping_pulse": { + "name": "disable_ping_pulse", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "replace_ping_with_icons": { + "name": "replace_ping_with_icons", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "use_debug_language": { + "name": "use_debug_language", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "auto_focus_search": { + "name": "auto_focus_search", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_setting_user_id_user_id_fk": { + "name": "user_setting_user_id_user_id_fk", + "tableFrom": "user_setting", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "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 + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_owner": { + "name": "is_owner", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 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" + ] + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 6af8ab350ad..a456183827f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1695874816934, "tag": "0000_supreme_the_captain", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1730643218521, + "tag": "0001_brave_mimic", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index bd89afd27f3..1687d0ec5cc 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "test:coverage": "SKIP_ENV_VALIDATION=1 vitest run --coverage", "docker:build": "turbo build && docker build . -t homarr:local-dev", "docker:start": "docker run -p 7575:7575 --name homarr-development homarr:local-dev", - "db:migrate": "dotenv tsx drizzle/migrate/migrate.ts ./drizzle" + "db:migrate": "dotenv tsx drizzle/migrate/migrate.ts ./drizzle", + "db:add": "drizzle-kit generate:sqlite --config ./drizzle.config.ts" }, "dependencies": { "@ctrl/deluge": "^4.1.0", @@ -126,7 +127,7 @@ "@types/cookies": "^0.7.7", "@types/dockerode": "^3.3.9", "@types/ldapjs": "^3.0.2", - "@types/node": "18.17.8", + "@types/node": "^20.6.0", "@types/prismjs": "^1.26.0", "@types/react": "^18.2.11", "@types/swagger-ui-react": "^4.18.3", diff --git a/public/locales/en/layout/manage.json b/public/locales/en/layout/manage.json index 39265bd04c3..9a5cee10c5f 100644 --- a/public/locales/en/layout/manage.json +++ b/public/locales/en/layout/manage.json @@ -26,7 +26,8 @@ "title": "Tools", "items": { "docker": "Docker", - "api": "API" + "api": "API", + "migrate": "Migrate to 1.0" } }, "about": { diff --git a/public/locales/en/manage/migrate.json b/public/locales/en/manage/migrate.json new file mode 100644 index 00000000000..ae58312f352 --- /dev/null +++ b/public/locales/en/manage/migrate.json @@ -0,0 +1,33 @@ +{ + "metaTitle": "Migrate to 1.0", + "pageTitle": "Migrate boards, integrations and users", + "description": "Exports your boards and users to a ZIP-Archive to migrate them to Homarr after version 1.0.0", + "securityNote": { + "title": "Security Note", + "text": "When exporting users and integrations it will also open a modal with an encryption key. This key is required to import the data into Homarr. Keep it safe and do not share it with anyone." + }, + "form": { + "label": "Select everything you want to export", + "option": { + "boards": { + "label": "Export boards" + }, + "integrations": { + "label": "Export integrations", + "description": "This will include encrypted credentials for integrations. Only available when exporting boards" + }, + "users": { + "label": "Export users", + "description": "This will only export credential users, passwords hash and salt are encrypted" + } + } + }, + "action": { + "export": "Export data" + }, + "modal": { + "title": "Encryption key", + "description": "Your data has been exported. Keep this key safe and do not share it with anyone. You will need this key to import the data into Homarr.", + "copyDismiss": "Copy & dismiss" + } +} \ No newline at end of file diff --git a/src/components/layout/Templates/ManageLayout.tsx b/src/components/layout/Templates/ManageLayout.tsx index 80c5af53144..9ed446e27f1 100644 --- a/src/components/layout/Templates/ManageLayout.tsx +++ b/src/components/layout/Templates/ManageLayout.tsx @@ -17,11 +17,13 @@ import { IconBrandDiscord, IconBrandDocker, IconBrandGithub, + IconFileExport, IconGitFork, IconHome, IconInfoSmall, IconLayoutDashboard, - IconMailForward, IconPlug, + IconMailForward, + IconPlug, IconQuestionMark, IconTool, IconUser, @@ -103,8 +105,12 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => { }, api: { icon: IconPlug, - href: '/manage/tools/swagger' - } + href: '/manage/tools/swagger', + }, + migrate: { + icon: IconFileExport, + href: '/manage/tools/migrate', + }, }, }, help: { diff --git a/src/hooks/useSetSafeInterval.tsx b/src/hooks/useSetSafeInterval.tsx index 4bedbc15e36..f34a18da597 100644 --- a/src/hooks/useSetSafeInterval.tsx +++ b/src/hooks/useSetSafeInterval.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react'; export function useSetSafeInterval() { - const timers = useRef([]); + const timers = useRef([]); function setSafeInterval(callback: () => void, delay: number) { const newInterval = setInterval(callback, delay); diff --git a/src/pages/api/migrate.ts b/src/pages/api/migrate.ts new file mode 100644 index 00000000000..a3f9fbaca75 --- /dev/null +++ b/src/pages/api/migrate.ts @@ -0,0 +1,109 @@ +import AdmZip from 'adm-zip'; +import crypto, { randomBytes } from 'crypto'; +import { eq, isNotNull } from 'drizzle-orm'; +import fs from 'fs'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { getServerAuthSession } from '~/server/auth'; +import { db } from '~/server/db'; +import { migrateTokens, users } from '~/server/db/schema'; +import { getConfig } from '~/tools/config/getConfig'; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getServerAuthSession({ req, res }); + if (!session) { + return res.status(401).end(); + } + + if (!session.user.isAdmin) { + return res.status(403).end('Not an admin'); + } + + const token = req.query.token; + + if (!token || Array.isArray(token)) { + return res.status(400).end(); + } + + const dbToken = await db.query.migrateTokens.findFirst({ + where: eq(migrateTokens.token, token), + }); + + if (!dbToken) { + return res.status(403).end('No db token'); + } + + if (dbToken.expires < new Date()) { + return res.status(403).end('Token expired'); + } + + const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json')); + + const zip = new AdmZip(); + + for (const file of files) { + const data = await getConfig(file.replace('.json', '')); + + const mappedApps = data.apps.map((app) => ({ + ...app, + integration: + app.integration && dbToken.integrations + ? { + ...app.integration, + properties: app.integration.properties.map((property) => ({ + ...property, + value: property.value ? encryptSecret(property.value, dbToken.token) : null, + })), + } + : null, + })); + + const content = JSON.stringify( + { + ...data, + apps: mappedApps, + }, + null, + 2 + ); + zip.addFile(file, Buffer.from(content, 'utf-8')); + } + + if (dbToken.users) { + // Only credentials users + const dbUsers = await db.query.users.findMany({ + with: { settings: true }, + where: isNotNull(users.password), + }); + const encryptedUsers = dbUsers.map((user) => ({ + ...user, + password: user.password ? encryptSecret(user.password, dbToken.token) : null, + salt: user.salt ? encryptSecret(user.salt, dbToken.token) : null, + })); + const content = JSON.stringify(encryptedUsers, null, 2); + zip.addFile('users/users.json', Buffer.from(content, 'utf-8')); + } + + if (dbToken.integrations || dbToken.users) { + const checksum = randomBytes(16).toString('hex'); + const encryptedChecksum = encryptSecret(checksum, dbToken.token); + const content = `${checksum}\n${encryptedChecksum}`; + zip.addFile('checksum.txt', Buffer.from(content, 'utf-8')); + } + + const zipBuffer = zip.toBuffer(); + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', 'attachment; filename=migrate-homarr.zip'); + res.setHeader('Content-Length', zipBuffer.length.toString()); + res.status(200).end(zipBuffer); +}; + +export default handler; + +export function encryptSecret(text: string, encryptionKey: string): `${string}.${string}` { + const key = Buffer.from(encryptionKey, 'hex'); + const initializationVector = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), initializationVector); + let encrypted = cipher.update(text); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return `${encrypted.toString('hex')}.${initializationVector.toString('hex')}`; +} diff --git a/src/pages/manage/tools/migrate.tsx b/src/pages/manage/tools/migrate.tsx new file mode 100644 index 00000000000..1c0aaf85198 --- /dev/null +++ b/src/pages/manage/tools/migrate.tsx @@ -0,0 +1,154 @@ +import { + Alert, + Button, + Checkbox, + CopyButton, + Input, + Modal, + PasswordInput, + Stack, + Text, + Title, +} from '@mantine/core'; +import { GetServerSideProps } from 'next'; +import { useTranslation } from 'next-i18next'; +import Head from 'next/head'; +import { useState } from 'react'; +import { ManageLayout } from '~/components/layout/Templates/ManageLayout'; +import { getServerAuthSession } from '~/server/auth'; +import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; +import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; +import { api } from '~/utils/api'; + +/** + * 1. Send selected options to the server + * 2. Create a download token and send it back to the client + * 3. Client downloads the ZIP file + * 4. Client shows the encryption key in a modal + */ + +const ManagementPage = () => { + const { t } = useTranslation('manage/migrate'); + const metaTitle = `${t('metaTitle')} • Homarr`; + const { mutateAsync } = api.migrate.createToken.useMutation(); + const [options, setOptions] = useState({ + boards: true, + integrations: true, + users: true, + }); + const [token, setToken] = useState(null); + const [opened, setOpened] = useState(false); + const onClick = async () => { + await mutateAsync(options, { + onSuccess: (token) => { + // Download ZIP file + const link = document.createElement('a'); + const baseUrl = window.location.origin; + link.href = `${baseUrl}/api/migrate?token=${token}`; + link.download = 'migration.zip'; + link.click(); + + // Token is only needed when exporting users or integrations + if (options.users || options.integrations) { + setToken(token); + setOpened(true); + } + }, + }); + }; + + return ( + + + {metaTitle} + + + + {t('pageTitle')} + {t('description')} + + + {t('securityNote.text')} + + + + + + setOptions((prev) => ({ + ...prev, + boards: event.target.checked, + integrations: false, + })) + } + /> + + setOptions((prev) => ({ ...prev, integrations: event.target.checked })) + } + description={t('form.option.integrations.description')} + /> + setOptions((prev) => ({ ...prev, users: event.target.checked }))} + description={t('form.option.users.description')} + /> + + + + + + + setOpened(false)} title={t('modal.title')}> + {token && ( + + {t('modal.description')} + + + {({ copy }) => ( + + )} + + + )} + + + ); +}; + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + const session = await getServerAuthSession(ctx); + + const result = checkForSessionOrAskForLogin(ctx, session, () => Boolean(session?.user.isAdmin)); + if (result) { + return result; + } + + const translations = await getServerSideTranslations( + ['layout/manage', 'manage/migrate'], + ctx.locale, + ctx.req, + ctx.res + ); + return { + props: { + ...translations, + }, + }; +}; + +export default ManagementPage; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 98f0ee68f55..2ce6cc1e4a0 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,3 +1,4 @@ +import { tdarrRouter } from '~/server/api/routers/tdarr'; import { createTRPCRouter } from '~/server/api/trpc'; import { appRouter } from './routers/app'; @@ -14,6 +15,7 @@ import { indexerManagerRouter } from './routers/indexer-manager'; import { inviteRouter } from './routers/invite/invite-router'; import { mediaRequestsRouter } from './routers/media-request'; import { mediaServerRouter } from './routers/media-server'; +import { migrateRouter } from './routers/migrate'; import { notebookRouter } from './routers/notebook'; import { overseerrRouter } from './routers/overseerr'; import { passwordRouter } from './routers/password'; @@ -22,7 +24,6 @@ import { smartHomeEntityStateRouter } from './routers/smart-home/entity-state'; import { usenetRouter } from './routers/usenet/router'; import { userRouter } from './routers/user'; import { weatherRouter } from './routers/weather'; -import { tdarrRouter } from '~/server/api/routers/tdarr'; /** * This is the primary router for your server. @@ -53,6 +54,7 @@ export const rootRouter = createTRPCRouter({ smartHomeEntityState: smartHomeEntityStateRouter, healthMonitoring: healthMonitoringRouter, tdarr: tdarrRouter, + migrate: migrateRouter, }); // export type definition of API diff --git a/src/server/api/routers/migrate.ts b/src/server/api/routers/migrate.ts new file mode 100644 index 00000000000..b167d74688d --- /dev/null +++ b/src/server/api/routers/migrate.ts @@ -0,0 +1,26 @@ +import { randomBytes } from 'crypto'; +import dayjs from 'dayjs'; +import { v4 } from 'uuid'; +import { z } from 'zod'; +import { db } from '~/server/db'; +import { migrateTokens } from '~/server/db/schema'; + +import { adminProcedure, createTRPCRouter } from '../trpc'; + +export const migrateRouter = createTRPCRouter({ + createToken: adminProcedure + .input(z.object({ boards: z.boolean(), users: z.boolean(), integrations: z.boolean() })) + .mutation(async ({ input }) => { + const id = v4(); + const token = randomBytes(32).toString('hex'); + + await db.insert(migrateTokens).values({ + id, + token, + ...input, + expires: dayjs().add(5, 'minutes').toDate(), + }); + + return token; + }), +}); diff --git a/src/server/api/routers/notebook.ts b/src/server/api/routers/notebook.ts index e3f0339018b..c75a4e112a9 100644 --- a/src/server/api/routers/notebook.ts +++ b/src/server/api/routers/notebook.ts @@ -6,7 +6,7 @@ import { getConfig } from '~/tools/config/getConfig'; import { BackendConfigType } from '~/types/config'; import { INotebookWidget } from '~/widgets/notebook/NotebookWidgetTile'; -import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc'; +import { adminProcedure, createTRPCRouter } from '../trpc'; export const notebookRouter = createTRPCRouter({ update: adminProcedure diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 9ee168d0599..4909723e6e9 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -109,6 +109,17 @@ export const invites = sqliteTable('invite', { export type Invite = InferSelectModel; +export const migrateTokens = sqliteTable('migrate_token', { + id: text('id').notNull().primaryKey(), + token: text('token').notNull().unique(), + boards: int('boards', { mode: 'boolean' }).notNull(), + users: int('users', { mode: 'boolean' }).notNull(), + integrations: int('integrations', { mode: 'boolean' }).notNull(), + expires: int('expires', { + mode: 'timestamp', + }).notNull(), +}); + export const accountRelations = relations(accounts, ({ one }) => ({ user: one(users, { fields: [accounts.userId], diff --git a/yarn.lock b/yarn.lock index f95bb243390..4a654fe0d4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3509,13 +3509,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.17.8": - version: 18.17.8 - resolution: "@types/node@npm:18.17.8" - checksum: ebb71526368c9c58f03e2c2408bfda4aa686c13d84226e2c9b48d9c4aee244fb82e672aaf4aa8ccb6e4993b4274d5f4b2b3d52d0a2e57ab187ae653903376411 - languageName: node - linkType: hard - "@types/node@npm:^16.10.2": version: 16.18.61 resolution: "@types/node@npm:16.18.61" @@ -3532,6 +3525,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.6.0": + version: 20.17.6 + resolution: "@types/node@npm:20.17.6" + dependencies: + undici-types: ~6.19.2 + checksum: d51dbb9881c94d0310b32b5fd8013e3261595c61bc888fa27258469c93c3dc0b3c4d20a9f28f3f5f79562f6737e28e7f3dd04940dc8b4d966d34aaf318f7f69b + languageName: node + linkType: hard + "@types/object.omit@npm:^3.0.0": version: 3.0.3 resolution: "@types/object.omit@npm:3.0.3" @@ -7622,7 +7624,7 @@ __metadata: "@types/cookies": ^0.7.7 "@types/dockerode": ^3.3.9 "@types/ldapjs": ^3.0.2 - "@types/node": 18.17.8 + "@types/node": ^20.6.0 "@types/prismjs": ^1.26.0 "@types/react": ^18.2.11 "@types/swagger-ui-react": ^4.18.3 @@ -12567,6 +12569,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: de51f1b447d22571cf155dfe14ff6d12c5bdaec237c765085b439c38ca8518fc360e88c70f99469162bf2e14188a7b0bcb06e1ed2dc031042b984b0bb9544017 + languageName: node + linkType: hard + "undici@npm:^5.24.0": version: 5.28.2 resolution: "undici@npm:5.28.2"