diff --git a/.eslintrc.js b/.eslintrc.js index 95e15ef7..4ca474ae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,11 +1,6 @@ /** @type {import('eslint').Linter.Config} */ module.exports = { - extends: [ - '@remix-run/eslint-config', - '@remix-run/eslint-config/node', - 'plugin:prettier/recommended', - 'plugin:@typescript-eslint/recommended', - ], + extends: ['next/core-web-vitals', 'plugin:prettier/recommended', 'plugin:@typescript-eslint/recommended'], root: true, plugins: ['unused-imports', '@typescript-eslint'], parser: '@typescript-eslint/parser', diff --git a/.gitignore b/.gitignore index 2712d52f..8ab94350 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,28 @@ -node_modules +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -/.cache -/build -/public/build +# dependencies +/node_modules +/.pnp +.pnp.js # yarn .yarn/* !.yarn/releases !.yarn/plugins +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + # misc .DS_Store *.pem -.idea # debug npm-debug.log* @@ -20,15 +30,14 @@ yarn-debug.log* yarn-error.log* # local env files -.env -.env.local -.env.development.local -.env.test.local -.env.production.local +.env* # vercel .vercel +# typescript +*.tsbuildinfo +next-env.d.ts + # zipline -uploads*/ -dist/ \ No newline at end of file +uploads*/ \ No newline at end of file diff --git a/next.config.js b/next.config.js new file mode 100644 index 00000000..a843cbee --- /dev/null +++ b/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +module.exports = nextConfig diff --git a/package.json b/package.json index 31b734c8..2650899b 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,12 @@ "version": "4.0.0-dev.1", "scripts": { "build": "run-s build:*", - "build:remix": "remix build", + "build:remix": "next build", "build:server": "tsup", - "dev": "run-p dev:remix & (run-s dev:build && run-s dev:server)", + "dev": "run-s dev:build dev:server", "dev:build": "cross-env NODE_ENV=development run-s build:server", - "dev:remix": "cross-env NODE_ENV=development remix watch", "dev:server": "cross-env NODE_ENV=development DEBUG=zipline node --require ./node_modules/dotenv/config ./build/server.js", - "start": "node ./server.mjs", + "start": "node ./build/server.mjs", "lint": "eslint --cache --ignore-path .gitignore --fix .", "format": "prettier --write --ignore-path .gitignore .", "validate": "run-p lint format" @@ -26,23 +25,20 @@ "@mantine/form": "^6.0.14", "@mantine/hooks": "^6.0.14", "@mantine/modals": "^6.0.14", + "@mantine/next": "^6.0.14", "@mantine/notifications": "^6.0.14", - "@mantine/prism": "^6.0.14", - "@mantine/remix": "^6.0.14", + "@mantine/nprogress": "^6.0.14", "@prisma/client": "4.16.1", "@prisma/internals": "^4.16.1", "@prisma/migrate": "^4.16.1", - "@remix-run/express": "^1.17.1", - "@remix-run/node": "^1.16.1", - "@remix-run/react": "^1.16.1", - "@remix-run/v1-route-convention": "^0.1.2", - "@types/express": "^4.17.17", + "@tabler/icons-react": "^2.22.0", + "argon2": "^0.30.3", "bytes": "^3.1.2", "colorette": "^2.0.20", "dayjs": "^1.11.8", "express": "^4.18.2", - "isbot": "^3.6.10", "ms": "^2.1.3", + "next": "^13.4.7", "react": "^18.2.0", "react-dom": "^18.2.0", "znv": "^0.3.2", @@ -52,6 +48,7 @@ "@remix-run/dev": "^1.16.1", "@remix-run/eslint-config": "^1.16.1", "@types/bytes": "^3.1.1", + "@types/express": "^4.17.17", "@types/node": "^20.3.1", "@types/react": "^18.2.7", "@types/react-dom": "^18.2.4", diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..c759b743 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,14 @@ +module.exports = { + plugins: { + 'postcss-preset-mantine': {}, + 'postcss-simple-vars': { + variables: { + 'mantine-breakpoint-xs': '36em', + 'mantine-breakpoint-sm': '48em', + 'mantine-breakpoint-md': '62em', + 'mantine-breakpoint-lg': '75em', + 'mantine-breakpoint-xl': '88em', + }, + }, + }, +}; \ No newline at end of file diff --git a/prisma/migrations/20230624064156_init/migration.sql b/prisma/migrations/20230624064156_init/migration.sql deleted file mode 100644 index 608d37f9..00000000 --- a/prisma/migrations/20230624064156_init/migration.sql +++ /dev/null @@ -1,234 +0,0 @@ --- CreateEnum -CREATE TYPE "OAuthProviderType" AS ENUM ('DISCORD', 'GOOGLE', 'GITHUB'); - --- CreateEnum -CREATE TYPE "LimitType" AS ENUM ('UPLOAD_COUNT', 'UPLOAD_SIZE', 'SHORTEN_COUNT'); - --- CreateEnum -CREATE TYPE "LimitTimeframe" AS ENUM ('SECONDLY', 'MINUTELY', 'HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'); - --- CreateEnum -CREATE TYPE "IncompleteFileStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETE', 'FAILED'); - --- CreateTable -CREATE TABLE "zipline_meta" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "firstSetup" BOOLEAN NOT NULL DEFAULT true, - - CONSTRAINT "zipline_meta_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "username" TEXT NOT NULL, - "password" TEXT, - "avatar" TEXT, - "token" TEXT NOT NULL, - "administrator" BOOLEAN NOT NULL DEFAULT false, - "ziplineId" TEXT NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "OAuthProvider" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "userId" TEXT NOT NULL, - "provider" "OAuthProviderType" NOT NULL, - "accessToken" TEXT NOT NULL, - "refreshToken" TEXT NOT NULL, - "expiresIn" INTEGER NOT NULL, - "scope" TEXT NOT NULL, - "tokenType" TEXT NOT NULL, - "profile" JSONB NOT NULL, - - CONSTRAINT "OAuthProvider_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "UserLimit" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "type" "LimitType" NOT NULL, - "value" INTEGER NOT NULL, - "timeframe" "LimitTimeframe" NOT NULL, - "userId" TEXT NOT NULL, - - CONSTRAINT "UserLimit_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "File" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "deletesAt" TIMESTAMP(3), - "name" TEXT NOT NULL, - "originalName" TEXT NOT NULL, - "path" TEXT NOT NULL, - "size" INTEGER NOT NULL, - "type" TEXT NOT NULL, - "views" INTEGER NOT NULL DEFAULT 0, - "favorite" BOOLEAN NOT NULL DEFAULT false, - "password" TEXT, - "zeroWidthSpace" TEXT, - "userId" TEXT, - "folderId" TEXT, - - CONSTRAINT "File_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Folder" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "name" TEXT NOT NULL, - "userId" TEXT NOT NULL, - - CONSTRAINT "Folder_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "IncompleteFile" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "status" "IncompleteFileStatus" NOT NULL, - "chunksTotal" INTEGER NOT NULL, - "chunksComplete" INTEGER NOT NULL, - "metadata" JSONB NOT NULL, - "userId" TEXT NOT NULL, - - CONSTRAINT "IncompleteFile_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Tag" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "name" TEXT NOT NULL, - "color" TEXT NOT NULL, - - CONSTRAINT "Tag_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Url" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "vanity" TEXT, - "destination" TEXT NOT NULL, - "name" TEXT NOT NULL, - "zeroWidthSpace" TEXT, - "userId" TEXT, - - CONSTRAINT "Url_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Metric" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "data" JSONB NOT NULL, - "ziplineId" TEXT NOT NULL, - - CONSTRAINT "Metric_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Invite" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "expiresAt" TIMESTAMP(3), - "code" TEXT NOT NULL, - "used" BOOLEAN NOT NULL DEFAULT false, - "inviterId" TEXT NOT NULL, - "ziplineId" TEXT NOT NULL, - - CONSTRAINT "Invite_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "_FileToTag" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); - --- CreateIndex -CREATE UNIQUE INDEX "User_token_key" ON "User"("token"); - --- CreateIndex -CREATE UNIQUE INDEX "OAuthProvider_userId_provider_key" ON "OAuthProvider"("userId", "provider"); - --- CreateIndex -CREATE UNIQUE INDEX "UserLimit_type_key" ON "UserLimit"("type"); - --- CreateIndex -CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name"); - --- CreateIndex -CREATE UNIQUE INDEX "Url_name_key" ON "Url"("name"); - --- CreateIndex -CREATE UNIQUE INDEX "Invite_code_key" ON "Invite"("code"); - --- CreateIndex -CREATE UNIQUE INDEX "_FileToTag_AB_unique" ON "_FileToTag"("A", "B"); - --- CreateIndex -CREATE INDEX "_FileToTag_B_index" ON "_FileToTag"("B"); - --- AddForeignKey -ALTER TABLE "User" ADD CONSTRAINT "User_ziplineId_fkey" FOREIGN KEY ("ziplineId") REFERENCES "zipline_meta"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "OAuthProvider" ADD CONSTRAINT "OAuthProvider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UserLimit" ADD CONSTRAINT "UserLimit_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "IncompleteFile" ADD CONSTRAINT "IncompleteFile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Metric" ADD CONSTRAINT "Metric_ziplineId_fkey" FOREIGN KEY ("ziplineId") REFERENCES "zipline_meta"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Invite" ADD CONSTRAINT "Invite_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Invite" ADD CONSTRAINT "Invite_ziplineId_fkey" FOREIGN KEY ("ziplineId") REFERENCES "zipline_meta"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_FileToTag" ADD CONSTRAINT "_FileToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "File"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_FileToTag" ADD CONSTRAINT "_FileToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92c..00000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d7fdb57a..80c6b74a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,12 +13,6 @@ model Zipline { updatedAt DateTime @updatedAt firstSetup Boolean @default(true) - - metrics Metric[] - users User[] - invite Invite[] - - @@map("zipline_meta") } model User { @@ -39,9 +33,6 @@ model User { invites Invite[] oauthProviders OAuthProvider[] IncompleteFile IncompleteFile[] - - Zipline Zipline @relation(fields: [ziplineId], references: [id], onDelete: Cascade, onUpdate: Cascade) - ziplineId String } model OAuthProvider { @@ -191,9 +182,6 @@ model Metric { updatedAt DateTime @updatedAt data Json - - Zipline Zipline @relation(fields: [ziplineId], references: [id], onDelete: Cascade, onUpdate: Cascade) - ziplineId String } model Invite { @@ -207,7 +195,4 @@ model Invite { inviter User @relation(fields: [inviterId], references: [id], onDelete: Cascade, onUpdate: Cascade) inviterId String - - Zipline Zipline @relation(fields: [ziplineId], references: [id], onDelete: Cascade, onUpdate: Cascade) - ziplineId String } diff --git a/public/favicon.png b/public/favicon.png deleted file mode 100644 index 188762f0..00000000 Binary files a/public/favicon.png and /dev/null differ diff --git a/remix.config.js b/remix.config.js deleted file mode 100644 index 8ddcf6fe..00000000 --- a/remix.config.js +++ /dev/null @@ -1,27 +0,0 @@ -const { createRoutesFromFolders } = require('@remix-run/v1-route-convention'); - -/** - * @type {import('@remix-run/dev').AppConfig} - */ -module.exports = { - ignoredRouteFiles: ['**/.*'], - appDirectory: 'src/app', - // assetsBuildDirectory: 'public/build', - // serverBuildPath: 'build/index.js', - serverModuleFormat: 'cjs', - future: { - unstable_dev: true, - v2_routeConvention: true, - v2_errorBoundary: true, - v2_meta: true, - v2_normalizeFormMethod: true, - v2_headers: true, - }, - publicPath: '/modules/', - // use directory structure. - routes(defineRoutes) { - return createRoutesFromFolders(defineRoutes, { - appDirectory: 'src/app', - }); - }, -}; diff --git a/remix.env.d.ts b/remix.env.d.ts deleted file mode 100644 index 72e2affe..00000000 --- a/remix.env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/src/app/entry.client.tsx b/src/app/entry.client.tsx deleted file mode 100644 index 44ccc447..00000000 --- a/src/app/entry.client.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { RemixBrowser } from '@remix-run/react'; -import { hydrate } from 'react-dom'; -import { ClientProvider } from '@mantine/remix'; - -hydrate( - - - , - document -); - \ No newline at end of file diff --git a/src/app/entry.server.tsx b/src/app/entry.server.tsx deleted file mode 100644 index 27f24bab..00000000 --- a/src/app/entry.server.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { renderToString } from 'react-dom/server'; -import { RemixServer } from '@remix-run/react'; -import type { EntryContext } from '@remix-run/node'; -import { injectStyles, createStylesServer } from '@mantine/remix'; - -const server = createStylesServer(); - -export default function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - let markup = renderToString(); - responseHeaders.set('Content-Type', 'text/html'); - - return new Response(`${injectStyles(markup, server)}`, { - status: responseStatusCode, - headers: responseHeaders, - }); -} \ No newline at end of file diff --git a/src/app/loader.ts b/src/app/loader.ts deleted file mode 100644 index 166385b9..00000000 --- a/src/app/loader.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { LoaderArgs } from '@remix-run/node'; -import type { Config } from 'lib/config/Config'; - -export type TypedLoaderArgs = { - context: T; -} & Omit; - -export type RouteContext = { - config: Config; -}; diff --git a/src/app/root.tsx b/src/app/root.tsx deleted file mode 100644 index 885a5778..00000000 --- a/src/app/root.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { V2_MetaFunction } from '@remix-run/node'; -import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react'; -import { MantineProvider, createEmotionCache } from '@mantine/core'; -import { StylesPlaceholder } from '@mantine/remix'; - -export const meta: V2_MetaFunction = () => [ - { charSet: 'utf-8' }, - { title: 'Zipline' }, - { name: 'viewport', content: 'width=device-width,initial-scale=1' }, -]; - -createEmotionCache({ key: 'mantine' }); - -export default function App() { - return ( - - - - - - - - - - - - - - - - ); -} diff --git a/src/app/routes/$.tsx b/src/app/routes/$.tsx deleted file mode 100644 index 0d760c61..00000000 --- a/src/app/routes/$.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { json } from '@remix-run/node'; -import { isRouteErrorResponse, useRouteError } from '@remix-run/react'; -import { RouteContext, TypedLoaderArgs } from '~/loader'; - -export const loader = async ({ params, context }: TypedLoaderArgs) => { - const slug = params['*']?.split('/').filter(Boolean) ?? []; - if (!slug.length) { - throw json('Not Found (no slug)', { status: 404 }); - } - - console.log(slug, context.config); - - if (slug[0] === context.config.files.route) { - } else { - throw json('Not Found (catchall)', { status: 404 }); - } - - return json({ slug }); -}; - -export function ErrorBoundary() { - const error = useRouteError(); - - if (isRouteErrorResponse(error)) { - return
{JSON.stringify(error, null, 2)}
; - } else if (error instanceof Error) { - return
{JSON.stringify(error, null, 2)}
; - } else { - return

Unknown error

; - } -} - -// export default function Index() { -// return

Index

; -// } diff --git a/src/app/routes/api/ping.ts b/src/app/routes/api/ping.ts deleted file mode 100644 index ca8e9c57..00000000 --- a/src/app/routes/api/ping.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { LoaderArgs, json } from '@remix-run/node'; -import { prisma } from '~/db.server'; - -export async function loader({ context, request }: LoaderArgs) { - try { - // test database connection - await prisma.user.count(); - - return json({ pong: true }, { status: 200 }); - } catch (e) { - return json({ pong: false }, { status: 500 }); - } -} diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx deleted file mode 100644 index e50d7d0f..00000000 --- a/src/app/routes/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { LoaderArgs, json } from '@remix-run/node'; -import { useLoaderData } from '@remix-run/react'; -import { prisma } from '~/db.server'; - -export async function loader({}: LoaderArgs) { - let zipline = await prisma.zipline.findFirst(); - if (!zipline) { - zipline = await prisma.zipline.create({ data: {} }); - } - - return json({ zipline }); -} - -export default function Index() { - const { zipline } = useLoaderData(); - - return ( -
-
{JSON.stringify(zipline, null, 2)}
-
- ); -} diff --git a/src/app/session.server.ts b/src/app/session.server.ts deleted file mode 100644 index 694e1ef6..00000000 --- a/src/app/session.server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createCookieSessionStorage } from "@remix-run/node"; - -let sessionSecret = process.env.SESSION_SECRET; -if (!sessionSecret) { - throw new Error("SESSION_SECRET must be set"); -} - -export let sessionStorage = createCookieSessionStorage({ - cookie: { - name: "__session", - secrets: [sessionSecret], - sameSite: "lax", - path: "/", - maxAge: 60 * 60 * 24 * 30, - httpOnly: true, - }, -}); - -const USER_SESSION_KEY = "userId"; - -export async function getSession(request: Request) { - const cookie = request.headers.get("Cookie"); - return sessionStorage.getSession(cookie); -} - diff --git a/src/app/sleep.ts b/src/app/sleep.ts deleted file mode 100644 index f59140d2..00000000 --- a/src/app/sleep.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function sleep(ms: number, value: T) { - return new Promise((resolve) => setTimeout(() => resolve(value), ms)); -} diff --git a/src/lib/config/Config.ts b/src/lib/config/Config.ts deleted file mode 100644 index 53076f01..00000000 --- a/src/lib/config/Config.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface Config { - core: ConfigCore; - files: ConfigFiles; -} - -export interface ConfigCore { - port: number; - sessionSecret: string; - databaseUrl: string; -} - -export interface ConfigFiles { - route: string; -} diff --git a/src/lib/config/convert.ts b/src/lib/config/convert.ts deleted file mode 100644 index 21ee8523..00000000 --- a/src/lib/config/convert.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ValidatedEnv } from './read'; - -export function convertEnv(env: ValidatedEnv) { - return { - core: { - port: env.PORT, - sessionSecret: env.SESSION_SECRET, - databaseUrl: env.DATABASE_URL, - }, - files: { - route: env.FILES_ROUTE, - }, - }; -} diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts new file mode 100644 index 00000000..a1b9c72c --- /dev/null +++ b/src/lib/config/index.ts @@ -0,0 +1,16 @@ +import { readEnv } from './read'; +import { validateEnv, Config } from './validate'; + +let config: Config; + +declare global { + var __config__: Config; +} + +if (!global.__config__) { + global.__config__ = validateEnv(readEnv()); +} + +config = global.__config__; + +export { config }; diff --git a/src/lib/config/read.ts b/src/lib/config/read.ts index 13efe028..c653c152 100644 --- a/src/lib/config/read.ts +++ b/src/lib/config/read.ts @@ -1,23 +1,120 @@ -import { log } from 'src/lib/logger'; -import { parseEnv } from 'znv'; -import { z } from 'zod'; +import bytes from 'bytes'; +import msFn from 'ms'; -const logger = log('config').c('read'); +type EnvType = 'string' | 'number' | 'boolean' | 'byte' | 'ms'; + +export type ParsedEnv = ReturnType; + +export const PROP_TO_ENV: Record = { + 'core.port': 'CORE_PORT', + 'core.hostname': 'CORE_HOSTNAME', + 'core.secret': 'CORE_SECRET', + 'core.databaseUrl': 'CORE_DATABASE_URL', + + 'files.route': 'FILES_ROUTE', +}; export function readEnv() { - logger.debug('reading env'); + const envs = [ + env(PROP_TO_ENV['core.port'], 'core.port', 'number'), + env(PROP_TO_ENV['core.hostname'], 'core.hostname', 'string'), + env(PROP_TO_ENV['core.secret'], 'core.secret', 'string'), + env(PROP_TO_ENV['core.databaseUrl'], 'core.databaseUrl', 'string'), - const validation = parseEnv(process.env, { - PORT: z.number().default(3000), - SESSION_SECRET: z.string(), - DATABASE_URL: z.string(), + env(PROP_TO_ENV['files.route'], 'files.route', 'string'), + ]; - FILES_ROUTE: z.string().default('u'), - }); + const raw = { + core: { + port: undefined, + hostname: undefined, + secret: undefined, + databaseUrl: undefined, + }, + files: { + route: undefined, + }, + }; - logger.debug('env read', JSON.stringify(validation)); + for (let i = 0; i !== envs.length; ++i) { + const env = envs[i]; + const value = process.env[env.variable]; + if (value === undefined) continue; - return validation; + const parsed = parse(value, env.type); + if (parsed === undefined) continue; + + setDotProp(raw, env.property, parsed); + } + + return raw; } -export type ValidatedEnv = ReturnType; +function env(variable: string, property: string, type: EnvType) { + return { + variable, + property, + type, + }; +} + +function setDotProp(obj: Record, property: string, value: unknown) { + const parts = property.split('.'); + const last = parts.pop()!; + + for (let i = 0; i !== parts.length; ++i) { + const part = parts[i]; + const next = obj[part]; + + if (typeof next === 'object' && next !== null) { + obj = next; + } else { + obj = obj[part] = {}; + } + } + + obj[last] = value; +} + +function parse(value: string, type: EnvType) { + switch (type) { + case 'string': + return string(value); + case 'number': + return number(value); + case 'boolean': + return boolean(value); + case 'byte': + return byte(value); + case 'ms': + return ms(value); + default: + return undefined; + } +} + +function string(value: string) { + return value; +} + +function number(value: string) { + const num = Number(value); + if (isNaN(num)) return undefined; + + return num; +} + +function boolean(value: string) { + if (value === 'true') return true; + if (value === 'false') return false; + + return undefined; +} + +function byte(value: string) { + return bytes(value); +} + +function ms(value: string) { + return msFn(value); +} diff --git a/src/lib/config/validate.ts b/src/lib/config/validate.ts new file mode 100644 index 00000000..a2bf96da --- /dev/null +++ b/src/lib/config/validate.ts @@ -0,0 +1,68 @@ +import { ZodError, z } from 'zod'; +import { PROP_TO_ENV, ParsedEnv } from './read'; +import { log } from '../logger'; + +const schema = z.object({ + core: z.object({ + port: z.number().default(3000), + hostname: z.string().default('localhost'), + secret: z.string().superRefine((s, c) => { + if (s === 'changethis') + return c.addIssue({ + code: 'custom', + message: 'Secret must be changed from the default value', + path: ['core', 'secret'], + }); + + if (s.length <= 16) { + return c.addIssue({ + code: 'too_small', + minimum: 16, + type: 'string', + inclusive: true, + message: 'Secret must contain at least 16 characters', + path: ['core', 'secret'], + exact: false, + }); + } + }), + databaseUrl: z.string().url(), + }), + files: z.object({ + route: z.string().default('u'), + }), +}); + +export type Config = z.infer; + +export function validateEnv(env: ParsedEnv): Config { + const logger = log('config').c('validate'); + + try { + const validated = schema.parse(env); + + if (!validated) { + logger.error('There was an error while validating the environment.'); + process.exit(1); + } + + return validated; + } catch (e) { + if (e instanceof ZodError) { + logger.error(`There were ${e.errors.length} error(s) while validating the environment.`); + + for (let i = 0; i !== e.errors.length; ++i) { + const error = e.errors[i]; + logger.debug(JSON.stringify(error)); + + const path = PROP_TO_ENV[error.path.join('.')]; + + logger.error(`${path}: ${error.message}`); + } + + process.exit(1); + } + + throw e; + } +} diff --git a/src/lib/cookie.ts b/src/lib/cookie.ts new file mode 100644 index 00000000..bb2151d4 --- /dev/null +++ b/src/lib/cookie.ts @@ -0,0 +1,19 @@ +export function serializeCookie( + name: string, + value: string, + options: { + expires?: Date; + maxAge?: number; + path?: string; + sameSite?: 'strict' | 'lax' | 'none'; + } = {} +) { + const cookie = [`${name}=${value}`]; + + if (options.expires) cookie.push(`Expires=${options.expires.toUTCString()}`); + if (options.maxAge) cookie.push(`Max-Age=${options.maxAge}`); + if (options.path) cookie.push(`Path=${options.path}`); + if (options.sameSite) cookie.push(`SameSite=${options.sameSite}`); + + return cookie.join('; '); +} diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts new file mode 100644 index 00000000..7d5c65e8 --- /dev/null +++ b/src/lib/crypto.ts @@ -0,0 +1,97 @@ +import crypto from 'crypto'; +import { hash, verify } from 'argon2'; + +const ALGORITHM = 'aes-256-cbc'; + +export function createKey(secret: string) { + const hash = crypto.createHash('sha256'); + hash.update(secret); + + return hash.digest('hex').slice(0, 32); +} + +export function encrypt(value: string, secret: string): string { + const key = createKey(secret); + const iv = crypto.randomBytes(16); + + const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(key), iv); + + const encrypted = cipher.update(value); + const final = cipher.final(); + + const buffer = Buffer.alloc(encrypted.length + final.length); + buffer.set(encrypted); + buffer.set(final, encrypted.length); + + return iv.toString('hex') + '.' + buffer.toString('hex'); +} + +export function decrypt(value: string, secret: string): string { + const key = createKey(secret); + const [iv, encrypted] = value.split('.'); + + const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(key), Buffer.from(iv, 'hex')); + + const decrypted = decipher.update(Buffer.from(encrypted, 'hex')); + const final = decipher.final(); + + const buffer = Buffer.alloc(decrypted.length + final.length); + buffer.set(decrypted); + buffer.set(final, decrypted.length); + + return buffer.toString(); +} + +export function randomCharacters(length: number): string { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + + let result = ''; + + for (let i = 0; i !== length; ++i) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + + return result; +} + +export function createToken(): string { + const date = Date.now(); + const random = randomCharacters(32); + + const date64 = Buffer.from(date.toString()).toString('base64'); + const random64 = Buffer.from(random).toString('base64'); + + return `${date64}.${random64}`; +} + +export function encryptToken(token: string, secret: string): string { + const key = createKey(secret); + + const date = Date.now(); + const date64 = Buffer.from(date.toString()).toString('base64'); + + const encrypted = encrypt(token, key); + const encrypted64 = Buffer.from(encrypted).toString('base64'); + + return `${date64}.${encrypted64}`; +} + +export function decryptToken(encryptedToken: string, secret: string): [number, string] { + const key = createKey(secret); + const [date64, encrypted64] = encryptedToken.split('.'); + + const date = parseInt(Buffer.from(date64, 'base64').toString('ascii'), 10); + + const encrypted = Buffer.from(encrypted64, 'base64').toString('ascii'); + + return [date, decrypt(encrypted, key)]; +} + +export async function hashPassword(password: string) { + return hash(password); +} + +export async function verifyPassword(password: string, hash: string) { + return verify(hash, password); +} diff --git a/src/app/db.server.ts b/src/lib/db/index.ts similarity index 93% rename from src/app/db.server.ts rename to src/lib/db/index.ts index 5611a64f..4cf0e4b2 100644 --- a/src/app/db.server.ts +++ b/src/lib/db/index.ts @@ -1,5 +1,5 @@ import { PrismaClient } from '@prisma/client'; -import { log } from 'src/lib/logger'; +import { log } from '@/lib/logger'; let prisma: PrismaClient; diff --git a/src/lib/migration/index.ts b/src/lib/db/migration/index.ts similarity index 97% rename from src/lib/migration/index.ts rename to src/lib/db/migration/index.ts index 3827a9ee..7eb51205 100644 --- a/src/lib/migration/index.ts +++ b/src/lib/db/migration/index.ts @@ -1,6 +1,6 @@ import { Migrate } from '@prisma/migrate/dist/Migrate'; import { ensureDatabaseExists } from '@prisma/migrate/dist/utils/ensureDatabaseExists'; -import { log } from 'lib/logger'; +import { log } from '@/lib/logger'; export async function runMigrations() { const migrate = new Migrate('./prisma/schema.prisma'); diff --git a/src/lib/migration/types.d.ts b/src/lib/db/migration/types.d.ts similarity index 100% rename from src/lib/migration/types.d.ts rename to src/lib/db/migration/types.d.ts diff --git a/src/lib/db/queries/user.ts b/src/lib/db/queries/user.ts new file mode 100644 index 00000000..26ac344f --- /dev/null +++ b/src/lib/db/queries/user.ts @@ -0,0 +1,45 @@ +import { Prisma } from '@prisma/client'; +import { prisma } from '..'; + +export type User = { + id: string; + username: string; + createdAt: Date; + updatedAt: Date; + administrator: boolean; + avatar?: string | null; + password?: string | null; +}; + +export async function getUser( + where: Prisma.UserWhereInput | Prisma.UserWhereUniqueInput, + options?: { password?: boolean; avatar?: boolean } +): Promise { + return prisma.user.findFirst({ + where, + select: { + administrator: true, + avatar: options?.avatar || false, + id: true, + createdAt: true, + updatedAt: true, + password: options?.password || false, + username: true, + }, + }); +} + +export async function getUserTokenRaw( + where: Prisma.UserWhereInput | Prisma.UserWhereUniqueInput +): Promise { + const user = await prisma.user.findFirst({ + where, + select: { + token: true, + }, + }); + + if (!user) return null; + + return user.token; +} diff --git a/src/lib/db/queries/zipline.ts b/src/lib/db/queries/zipline.ts new file mode 100644 index 00000000..05a7c506 --- /dev/null +++ b/src/lib/db/queries/zipline.ts @@ -0,0 +1,12 @@ +import { prisma } from '..'; + +export async function getZipline() { + const zipline = await prisma.zipline.findFirst(); + if (!zipline) { + return prisma.zipline.create({ + data: {}, + }); + } + + return zipline; +} diff --git a/src/lib/logger.ts b/src/lib/logger.ts index f59bfe6e..2a9326e0 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -1,5 +1,5 @@ import dayjs from 'dayjs'; -import { blue, green, red, yellow, gray, white, bold } from 'colorette'; +import { green, red, yellow, gray, white, bold } from 'colorette'; export type LoggerLevel = 'info' | 'warn' | 'error' | 'debug' | 'trace'; diff --git a/src/lib/middleware/combine.ts b/src/lib/middleware/combine.ts new file mode 100644 index 00000000..a03f0cb3 --- /dev/null +++ b/src/lib/middleware/combine.ts @@ -0,0 +1,10 @@ +import { NextApiReq, NextApiRes } from '../response'; + +export function combine(middleware: Middleware[], handler: Handler) { + return middleware.reduceRight((handler, middleware) => { + return middleware(handler); + }, handler); +} + +export type Middleware = (...args: any[]) => Handler; +export type Handler = (req: NextApiReq, res: NextApiRes) => Promise; diff --git a/src/lib/middleware/cors.ts b/src/lib/middleware/cors.ts new file mode 100644 index 00000000..14ad06c9 --- /dev/null +++ b/src/lib/middleware/cors.ts @@ -0,0 +1,19 @@ +import { NextApiReq, NextApiRes } from '../response'; +import { Handler } from './combine'; + +export function cors() { + return (handler: Handler) => { + return async (req: NextApiReq, res: NextApiRes) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept'); + res.setHeader('Access-Control-Max-Age', '86400'); + + if (req.method === 'OPTIONS') { + return res.status(200).end(); + } + + return handler(req, res); + }; + }; +} diff --git a/src/lib/middleware/method.ts b/src/lib/middleware/method.ts new file mode 100644 index 00000000..9d802eb9 --- /dev/null +++ b/src/lib/middleware/method.ts @@ -0,0 +1,19 @@ +import { NextApiReq, NextApiRes, methodNotAllowed } from '../response'; +import { Handler } from './combine'; + +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'; + +export function method(allowedMethods: HttpMethod[] = []) { + return (handler: Handler) => { + return async (req: NextApiReq, res: NextApiRes) => { + allowedMethods.push('OPTIONS'); + + if (!allowedMethods.includes(req.method as HttpMethod)) { + res.setHeader('Allow', allowedMethods.join(', ')); + return methodNotAllowed(res); + } + + return handler(req, res); + }; + }; +} diff --git a/src/lib/middleware/ziplineAuth.ts b/src/lib/middleware/ziplineAuth.ts new file mode 100644 index 00000000..6c931ffc --- /dev/null +++ b/src/lib/middleware/ziplineAuth.ts @@ -0,0 +1,40 @@ +import { config } from '../config'; +import { decryptToken } from '../crypto'; +import { prisma } from '../db'; +import { NextApiReq, NextApiRes, forbidden, unauthorized } from '../response'; +import { Handler } from './combine'; + +export type ZiplineAuthOptions = { + administratorOnly?: boolean; +}; + +export function ziplineAuth(options: ZiplineAuthOptions) { + return (handler: Handler) => { + return async (req: NextApiReq, res: NextApiRes) => { + let rawToken: string | undefined; + + if (req.cookies.zipline_auth) rawToken = req.cookies.zipline_auth; + else if (req.headers.authorization) rawToken = req.headers.authorization; + + if (!rawToken) return unauthorized(res); + + const [date, token] = decryptToken(rawToken, config.core.secret); + + if (isNaN(new Date(date).getTime())) return unauthorized(res); + + const user = await prisma.user.findUnique({ + where: { + token, + }, + }); + + if (!user) return unauthorized(res); + + req.user = user; + + if (options.administratorOnly && !user.administrator) return forbidden(res); + + return handler(req, res); + }; + }; +} diff --git a/src/lib/response.ts b/src/lib/response.ts new file mode 100644 index 00000000..01390104 --- /dev/null +++ b/src/lib/response.ts @@ -0,0 +1,96 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { User } from './db/queries/user'; + +export interface NextApiReq extends NextApiRequest { + query: Query & { [k: string]: string | string[] }; + body: Body; + headers: Headers & { [k: string]: string }; + + user?: User; +} + +export type NextApiRes = NextApiResponse; + +export function ok(res: NextApiRes, data: Record = {}) { + return res.status(200).json(data); +} + +// Client wrong data, etc +export function badRequest( + res: NextApiRes, + message: string = 'Bad Request', + data: Record = {} +) { + return res.status(400).json({ + error: message, + ...data, + }); +} + +// No authorization +export function unauthorized( + res: NextApiRes, + message: string = 'Unauthorized', + data: Record = {} +) { + return res.status(401).json({ + error: message, + ...data, + }); +} + +// User's permission level does not meet requirements for this resource +export function forbidden( + res: NextApiRes, + message: string = 'Forbidden', + data: Record = {} +) { + return res.status(403).json({ + error: message, + ...data, + }); +} + +export function notFound(res: NextApiRes, message: string = 'Not Found', data: Record = {}) { + return res.status(404).json({ + error: message, + ...data, + }); +} + +export function ratelimited( + res: NextApiRes, + retryAfter: number, + message: string = 'Ratelimited', + data: Record = {} +) { + res.setHeader('Retry-After', retryAfter); + return res.status(429).json({ + error: message, + retryAfter, + ...data, + }); +} + +export function serverError( + res: NextApiRes, + message: string = 'Internal Server Error', + data: Record = {} +) { + return res.status(500).json({ + error: message, + ...data, + }); +} + +export function methodNotAllowed( + res: NextApiRes, + message: string = 'Method Not Allowed', + data: Record = {} +) { + return res.status(405).json({ + error: message, + method: res.req?.method || 'unknown', + ...data, + }); +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx new file mode 100644 index 00000000..8cdf6791 --- /dev/null +++ b/src/pages/_app.tsx @@ -0,0 +1,26 @@ +import { AppProps } from 'next/app'; +import Head from 'next/head'; +import { MantineProvider } from '@mantine/core'; + +export default function App(props: AppProps) { + const { Component, pageProps } = props; + + return ( + <> + + Zipline + + + + + + + + ); +} diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx new file mode 100644 index 00000000..9cb1980d --- /dev/null +++ b/src/pages/_document.tsx @@ -0,0 +1,20 @@ +import { createGetInitialProps } from '@mantine/next'; +import Document, { Head, Html, Main, NextScript } from 'next/document'; + +const getInitialProps = createGetInitialProps(); + +export default class _Document extends Document { + static getInitialProps = getInitialProps; + + render() { + return ( + + + +
+ + + + ); + } +} \ No newline at end of file diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts new file mode 100644 index 00000000..8c75c963 --- /dev/null +++ b/src/pages/api/auth/login.ts @@ -0,0 +1,56 @@ +import { config } from '@/lib/config'; +import { serializeCookie } from '@/lib/cookie'; +import { encryptToken, verifyPassword } from '@/lib/crypto'; +import { User, getUser, getUserTokenRaw } from '@/lib/db/queries/user'; +import { combine } from '@/lib/middleware/combine'; +import { cors } from '@/lib/middleware/cors'; +import { method } from '@/lib/middleware/method'; +import { NextApiReq, NextApiRes, badRequest, ok } from '@/lib/response'; + +type Data = { + user: User; + token: string; +}; + +type Body = { + username: string; + password: string; +} + +async function handler(req: NextApiReq, res: NextApiRes) { + const { username, password } = req.body; + + if (!username) return badRequest(res, 'Username is required'); + if (!password) return badRequest(res, 'Password is required'); + + const user = await getUser({ username }, { password: true }); + if (!user) return badRequest(res, 'Invalid username'); + + if (!user.password) return badRequest(res, 'User does not have a password, login through a provider'); + const valid = await verifyPassword(password, user.password); + if (!valid) return badRequest(res, 'Invalid password'); + + const rawToken = await getUserTokenRaw({ id: user.id }); + if (!rawToken) return badRequest(res, 'User does not have a token'); + + const token = encryptToken(rawToken, config.core.secret); + + const cookie = serializeCookie('zipline_token', token, { + // week + maxAge: 60 * 60 * 24 * 7, + expires: new Date(Date.now() + 60 * 60 * 24 * 7 * 1000), + path: '/', + sameSite: 'lax', + }); + + res.setHeader('Set-Cookie', cookie); + + delete user.password; + + return ok(res, { + user, + token, + }); +} + +export default combine([cors(), method(['POST'])], handler); diff --git a/src/pages/api/healthcheck.ts b/src/pages/api/healthcheck.ts new file mode 100644 index 00000000..a5344ab2 --- /dev/null +++ b/src/pages/api/healthcheck.ts @@ -0,0 +1,51 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { prisma } from '@/lib/db'; +import { log } from '@/lib/logger'; +import { badRequest, ok, ratelimited, serverError } from '@/lib/response'; +import { combine } from '@/lib/middleware/combine'; +import { cors } from '@/lib/middleware/cors'; +import { method } from '@/lib/middleware/method'; + +type Data = { + pass: boolean; +}; + +const ratelimit: Map = new Map(); + +export async function handler(req: NextApiRequest, res: NextApiResponse) { + const logger = log('api').c('healthcheck'); + + const ip = (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress; + if (!ip) { + logger.debug(`request without an ip address blocked`); + return badRequest(res, 'no ip address found'); + } + + const last = ratelimit.get(ip); + + if (last) { + if (last && Date.now() - last < 10000) { + logger.debug(`request from ${ip} blocked due to ratelimit`); + return ratelimited(res, Math.ceil((last + 10000 - Date.now()) / 1000)); + } else { + ratelimit.delete(ip); + } + } + + try { + await prisma.$queryRaw`SELECT 1;`; + ratelimit.set(ip, Date.now()); + + return ok(res, { pass: true }); + } catch (e) { + logger.error('there was an error during a healthcheck').error(e); + ratelimit.set(ip, Date.now()); + + return serverError(res, 'there was an error during a healthcheck', { + pass: false, + }); + } +} + +export default combine([cors(), method(['GET'])], handler); diff --git a/src/pages/api/setup.ts b/src/pages/api/setup.ts new file mode 100644 index 00000000..eb01c543 --- /dev/null +++ b/src/pages/api/setup.ts @@ -0,0 +1,74 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { prisma } from '@/lib/db'; +import { log } from '@/lib/logger'; +import { getZipline } from '@/lib/db/queries/zipline'; +import { NextApiReq, NextApiRes, badRequest, forbidden, methodNotAllowed, ok } from '@/lib/response'; +import { combine } from '@/lib/middleware/combine'; +import { cors } from '@/lib/middleware/cors'; +import { method } from '@/lib/middleware/method'; +import { createToken, hashPassword } from '@/lib/crypto'; +import { User } from '@/lib/db/queries/user'; + +type Response = { + firstSetup: boolean; + user: User; +}; + +type Body = { + username: string; + password: string; +}; + +export async function handler(req: NextApiReq, res: NextApiRes) { + const logger = log('api').c('setup'); + const { firstSetup, id } = await getZipline(); + + if (!firstSetup) return forbidden(res); + + logger.info('first setup running'); + + if (req.method === 'GET') { + return ok(res, { firstSetup }); + } + + const { username, password } = req.body; + if (!username) return badRequest(res, 'Username is required'); + if (!password) return badRequest(res, 'Password is required'); + + if (password.length < 8) return badRequest(res, 'Password must be at least 8 characters long'); + + const user = await prisma.user.create({ + data: { + username, + password: await hashPassword(password), + administrator: true, + token: createToken(), + }, + select: { + administrator: true, + id: true, + createdAt: true, + updatedAt: true, + username: true, + }, + }); + + logger.info('first setup complete'); + + await prisma.zipline.update({ + where: { + id, + }, + data: { + firstSetup: false, + }, + }); + + return ok(res, { + firstSetup, + user, + }); +} + +export default combine([cors(), method(['GET', 'POST'])], handler); diff --git a/src/pages/api/user/index.ts b/src/pages/api/user/index.ts new file mode 100644 index 00000000..1cc7bc03 --- /dev/null +++ b/src/pages/api/user/index.ts @@ -0,0 +1,13 @@ +import { User } from '@/lib/db/queries/user'; +import { combine } from '@/lib/middleware/combine'; +import { cors } from '@/lib/middleware/cors'; +import { method } from '@/lib/middleware/method'; +import { NextApiReq, NextApiRes } from '@/lib/response'; + +type Response = { + user: User; +}; + +export async function handler(req: NextApiReq, res: NextApiRes) {} + +export default combine([cors(), method(['GET', 'POST', 'PATCH'])], handler); diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx new file mode 100644 index 00000000..38346fdf --- /dev/null +++ b/src/pages/auth/login.tsx @@ -0,0 +1,13 @@ +import { Box, Center, Text, TextInput, Title } from '@mantine/core'; + +export default function Login() { + return ( + +
+ + <b>Zipline</b> + +
+
+ ); +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 00000000..0cae69e8 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,13 @@ +import Head from 'next/head'; + +export default function Home() { + return ( + <> + + Zipline + + +
hi
+ + ); +} diff --git a/src/server/index.ts b/src/server/index.ts index bc9430b3..a5ff7fc4 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,58 +1,50 @@ +import { validateEnv } from '@/lib/config/validate'; +import { readEnv } from '@/lib/config/read'; +import { createToken, decryptToken, encryptToken } from '@/lib/crypto'; +import { runMigrations } from '@/lib/db/migration'; +import { log } from '@/lib/logger'; import express from 'express'; -import { join } from 'path'; -import { createRequestHandler } from '@remix-run/express'; -import { convertEnv } from 'src/lib/config/convert'; -import { log } from 'src/lib/logger'; -import { readEnv } from 'src/lib/config/read'; -import { runMigrations } from 'src/lib/migration'; +import next from 'next'; +import { parse } from 'url'; const MODE = process.env.NODE_ENV || 'production'; -const BUILD_DIR = join(process.cwd(), 'build'); const logger = log('server'); -logger.info(`starting zipline in ${MODE} mode`); +async function main() { + logger.info(`starting zipline in ${MODE} mode`); -runMigrations().then(() => {}); + const server = express(); -const server = express(); -const config = convertEnv(readEnv()); + logger.info('reading environment for configuration'); + const config = validateEnv(readEnv()); -server.disable('x-powered-by'); + process.env.DATABASE_URL = config.core.databaseUrl; -server.use('/modules', express.static('public/build', { maxAge: '1y', immutable: true })); -server.use(express.static('public', { maxAge: '1h' })); + await runMigrations(); -server.all( - '*', - MODE === 'production' - ? createRequestHandler({ build: require(BUILD_DIR) }) - : (...args) => { - purgeRequireCache(); - const requestHandler = createRequestHandler({ - build: require(BUILD_DIR), - mode: MODE, - getLoadContext() { - return { - config, - }; - }, - }); - return requestHandler(...args); - } -); + server.disable('x-powered-by'); + server.use(express.static('public', { maxAge: '1h' })); -server.listen(3000, () => { - require(BUILD_DIR); + const app = next({ + dev: MODE === 'development', + quiet: MODE === 'production', + hostname: config.core.hostname, + port: config.core.port, + dir: '.', + }); + const handle = app.getRequestHandler(); - logger.info(`server listening on port ${config.core.port}`); -}); + await app.prepare(); -function purgeRequireCache() { - for (const key in require.cache) { - if (key.startsWith(BUILD_DIR)) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete require.cache[key]; - } - } + server.all('*', (req, res) => { + const parsedUrl = parse(req.url!, true); + return handle(req, res, parsedUrl); + }); + + server.listen(config.core.port, config.core.hostname, () => { + logger.info(`server listening on port ${config.core.port}`); + }); } + +main(); diff --git a/tsconfig.json b/tsconfig.json index 2dcf1e82..84020d50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,23 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2019"], - "esModuleInterop": true, - "jsx": "react-jsx", - "moduleResolution": "node", "target": "esnext", - "strict": true, - "skipLibCheck": true, - "baseUrl": ".", - "paths": { - "~/*": ["src/app/*"], - "lib/*": ["src/lib/*"] - }, - "forceConsistentCasingInFileNames": true, + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, - "isolatedModules": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", "resolveJsonModule": true, - "noEmit": true - } + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index d78be02b..1d658ee4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3440,6 +3440,20 @@ __metadata: languageName: node linkType: hard +"@mantine/next@npm:^6.0.14": + version: 6.0.14 + resolution: "@mantine/next@npm:6.0.14" + dependencies: + "@mantine/ssr": 6.0.14 + "@mantine/styles": 6.0.14 + peerDependencies: + next: "*" + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: d610f6f353b5184610fd2f1cfd51de47d9a7f2006f9c74661c7911ff64e86c41876404e6642a76fb549600579d9a164b4f0b1de645ceeec78094a6ab922a4525 + languageName: node + linkType: hard + "@mantine/notifications@npm:^6.0.14": version: 6.0.14 resolution: "@mantine/notifications@npm:6.0.14" @@ -3455,33 +3469,17 @@ __metadata: languageName: node linkType: hard -"@mantine/prism@npm:^6.0.14": +"@mantine/nprogress@npm:^6.0.14": version: 6.0.14 - resolution: "@mantine/prism@npm:6.0.14" + resolution: "@mantine/nprogress@npm:6.0.14" dependencies: "@mantine/utils": 6.0.14 - prism-react-renderer: ^1.2.1 peerDependencies: "@mantine/core": 6.0.14 "@mantine/hooks": 6.0.14 react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: a5e554a4ff310949ee3104e50f7d7b9086046b2a2fdaa705ec6b18c152963f5d2a416e7a165a33f5b053c23a0cf7c95cc2bdc0b9ddd34b0abf9efacb91351c2c - languageName: node - linkType: hard - -"@mantine/remix@npm:^6.0.14": - version: 6.0.14 - resolution: "@mantine/remix@npm:6.0.14" - dependencies: - "@mantine/ssr": 6.0.14 - "@mantine/styles": 6.0.14 - peerDependencies: - "@mantine/core": 6.0.14 - "@mantine/hooks": 6.0.14 - react: ">=16.8.0" - react-dom: ">=16.8.0" - checksum: 52a131ebd7369ea240dd96464263e2f4440073ce32cddaa0810f5157924223828715a9644058a1d108e7a6bb56827ead474e110002f37dd771a6043bcb854219 + checksum: a26d216f3869624a512409d148f5eea6edeee1f29688ef8113f7a5229e051ed3d5d0bed9a13746a53d2d490e391cd8ca5a5ea22133aa868532d920a498620722 languageName: node linkType: hard @@ -3523,6 +3521,95 @@ __metadata: languageName: node linkType: hard +"@mapbox/node-pre-gyp@npm:^1.0.10": + version: 1.0.10 + resolution: "@mapbox/node-pre-gyp@npm:1.0.10" + dependencies: + detect-libc: ^2.0.0 + https-proxy-agent: ^5.0.0 + make-dir: ^3.1.0 + node-fetch: ^2.6.7 + nopt: ^5.0.0 + npmlog: ^5.0.1 + rimraf: ^3.0.2 + semver: ^7.3.5 + tar: ^6.1.11 + bin: + node-pre-gyp: bin/node-pre-gyp + checksum: 1a98db05d955b74dad3814679593df293b9194853698f3f5f1ed00ecd93128cdd4b14fb8767fe44ac6981ef05c23effcfdc88710e7c1de99ccb6f647890597c8 + languageName: node + linkType: hard + +"@next/env@npm:13.4.7": + version: 13.4.7 + resolution: "@next/env@npm:13.4.7" + checksum: 5a2bba68fb8c80c87324025f10af7fe7319efdb15777247bfa8ff58e61bcc19b150bce4068396351e6c6df3344294cc06c03a2fb1bb0330659d230830a202c53 + languageName: node + linkType: hard + +"@next/swc-darwin-arm64@npm:13.4.7": + version: 13.4.7 + resolution: "@next/swc-darwin-arm64@npm:13.4.7" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-darwin-x64@npm:13.4.7": + version: 13.4.7 + resolution: "@next/swc-darwin-x64@npm:13.4.7" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@next/swc-linux-arm64-gnu@npm:13.4.7": + version: 13.4.7 + resolution: "@next/swc-linux-arm64-gnu@npm:13.4.7" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-arm64-musl@npm:13.4.7": + version: 13.4.7 + resolution: "@next/swc-linux-arm64-musl@npm:13.4.7" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-linux-x64-gnu@npm:13.4.7": + version: 13.4.7 + resolution: "@next/swc-linux-x64-gnu@npm:13.4.7" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-x64-musl@npm:13.4.7": + version: 13.4.7 + resolution: "@next/swc-linux-x64-musl@npm:13.4.7" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-win32-arm64-msvc@npm:13.4.7": + version: 13.4.7 + resolution: "@next/swc-win32-arm64-msvc@npm:13.4.7" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-win32-ia32-msvc@npm:13.4.7": + version: 13.4.7 + resolution: "@next/swc-win32-ia32-msvc@npm:13.4.7" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@next/swc-win32-x64-msvc@npm:13.4.7": + version: 13.4.7 + resolution: "@next/swc-win32-x64-msvc@npm:13.4.7" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -3604,6 +3691,13 @@ __metadata: languageName: node linkType: hard +"@phc/format@npm:^1.0.0": + version: 1.0.0 + resolution: "@phc/format@npm:1.0.0" + checksum: 15ee02504fbc16590923d89b1f1c2f5892df27cf2bf19180e5678511413e87b6e5355815a092749cd01698855ee5a0fc5d2393951c727acd650934eed290e26e + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -4041,47 +4135,6 @@ __metadata: languageName: node linkType: hard -"@remix-run/express@npm:^1.17.1": - version: 1.17.1 - resolution: "@remix-run/express@npm:1.17.1" - dependencies: - "@remix-run/node": 1.17.1 - peerDependencies: - express: ^4.17.1 - checksum: b6109837f0f6d5c91794e137c68ea25eec40e43bcdecf63c6c80a3b38b1c27d34f06c91da54df6e3f2d16e6d65979f743fe58dd18bc85d2fe8bd7e38ef72a13f - languageName: node - linkType: hard - -"@remix-run/node@npm:1.17.1, @remix-run/node@npm:^1.16.1": - version: 1.17.1 - resolution: "@remix-run/node@npm:1.17.1" - dependencies: - "@remix-run/server-runtime": 1.17.1 - "@remix-run/web-fetch": ^4.3.4 - "@remix-run/web-file": ^3.0.2 - "@remix-run/web-stream": ^1.0.3 - "@web3-storage/multipart-parser": ^1.0.0 - abort-controller: ^3.0.0 - cookie-signature: ^1.1.0 - source-map-support: ^0.5.21 - stream-slice: ^0.1.2 - checksum: 4e4d984ca1dc01f7c50983f99fbbbc97cb0629cac4cedad3970fc3591207c8d1ff690cfa07451dc3b1f9ad30ae75200bb21006b3793548d89a64daa40d5bf5eb - languageName: node - linkType: hard - -"@remix-run/react@npm:^1.16.1": - version: 1.17.1 - resolution: "@remix-run/react@npm:1.17.1" - dependencies: - "@remix-run/router": 1.6.3 - react-router-dom: 6.13.0 - peerDependencies: - react: ">=16.8" - react-dom: ">=16.8" - checksum: 59f573c7d78af33b77ae4df76895ccb70cd5e11f73653567732a3a32dd92d192c984a1ccd3d178df214e66505e80a30b8b26b03bc829dc98ed4452db977a3c43 - languageName: node - linkType: hard - "@remix-run/router@npm:1.6.3": version: 1.6.3 resolution: "@remix-run/router@npm:1.6.3" @@ -4102,69 +4155,6 @@ __metadata: languageName: node linkType: hard -"@remix-run/v1-route-convention@npm:^0.1.2": - version: 0.1.2 - resolution: "@remix-run/v1-route-convention@npm:0.1.2" - dependencies: - minimatch: ^7.4.3 - peerDependencies: - "@remix-run/dev": ^1.15.0 - checksum: c057b4fd696e4bda869663be757fda2b647604ee65441882a2cf4aa5ea76e6630e293a4110817891cc8eab06574e04c60e6306f1e3b02dbfa21231882fc7a722 - languageName: node - linkType: hard - -"@remix-run/web-blob@npm:^3.0.3, @remix-run/web-blob@npm:^3.0.4": - version: 3.0.4 - resolution: "@remix-run/web-blob@npm:3.0.4" - dependencies: - "@remix-run/web-stream": ^1.0.0 - web-encoding: 1.1.5 - checksum: 07d9a71d1795e8973cdc59c1a325aaaae7b9099a96815849355a34667d7a953cac78a332e02b25e0722d4d7244b7fe6d7ce6fc854e8baf83e42e8403f4a321fd - languageName: node - linkType: hard - -"@remix-run/web-fetch@npm:^4.3.4": - version: 4.3.4 - resolution: "@remix-run/web-fetch@npm:4.3.4" - dependencies: - "@remix-run/web-blob": ^3.0.4 - "@remix-run/web-form-data": ^3.0.3 - "@remix-run/web-stream": ^1.0.3 - "@web3-storage/multipart-parser": ^1.0.0 - abort-controller: ^3.0.0 - data-uri-to-buffer: ^3.0.1 - mrmime: ^1.0.0 - checksum: 0c3370bfde1867722654734cddfab90982ba657a4244bf8e7e4aa49cb5e3c0b3429ed94b760d6881b8bf45d93bf411f29b9ffaf19332ecdc0bae1fa42b9a06fb - languageName: node - linkType: hard - -"@remix-run/web-file@npm:^3.0.2": - version: 3.0.2 - resolution: "@remix-run/web-file@npm:3.0.2" - dependencies: - "@remix-run/web-blob": ^3.0.3 - checksum: f3bda87b62648e3ef0c0049aa560318d64adf493566a6446eae5a9d15a6080eb0c8ba1f450d4d7bbfa6cbad8c8d6a7adf4e72d546a4305ce0c05f63a95f80db0 - languageName: node - linkType: hard - -"@remix-run/web-form-data@npm:^3.0.3": - version: 3.0.4 - resolution: "@remix-run/web-form-data@npm:3.0.4" - dependencies: - web-encoding: 1.1.5 - checksum: 75c4c07c3307081d17d63b6d26209c651e5ccb910b96fa467415dcceb3f5e7c82d18cc34f604ffc5e85e2d3e77b93b9c2ef670b97745deccfe03cf85647cca17 - languageName: node - linkType: hard - -"@remix-run/web-stream@npm:^1.0.0, @remix-run/web-stream@npm:^1.0.3": - version: 1.0.3 - resolution: "@remix-run/web-stream@npm:1.0.3" - dependencies: - web-streams-polyfill: ^3.1.1 - checksum: 61a76b9e4ddb364fa5faa8cf28484f39800bd9259d4c2b96c235bd436e539d8bb00d433fcf79730d7bdf103255473fe0ccfde5d3c20a152f738c5bb102b26377 - languageName: node - linkType: hard - "@rollup/pluginutils@npm:^4.0.0": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" @@ -4244,6 +4234,15 @@ __metadata: languageName: node linkType: hard +"@swc/helpers@npm:0.5.1": + version: 0.5.1 + resolution: "@swc/helpers@npm:0.5.1" + dependencies: + tslib: ^2.4.0 + checksum: 71e0e27234590435e4c62b97ef5e796f88e786841a38c7116a5e27a3eafa7b9ead7cdec5249b32165902076de78446945311c973e59bddf77c1e24f33a8f272a + languageName: node + linkType: hard + "@szmarczak/http-timer@npm:^4.0.5": version: 4.0.6 resolution: "@szmarczak/http-timer@npm:4.0.6" @@ -4253,6 +4252,25 @@ __metadata: languageName: node linkType: hard +"@tabler/icons-react@npm:^2.22.0": + version: 2.22.0 + resolution: "@tabler/icons-react@npm:2.22.0" + dependencies: + "@tabler/icons": 2.22.0 + prop-types: ^15.7.2 + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + checksum: fe2a4c3e5483269ee178195746cdc4b8c1e150dec78aae4bbf3884f33dd819929a7ff1e53eb6ba1426b914ba710e13a30d0e70caf9e588b8df45e500f70941dd + languageName: node + linkType: hard + +"@tabler/icons@npm:2.22.0": + version: 2.22.0 + resolution: "@tabler/icons@npm:2.22.0" + checksum: 3f0aaa801e8739d841ac5d335fbaee41399aaa9eeae03eb21c1dbe77877a29ba3664f16c225323a0635fad6548aca40212220edfb2919797454ea1029c5a5b78 + languageName: node + linkType: hard + "@tediousjs/connection-string@npm:^0.4.1": version: 0.4.2 resolution: "@tediousjs/connection-string@npm:0.4.2" @@ -4444,6 +4462,13 @@ __metadata: languageName: node linkType: hard +"@types/http-errors@npm:*": + version: 2.0.1 + resolution: "@types/http-errors@npm:2.0.1" + checksum: 3bb0c50b0a652e679a84c30cd0340d696c32ef6558518268c238840346c077f899315daaf1c26c09c57ddd5dc80510f2a7f46acd52bf949e339e35ed3ee9654f + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.9": version: 7.0.12 resolution: "@types/json-schema@npm:7.0.12" @@ -4621,12 +4646,13 @@ __metadata: linkType: hard "@types/serve-static@npm:*": - version: 1.15.1 - resolution: "@types/serve-static@npm:1.15.1" + version: 1.15.2 + resolution: "@types/serve-static@npm:1.15.2" dependencies: + "@types/http-errors": "*" "@types/mime": "*" "@types/node": "*" - checksum: 2e078bdc1e458c7dfe69e9faa83cc69194b8896cce57cb745016580543c7ab5af07fdaa8ac1765eb79524208c81017546f66056f44d1204f812d72810613de36 + checksum: 15c261dbfc57890f7cc17c04d5b22b418dfa0330c912b46c5d8ae2064da5d6f844ef7f41b63c7f4bbf07675e97ebe6ac804b032635ec742ae45d6f1274259b3e languageName: node linkType: hard @@ -4847,29 +4873,13 @@ __metadata: languageName: node linkType: hard -"@zxing/text-encoding@npm:0.9.0": - version: 0.9.0 - resolution: "@zxing/text-encoding@npm:0.9.0" - checksum: c23b12aee7639382e4949961304a1294776afaffa40f579e09ffecd0e5e68cf26ef3edd75009de46da8a536e571448755ca68b3e2ea707d53793c0edb2e2c34a - languageName: node - linkType: hard - -"abbrev@npm:^1.0.0": +"abbrev@npm:1, abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 languageName: node linkType: hard -"abort-controller@npm:^3.0.0": - version: 3.0.0 - resolution: "abort-controller@npm:3.0.0" - dependencies: - event-target-shim: ^5.0.0 - checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 - languageName: node - linkType: hard - "accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -5075,6 +5085,16 @@ __metadata: languageName: node linkType: hard +"are-we-there-yet@npm:^2.0.0": + version: 2.0.0 + resolution: "are-we-there-yet@npm:2.0.0" + dependencies: + delegates: ^1.0.0 + readable-stream: ^3.6.0 + checksum: 6c80b4fd04ecee6ba6e737e0b72a4b41bdc64b7d279edfc998678567ff583c8df27e27523bc789f2c99be603ffa9eaa612803da1d886962d2086e7ff6fa90c7c + languageName: node + linkType: hard + "are-we-there-yet@npm:^3.0.0": version: 3.0.1 resolution: "are-we-there-yet@npm:3.0.1" @@ -5092,6 +5112,17 @@ __metadata: languageName: node linkType: hard +"argon2@npm:^0.30.3": + version: 0.30.3 + resolution: "argon2@npm:0.30.3" + dependencies: + "@mapbox/node-pre-gyp": ^1.0.10 + "@phc/format": ^1.0.0 + node-addon-api: ^5.0.0 + checksum: 36784f69af8adad1e0e155a0f1999320999a3b76fb41a3b8f4674e1d896dc65a6e76bea4385b9433e8f935d0912a9ca81088b5d8d22e5fe4119d5abf9d10761f + languageName: node + linkType: hard + "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -5576,6 +5607,15 @@ __metadata: languageName: node linkType: hard +"busboy@npm:1.6.0": + version: 1.6.0 + resolution: "busboy@npm:1.6.0" + dependencies: + streamsearch: ^1.1.0 + checksum: 32801e2c0164e12106bf236291a00795c3c4e4b709ae02132883fe8478ba2ae23743b11c5735a0aae8afe65ac4b6ca4568b91f0d9fed1fdbc32ede824a73746e + languageName: node + linkType: hard + "bytes@npm:3.1.2, bytes@npm:^3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" @@ -5675,6 +5715,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001406": + version: 1.0.30001507 + resolution: "caniuse-lite@npm:1.0.30001507" + checksum: 7044172bdf65140c927cdaaff50368a97676f06a9fd8b515c046613bdf52cb769e9efb832ee491b8f8cc21f82c15f154a896efbab690f431bb064c95f3a2b7a8 + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.30001503": version: 1.0.30001506 resolution: "caniuse-lite@npm:1.0.30001506" @@ -5833,6 +5880,13 @@ __metadata: languageName: node linkType: hard +"client-only@npm:0.0.1": + version: 0.0.1 + resolution: "client-only@npm:0.0.1" + checksum: 0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8 + languageName: node + linkType: hard + "clone-response@npm:^1.0.2": version: 1.0.3 resolution: "clone-response@npm:1.0.3" @@ -5888,7 +5942,7 @@ __metadata: languageName: node linkType: hard -"color-support@npm:^1.1.3": +"color-support@npm:^1.1.2, color-support@npm:^1.1.3": version: 1.1.3 resolution: "color-support@npm:1.1.3" bin: @@ -5960,7 +6014,7 @@ __metadata: languageName: node linkType: hard -"console-control-strings@npm:^1.1.0": +"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed @@ -5997,13 +6051,6 @@ __metadata: languageName: node linkType: hard -"cookie-signature@npm:^1.1.0": - version: 1.2.1 - resolution: "cookie-signature@npm:1.2.1" - checksum: bb464aacac390b5d7d8ead2d6fff7c1c3b7378c7d0250921f48923fe889688e081ab33950448929db5f24d4f9f1506589a7ee1c685de8f12a3fdb30c49667ec5 - languageName: node - linkType: hard - "cookie@npm:0.5.0": version: 0.5.0 resolution: "cookie@npm:0.5.0" @@ -6146,7 +6193,7 @@ __metadata: languageName: node linkType: hard -"data-uri-to-buffer@npm:3, data-uri-to-buffer@npm:^3.0.1": +"data-uri-to-buffer@npm:3": version: 3.0.1 resolution: "data-uri-to-buffer@npm:3.0.1" checksum: c59c3009686a78c071806b72f4810856ec28222f0f4e252aa495ec027ed9732298ceea99c50328cf59b151dd34cbc3ad6150bbb43e41fc56fa19f48c99e9fc30 @@ -6404,6 +6451,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.0": + version: 2.0.1 + resolution: "detect-libc@npm:2.0.1" + checksum: ccb05fcabbb555beb544d48080179c18523a343face9ee4e1a86605a8715b4169f94d663c21a03c310ac824592f2ba9a5270218819bb411ad7be578a527593d7 + languageName: node + linkType: hard + "detect-newline@npm:3.1.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -7506,13 +7560,6 @@ __metadata: languageName: node linkType: hard -"event-target-shim@npm:^5.0.0": - version: 5.0.1 - resolution: "event-target-shim@npm:5.0.1" - checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166 - languageName: node - linkType: hard - "events@npm:^3.0.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -8024,6 +8071,23 @@ __metadata: languageName: node linkType: hard +"gauge@npm:^3.0.0": + version: 3.0.2 + resolution: "gauge@npm:3.0.2" + dependencies: + aproba: ^1.0.3 || ^2.0.0 + color-support: ^1.1.2 + console-control-strings: ^1.0.0 + has-unicode: ^2.0.1 + object-assign: ^4.1.1 + signal-exit: ^3.0.0 + string-width: ^4.2.3 + strip-ansi: ^6.0.1 + wide-align: ^1.1.2 + checksum: 81296c00c7410cdd48f997800155fbead4f32e4f82109be0719c63edc8560e6579946cc8abd04205297640691ec26d21b578837fd13a4e96288ab4b40b1dc3e9 + languageName: node + linkType: hard + "gauge@npm:^4.0.3": version: 4.0.4 resolution: "gauge@npm:4.0.4" @@ -8163,6 +8227,13 @@ __metadata: languageName: node linkType: hard +"glob-to-regexp@npm:^0.4.1": + version: 0.4.1 + resolution: "glob-to-regexp@npm:0.4.1" + checksum: e795f4e8f06d2a15e86f76e4d92751cf8bbfcf0157cea5c2f0f35678a8195a750b34096b1256e436f0cebc1883b5ff0888c47348443e69546a5a87f9e1eb1167 + languageName: node + linkType: hard + "glob@npm:7.1.6": version: 7.1.6 resolution: "glob@npm:7.1.6" @@ -8842,7 +8913,7 @@ __metadata: languageName: node linkType: hard -"is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1": +"is-arguments@npm:^1.1.1": version: 1.1.1 resolution: "is-arguments@npm:1.1.1" dependencies: @@ -8976,15 +9047,6 @@ __metadata: languageName: node linkType: hard -"is-generator-function@npm:^1.0.7": - version: 1.0.10 - resolution: "is-generator-function@npm:1.0.10" - dependencies: - has-tostringtag: ^1.0.0 - checksum: d54644e7dbaccef15ceb1e5d91d680eb5068c9ee9f9eb0a9e04173eb5542c9b51b5ab52c5537f5703e48d5fddfd376817c1ca07a84a407b7115b769d4bdde72b - languageName: node - linkType: hard - "is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" @@ -9165,7 +9227,7 @@ __metadata: languageName: node linkType: hard -"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": +"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.9": version: 1.1.10 resolution: "is-typed-array@npm:1.1.10" dependencies: @@ -9248,13 +9310,6 @@ __metadata: languageName: node linkType: hard -"isbot@npm:^3.6.10": - version: 3.6.12 - resolution: "isbot@npm:3.6.12" - checksum: e23782a6633bf60fc3db23171468fc3f4cdc36f5a707b0cf1f3b2aff2686bedb29d036411afec90a9de23901a251ff53596ab9ca31cc85223dd8f03392fc03e6 - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -9832,7 +9887,7 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:3.1.0, make-dir@npm:^3.0.0, make-dir@npm:^3.0.2": +"make-dir@npm:3.1.0, make-dir@npm:^3.0.0, make-dir@npm:^3.0.2, make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" dependencies: @@ -10540,15 +10595,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^7.4.3": - version: 7.4.6 - resolution: "minimatch@npm:7.4.6" - dependencies: - brace-expansion: ^2.0.1 - checksum: 1a6c8d22618df9d2a88aabeef1de5622eb7b558e9f8010be791cb6b0fa6e102d39b11c28d75b855a1e377b12edc7db8ff12a99c20353441caa6a05e78deb5da9 - languageName: node - linkType: hard - "minimatch@npm:^9.0.0, minimatch@npm:^9.0.1": version: 9.0.1 resolution: "minimatch@npm:9.0.1" @@ -10756,13 +10802,6 @@ __metadata: languageName: node linkType: hard -"mrmime@npm:^1.0.0": - version: 1.0.1 - resolution: "mrmime@npm:1.0.1" - checksum: cc979da44bbbffebaa8eaf7a45117e851f2d4cb46a3ada6ceb78130466a04c15a0de9a9ce1c8b8ba6f6e1b8618866b1352992bf1757d241c0ddca558b9f28a77 - languageName: node - linkType: hard - "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -10828,7 +10867,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.6": +"nanoid@npm:^3.3.4, nanoid@npm:^3.3.6": version: 3.3.6 resolution: "nanoid@npm:3.3.6" bin: @@ -10879,6 +10918,65 @@ __metadata: languageName: node linkType: hard +"next@npm:^13.4.7": + version: 13.4.7 + resolution: "next@npm:13.4.7" + dependencies: + "@next/env": 13.4.7 + "@next/swc-darwin-arm64": 13.4.7 + "@next/swc-darwin-x64": 13.4.7 + "@next/swc-linux-arm64-gnu": 13.4.7 + "@next/swc-linux-arm64-musl": 13.4.7 + "@next/swc-linux-x64-gnu": 13.4.7 + "@next/swc-linux-x64-musl": 13.4.7 + "@next/swc-win32-arm64-msvc": 13.4.7 + "@next/swc-win32-ia32-msvc": 13.4.7 + "@next/swc-win32-x64-msvc": 13.4.7 + "@swc/helpers": 0.5.1 + busboy: 1.6.0 + caniuse-lite: ^1.0.30001406 + postcss: 8.4.14 + styled-jsx: 5.1.1 + watchpack: 2.4.0 + zod: 3.21.4 + peerDependencies: + "@opentelemetry/api": ^1.1.0 + fibers: ">= 3.1.0" + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + dependenciesMeta: + "@next/swc-darwin-arm64": + optional: true + "@next/swc-darwin-x64": + optional: true + "@next/swc-linux-arm64-gnu": + optional: true + "@next/swc-linux-arm64-musl": + optional: true + "@next/swc-linux-x64-gnu": + optional: true + "@next/swc-linux-x64-musl": + optional: true + "@next/swc-win32-arm64-msvc": + optional: true + "@next/swc-win32-ia32-msvc": + optional: true + "@next/swc-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + fibers: + optional: true + sass: + optional: true + bin: + next: dist/bin/next + checksum: 76026a5def68c00064bc4860cd15a5f292220ccc73ff24245b3658a90a46f66c290d3543a59e1cb91310145141d4ad1238d7cf652f41f47cdf434ab8705af7d1 + languageName: node + linkType: hard + "nice-try@npm:^1.0.4": version: 1.0.5 resolution: "nice-try@npm:1.0.5" @@ -10902,7 +11000,16 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:2.6.11, node-fetch@npm:^2.6.9": +"node-addon-api@npm:^5.0.0": + version: 5.1.0 + resolution: "node-addon-api@npm:5.1.0" + dependencies: + node-gyp: latest + checksum: 2508bd2d2981945406243a7bd31362fc7af8b70b8b4d65f869c61731800058fb818cc2fd36c8eac714ddd0e568cc85becf5e165cebbdf7b5024d5151bbc75ea1 + languageName: node + linkType: hard + +"node-fetch@npm:2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": version: 2.6.11 resolution: "node-fetch@npm:2.6.11" dependencies: @@ -10944,6 +11051,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:^5.0.0": + version: 5.0.0 + resolution: "nopt@npm:5.0.0" + dependencies: + abbrev: 1 + bin: + nopt: bin/nopt.js + checksum: d35fdec187269503843924e0114c0c6533fb54bbf1620d0f28b4b60ba01712d6687f62565c55cc20a504eff0fbe5c63e22340c3fad549ad40469ffb611b04f2f + languageName: node + linkType: hard + "nopt@npm:^6.0.0": version: 6.0.0 resolution: "nopt@npm:6.0.0" @@ -11050,6 +11168,18 @@ __metadata: languageName: node linkType: hard +"npmlog@npm:^5.0.1": + version: 5.0.1 + resolution: "npmlog@npm:5.0.1" + dependencies: + are-we-there-yet: ^2.0.0 + console-control-strings: ^1.1.0 + gauge: ^3.0.0 + set-blocking: ^2.0.0 + checksum: 516b2663028761f062d13e8beb3f00069c5664925871a9b57989642ebe09f23ab02145bf3ab88da7866c4e112cafff72401f61a672c7c8a20edc585a7016ef5f + languageName: node + linkType: hard + "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -11835,6 +11965,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:8.4.14": + version: 8.4.14 + resolution: "postcss@npm:8.4.14" + dependencies: + nanoid: ^3.3.4 + picocolors: ^1.0.0 + source-map-js: ^1.0.2 + checksum: fe58766ff32e4becf65a7d57678995cfd239df6deed2fe0557f038b47c94e4132e7e5f68b5aa820c13adfec32e523b693efaeb65798efb995ce49ccd83953816 + languageName: node + linkType: hard + "postcss@npm:^8.4.19, postcss@npm:^8.4.23": version: 8.4.24 resolution: "postcss@npm:8.4.24" @@ -11919,15 +12060,6 @@ __metadata: languageName: node linkType: hard -"prism-react-renderer@npm:^1.2.1": - version: 1.3.5 - resolution: "prism-react-renderer@npm:1.3.5" - peerDependencies: - react: ">=0.14.9" - checksum: c18806dcbc4c0b4fd6fd15bd06b4f7c0a6da98d93af235c3e970854994eb9b59e23315abb6cfc29e69da26d36709a47e25da85ab27fed81b6812f0a52caf6dfa - languageName: node - linkType: hard - "prisma@npm:^4.16.1": version: 4.16.1 resolution: "prisma@npm:4.16.1" @@ -11981,7 +12113,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.6.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -12212,30 +12344,6 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:6.13.0": - version: 6.13.0 - resolution: "react-router-dom@npm:6.13.0" - dependencies: - "@remix-run/router": 1.6.3 - react-router: 6.13.0 - peerDependencies: - react: ">=16.8" - react-dom: ">=16.8" - checksum: f51131063c2d5e127b6b3f3f813c6d4988d0f37694a06697dc9d4a4d9d3825e2a4487ec9b81a1d356eb269018814d884ffc2e3d9ff056a46ae59c99c9e7e1086 - languageName: node - linkType: hard - -"react-router@npm:6.13.0": - version: 6.13.0 - resolution: "react-router@npm:6.13.0" - dependencies: - "@remix-run/router": 1.6.3 - peerDependencies: - react: ">=16.8" - checksum: 31a187005d05e063c59324564a283cd28052eaf848ad446c87658f4fc48fa9543329fe8a14d7be14d9bbf62410d383f8cf1cf13898a838bf9c1e3201fe93384c - languageName: node - linkType: hard - "react-style-singleton@npm:^2.2.1": version: 2.2.1 resolution: "react-style-singleton@npm:2.2.1" @@ -12958,7 +13066,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -13236,10 +13344,10 @@ __metadata: languageName: node linkType: hard -"stream-slice@npm:^0.1.2": - version: 0.1.2 - resolution: "stream-slice@npm:0.1.2" - checksum: 027111397bd709f299fb1bb34902baf4707bba8851219c9115df673be1075a2cecf54d8671e6258c94483d1fa4e931c6784e49f2e005b1b6d5e3b8b61028fbe1 +"streamsearch@npm:^1.1.0": + version: 1.1.0 + resolution: "streamsearch@npm:1.1.0" + checksum: 1cce16cea8405d7a233d32ca5e00a00169cc0e19fbc02aa839959985f267335d435c07f96e5e0edd0eadc6d39c98d5435fb5bbbdefc62c41834eadc5622ad942 languageName: node linkType: hard @@ -13456,6 +13564,22 @@ __metadata: languageName: node linkType: hard +"styled-jsx@npm:5.1.1": + version: 5.1.1 + resolution: "styled-jsx@npm:5.1.1" + dependencies: + client-only: 0.0.1 + peerDependencies: + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + checksum: 523a33b38603492547e861b98e29c873939b04e15fbe5ef16132c6f1e15958126647983c7d4675325038b428a5e91183d996e90141b18bdd1bbadf6e2c45b2fa + languageName: node + linkType: hard + "stylis@npm:4.2.0": version: 4.2.0 resolution: "stylis@npm:4.2.0" @@ -14313,19 +14437,6 @@ __metadata: languageName: node linkType: hard -"util@npm:^0.12.3": - version: 0.12.5 - resolution: "util@npm:0.12.5" - dependencies: - inherits: ^2.0.3 - is-arguments: ^1.0.4 - is-generator-function: ^1.0.7 - is-typed-array: ^1.1.3 - which-typed-array: ^1.1.2 - checksum: 705e51f0de5b446f4edec10739752ac25856541e0254ea1e7e45e5b9f9b0cb105bc4bd415736a6210edc68245a7f903bf085ffb08dd7deb8a0e847f60538a38a - languageName: node - linkType: hard - "utils-merge@npm:1.0.1": version: 1.0.1 resolution: "utils-merge@npm:1.0.1" @@ -14480,6 +14591,16 @@ __metadata: languageName: node linkType: hard +"watchpack@npm:2.4.0": + version: 2.4.0 + resolution: "watchpack@npm:2.4.0" + dependencies: + glob-to-regexp: ^0.4.1 + graceful-fs: ^4.1.2 + checksum: 23d4bc58634dbe13b86093e01c6a68d8096028b664ab7139d58f0c37d962d549a940e98f2f201cecdabd6f9c340338dc73ef8bf094a2249ef582f35183d1a131 + languageName: node + linkType: hard + "wcwidth@npm:^1.0.1": version: 1.0.1 resolution: "wcwidth@npm:1.0.1" @@ -14489,26 +14610,6 @@ __metadata: languageName: node linkType: hard -"web-encoding@npm:1.1.5": - version: 1.1.5 - resolution: "web-encoding@npm:1.1.5" - dependencies: - "@zxing/text-encoding": 0.9.0 - util: ^0.12.3 - dependenciesMeta: - "@zxing/text-encoding": - optional: true - checksum: 2234a2b122f41006ce07859b3c0bf2e18f46144fda2907d5db0b571b76aa5c26977c646100ad9c00d2f8a4f6f2b848bc02147845d8c447ab365ec4eff376338d - languageName: node - linkType: hard - -"web-streams-polyfill@npm:^3.1.1": - version: 3.2.1 - resolution: "web-streams-polyfill@npm:3.2.1" - checksum: b119c78574b6d65935e35098c2afdcd752b84268e18746606af149e3c424e15621b6f1ff0b42b2676dc012fc4f0d313f964b41a4b5031e525faa03997457da02 - languageName: node - linkType: hard - "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -14586,7 +14687,7 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.2, which-typed-array@npm:^1.1.9": +"which-typed-array@npm:^1.1.9": version: 1.1.9 resolution: "which-typed-array@npm:1.1.9" dependencies: @@ -14622,7 +14723,7 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.5": +"wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" dependencies: @@ -14807,24 +14908,22 @@ __metadata: "@mantine/form": ^6.0.14 "@mantine/hooks": ^6.0.14 "@mantine/modals": ^6.0.14 + "@mantine/next": ^6.0.14 "@mantine/notifications": ^6.0.14 - "@mantine/prism": ^6.0.14 - "@mantine/remix": ^6.0.14 + "@mantine/nprogress": ^6.0.14 "@prisma/client": 4.16.1 "@prisma/internals": ^4.16.1 "@prisma/migrate": ^4.16.1 "@remix-run/dev": ^1.16.1 "@remix-run/eslint-config": ^1.16.1 - "@remix-run/express": ^1.17.1 - "@remix-run/node": ^1.16.1 - "@remix-run/react": ^1.16.1 - "@remix-run/v1-route-convention": ^0.1.2 + "@tabler/icons-react": ^2.22.0 "@types/bytes": ^3.1.1 "@types/express": ^4.17.17 "@types/node": ^20.3.1 "@types/react": ^18.2.7 "@types/react-dom": ^18.2.4 "@types/signale": ^1.4.4 + argon2: ^0.30.3 bytes: ^3.1.2 colorette: ^2.0.20 cross-env: ^7.0.3 @@ -14832,8 +14931,8 @@ __metadata: dotenv: ^16.1.3 eslint: ^8.41.0 express: ^4.18.2 - isbot: ^3.6.10 ms: ^2.1.3 + next: ^13.4.7 npm-run-all: ^4.1.5 prisma: ^4.16.1 react: ^18.2.0 @@ -14856,7 +14955,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.21.4": +"zod@npm:3.21.4, zod@npm:^3.21.4": version: 3.21.4 resolution: "zod@npm:3.21.4" checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f