mirror of
https://github.com/diced/zipline.git
synced 2026-07-07 12:26:59 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57ab8deacb | |||
| 95ffa476f1 | |||
| f313ae1881 |
+2
-2
@@ -24,8 +24,8 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
COPY src ./src
|
||||
COPY .gitignore ./.gitignore
|
||||
|
||||
COPY postcss.config.cjs ./postcss.config.cjs
|
||||
COPY prettier.config.cjs ./prettier.config.cjs
|
||||
COPY postcss.config.mjs ./postcss.config.mjs
|
||||
COPY prettier.config.mjs ./prettier.config.mjs
|
||||
COPY eslint.config.mjs ./eslint.config.mjs
|
||||
COPY vite.config.ts ./vite.config.ts
|
||||
COPY tsup.config.ts ./tsup.config.ts
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
set -e
|
||||
|
||||
cd ${ZIPLINE_ROOT:-/zipline}
|
||||
exec node --enable-source-maps build/server
|
||||
exec node --enable-source-maps build/server/index.js
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
set -e
|
||||
|
||||
cd ${ZIPLINE_ROOT:-/zipline}
|
||||
exec node --enable-source-maps build/ctl "$@"
|
||||
exec node --enable-source-maps build/ctl/index.js "$@"
|
||||
|
||||
|
||||
+13
-12
@@ -3,14 +3,15 @@
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"version": "4.6.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsx scripts/build.ts",
|
||||
"dev": "cross-env NODE_ENV=development DEBUG=zipline tsx --require ./src/dotenv.js --enable-source-maps ./src/server",
|
||||
"dev:nd": "cross-env NODE_ENV=development tsx --require ./src/dotenv.js --enable-source-maps ./src/server",
|
||||
"dev:inspector": "cross-env NODE_ENV=development DEBUG=zipline tsx --require ./src/dotenv.js --inspect=0.0.0.0:9229 --enable-source-maps ./src/server",
|
||||
"start": "cross-env NODE_ENV=production node --trace-warnings --require ./src/dotenv.js ./build/server",
|
||||
"start:inspector": "cross-env NODE_ENV=production node --require ./src/dotenv.js --inspect=0.0.0.0:9229 --enable-source-maps ./build/server",
|
||||
"ctl": "NODE_ENV=production node --require ./src/dotenv.js --enable-source-maps ./build/ctl",
|
||||
"dev": "cross-env NODE_ENV=development DEBUG=zipline tsx --import ./src/dotenv.js --enable-source-maps ./src/server",
|
||||
"dev:nd": "cross-env NODE_ENV=development tsx --import ./src/dotenv.js --enable-source-maps ./src/server",
|
||||
"dev:inspector": "cross-env NODE_ENV=development DEBUG=zipline tsx --import ./src/dotenv.js --inspect=0.0.0.0:9229 --enable-source-maps ./src/server",
|
||||
"start": "cross-env NODE_ENV=production node --trace-warnings --import ./src/dotenv.js ./build/server/index.js",
|
||||
"start:inspector": "cross-env NODE_ENV=production node --import ./src/dotenv.js --inspect=0.0.0.0:9229 --enable-source-maps ./build/server/index.js",
|
||||
"ctl": "NODE_ENV=production node --import ./src/dotenv.js --enable-source-maps ./build/ctl/index.js",
|
||||
"validate": "tsx scripts/validate.ts",
|
||||
"openapi": "tsx scripts/openapi.ts",
|
||||
"db:prototype": "prisma db push --skip-generate && prisma generate --no-hints",
|
||||
@@ -42,11 +43,11 @@
|
||||
"@mantine/hooks": "^9.4.1",
|
||||
"@mantine/modals": "^9.4.1",
|
||||
"@mantine/notifications": "^9.4.1",
|
||||
"@prisma/adapter-pg": "6.13.0",
|
||||
"@prisma/client": "6.13.0",
|
||||
"@prisma/engines": "6.13.0",
|
||||
"@prisma/internals": "6.13.0",
|
||||
"@prisma/migrate": "6.13.0",
|
||||
"@prisma/adapter-pg": "7.8.0",
|
||||
"@prisma/client": "7.8.0",
|
||||
"@prisma/engines": "7.8.0",
|
||||
"@prisma/internals": "7.8.0",
|
||||
"@prisma/migrate": "7.8.0",
|
||||
"@simplewebauthn/browser": "^13.3.0",
|
||||
"@simplewebauthn/server": "^13.3.2",
|
||||
"@smithy/node-http-handler": "^4.9.3",
|
||||
@@ -74,7 +75,7 @@
|
||||
"ms": "^2.1.3",
|
||||
"nuqs": "^2.9.0",
|
||||
"otplib": "^13.4.1",
|
||||
"prisma": "6.13.0",
|
||||
"prisma": "7.8.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
|
||||
Generated
+2371
-5004
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @type {import('prettier').Config} */
|
||||
module.exports = {
|
||||
export default {
|
||||
singleQuote: true,
|
||||
jsxSingleQuote: true,
|
||||
printWidth: 110,
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'prisma/config';
|
||||
|
||||
try {
|
||||
process.loadEnvFile('.env');
|
||||
} catch {}
|
||||
|
||||
export default defineConfig({
|
||||
schema: './prisma/schema.prisma',
|
||||
migrations: {
|
||||
path: './prisma/migrations',
|
||||
},
|
||||
datasource: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
@@ -1,13 +1,11 @@
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/prisma"
|
||||
moduleFormat = "cjs"
|
||||
previewFeatures = ["queryCompiler", "driverAdapters"]
|
||||
provider = "prisma-client"
|
||||
output = "../src/prisma"
|
||||
moduleFormat = "esm"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Zipline {
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { run, step } from '.';
|
||||
import { API_ERRORS, ApiError, ApiErrorCode } from '../src/lib/api/errors';
|
||||
|
||||
const ALL_METHODS = ['delete', 'get', 'head', 'patch', 'post', 'put'];
|
||||
const GEN_PATH = path.resolve(__dirname, '..', 'openapi.json');
|
||||
const GEN_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'openapi.json');
|
||||
|
||||
const ALL_ERRORS = Object.keys(API_ERRORS)
|
||||
.map((code) => new ApiError(Number(code) as ApiErrorCode).toJSON())
|
||||
|
||||
@@ -57,9 +57,12 @@ function Form({ user, setUser, token }: { user: User; setUser: (u: User) => void
|
||||
initialValues: {
|
||||
username: user.username,
|
||||
password: '',
|
||||
currentPassword: '',
|
||||
},
|
||||
validate: {
|
||||
username: (value) => (value.length < 1 ? 'Username is required' : null),
|
||||
currentPassword: (value, values) =>
|
||||
values.password && !value ? 'Enter your current password to change it' : null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,16 +70,22 @@ function Form({ user, setUser, token }: { user: User; setUser: (u: User) => void
|
||||
const send: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
currentPassword?: string;
|
||||
} = {};
|
||||
|
||||
if (values.username !== user.username) send['username'] = values.username.trim();
|
||||
if (values.password) send['password'] = values.password.trim();
|
||||
if (values.password) {
|
||||
send['password'] = values.password.trim();
|
||||
send['currentPassword'] = values.currentPassword;
|
||||
}
|
||||
|
||||
const { data, error } = await fetchApi<Response['/api/user']>('/api/user', 'PATCH', send);
|
||||
|
||||
if (!data && error) {
|
||||
if (ApiError.check(error, 1039)) {
|
||||
form.setFieldError('username', error.error);
|
||||
} else if (ApiError.check(error, 1066) || ApiError.check(error, 1067)) {
|
||||
form.setFieldError('currentPassword', error.error);
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Error while updating user',
|
||||
@@ -91,6 +100,9 @@ function Form({ user, setUser, token }: { user: User; setUser: (u: User) => void
|
||||
|
||||
if (!data?.user) return;
|
||||
|
||||
form.setFieldValue('password', '');
|
||||
form.setFieldValue('currentPassword', '');
|
||||
|
||||
mutate('/api/user');
|
||||
mutate('/api/user/token');
|
||||
setUser(data.user);
|
||||
@@ -142,6 +154,15 @@ function Form({ user, setUser, token }: { user: User; setUser: (u: User) => void
|
||||
{...form.getInputProps('password')}
|
||||
leftSection={<IconAsteriskSimple size='1rem' />}
|
||||
/>
|
||||
{form.values.password && (
|
||||
<PasswordInput
|
||||
label='Current password'
|
||||
description='Required to change your password.'
|
||||
autoComplete='current-password'
|
||||
{...form.getInputProps('currentPassword')}
|
||||
leftSection={<IconAsteriskSimple size='1rem' />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button type='submit' mt='md' leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DATABASE_TO_PROP } from '@/lib/config/read/db';
|
||||
import { ENVS } from '@/lib/config/read/env';
|
||||
import { getProperty } from '@/lib/config/read/transform';
|
||||
import { validateConfigObject } from '@/lib/config/validate';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { randomCharacters } from '@/lib/random';
|
||||
|
||||
function convertValueToEnv(
|
||||
@@ -57,8 +58,6 @@ export async function exportConfig({ yml, showDefaults }: { yml?: boolean; showD
|
||||
|
||||
await reloadSettings();
|
||||
|
||||
const { prisma } = await import('@/lib/db/index.js');
|
||||
|
||||
const ziplineTable = await prisma.zipline.findFirst({
|
||||
omit: {
|
||||
id: true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { bytes } from '@/lib/bytes';
|
||||
import { config, reloadSettings } from '@/lib/config';
|
||||
import { getDatasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { guess } from '@/lib/mimes';
|
||||
import { statSync } from 'fs';
|
||||
import { mkdir, readdir } from 'fs/promises';
|
||||
@@ -14,7 +16,6 @@ export async function importDir(
|
||||
|
||||
await reloadSettings();
|
||||
|
||||
const { prisma } = await import('@/lib/db/index.js');
|
||||
let userId: string;
|
||||
|
||||
if (id) {
|
||||
@@ -85,7 +86,6 @@ export async function importDir(
|
||||
if (config.datasource.type === 'local')
|
||||
await mkdir(config.datasource.local!.directory, { recursive: true });
|
||||
|
||||
const { getDatasource } = await import('@/lib/datasource/index.js');
|
||||
const datasource = getDatasource(config);
|
||||
if (!datasource) return console.error('No datasource configured');
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { userSelect } from '@/lib/db/models/user';
|
||||
import { prisma } from '@/lib/db';
|
||||
|
||||
export async function listUsers({ extra, format, id }: { extra?: string[]; format?: boolean; id?: string }) {
|
||||
if (extra?.includes('list')) {
|
||||
@@ -20,7 +21,6 @@ export async function listUsers({ extra, format, id }: { extra?: string[]; forma
|
||||
}
|
||||
}
|
||||
|
||||
const { prisma } = await import('@/lib/db/index.js');
|
||||
const users = await prisma.user.findMany({
|
||||
where: id ? { id } : undefined,
|
||||
select,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { reloadSettings } from '@/lib/config';
|
||||
import { config, reloadSettings } from '@/lib/config';
|
||||
|
||||
export async function readConfig({ format }: { format: boolean }) {
|
||||
await reloadSettings();
|
||||
|
||||
const { config } = await import('@/lib/config/index.js');
|
||||
|
||||
console.log(JSON.stringify(config, null, format ? 2 : 0));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
const SUPPORTED_FIELDS = ['username', 'password', 'role', 'avatar', 'token', 'totpSecret'];
|
||||
|
||||
export async function setUser(property: string, value: string, { id }: { id: string }) {
|
||||
if (!SUPPORTED_FIELDS.includes(property)) return console.error('Unsupported field:', property);
|
||||
|
||||
const { prisma } = await import('@/lib/db/index.js');
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Command } from 'commander';
|
||||
import { version } from '../../package.json';
|
||||
import packageJson from '../../package.json' with { type: 'json' };
|
||||
import { listUsers } from './commands/list-users';
|
||||
import { readConfig } from './commands/read-config';
|
||||
import { setUser } from './commands/set-user';
|
||||
@@ -8,7 +8,7 @@ import { exportConfig } from './commands/export-config';
|
||||
|
||||
const cli = new Command();
|
||||
|
||||
cli.name('ziplinectl').version(version).description('controll utility for zipline');
|
||||
cli.name('ziplinectl').version(packageJson.version).description('control utility for zipline');
|
||||
|
||||
cli
|
||||
.command('read-config')
|
||||
|
||||
@@ -64,6 +64,8 @@ export const API_ERRORS = {
|
||||
1063: 'Already linked to this OAuth provider',
|
||||
1064: 'Invalid OAuth state parameter',
|
||||
1065: 'Invalid MIME type',
|
||||
1066: 'Current password is incorrect',
|
||||
1067: 'Current password is required to set a new password',
|
||||
|
||||
// 2xxx, session errors
|
||||
2000: 'Invalid login session',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import enabled from '../oauth/enabled';
|
||||
import { version } from '../../../package.json';
|
||||
import { version } from '../version';
|
||||
import { Config } from './validate';
|
||||
|
||||
export type SafeConfig = Omit<
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
import { Migrate } from '@prisma/migrate';
|
||||
import migratePkg from '@prisma/migrate';
|
||||
import { defineConfig } from 'prisma/config';
|
||||
import prismaInternals from '@prisma/internals';
|
||||
import { log } from '@/lib/logger';
|
||||
import { loadSchemaContext } from '@prisma/internals';
|
||||
|
||||
// @ts-ignore
|
||||
import { ensureDatabaseExists } from '@prisma/migrate/dist/utils/ensureDatabaseExists';
|
||||
import ensureDatabaseExistsPkg from '@prisma/migrate/dist/utils/ensureDatabaseExists.js';
|
||||
|
||||
const { Migrate } = migratePkg;
|
||||
const { ensureDatabaseExists } = ensureDatabaseExistsPkg;
|
||||
const {
|
||||
createSchemaPathInput,
|
||||
getSchemaDatasourceProvider,
|
||||
inferDirectoryConfig,
|
||||
loadSchemaContext,
|
||||
validatePrismaConfigWithDatasource,
|
||||
} = prismaInternals;
|
||||
|
||||
export async function runMigrations() {
|
||||
const baseDir = process.cwd();
|
||||
|
||||
const config = defineConfig({
|
||||
schema: './prisma/schema.prisma',
|
||||
migrations: { path: './prisma/migrations' },
|
||||
datasource: { url: process.env.DATABASE_URL! },
|
||||
});
|
||||
|
||||
const schemaContext = await loadSchemaContext({
|
||||
schemaPathFromArg: './prisma/schema.prisma',
|
||||
schemaPath: createSchemaPathInput({ schemaPathFromConfig: config.schema, baseDir }),
|
||||
printLoadMessage: false,
|
||||
});
|
||||
|
||||
const { migrationsDirPath } = inferDirectoryConfig(schemaContext, config);
|
||||
const validatedConfig = validatePrismaConfigWithDatasource({ config, cmd: 'migrate deploy' });
|
||||
|
||||
const migrate = await Migrate.setup({
|
||||
schemaEngineConfig: config,
|
||||
baseDir,
|
||||
migrationsDirPath,
|
||||
schemaContext,
|
||||
migrationsDirPath: './prisma/migrations',
|
||||
});
|
||||
|
||||
const logger = log('migrations');
|
||||
@@ -22,7 +45,11 @@ export async function runMigrations() {
|
||||
try {
|
||||
logger.debug('ensuring database exists...');
|
||||
|
||||
const dbCreated = await ensureDatabaseExists(schemaContext.primaryDatasource);
|
||||
const dbCreated = await ensureDatabaseExists(
|
||||
baseDir,
|
||||
getSchemaDatasourceProvider(schemaContext),
|
||||
validatedConfig,
|
||||
);
|
||||
if (dbCreated) {
|
||||
logger.info('database created');
|
||||
}
|
||||
@@ -30,7 +57,7 @@ export async function runMigrations() {
|
||||
logger.error('failed to create database' + e);
|
||||
logger.error('try creating the database manually and running the server again');
|
||||
|
||||
migrate.stop();
|
||||
await migrate.stop();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -42,10 +69,10 @@ export async function runMigrations() {
|
||||
} catch (e) {
|
||||
logger.error('failed to apply migrations' + e);
|
||||
|
||||
migrate.stop();
|
||||
await migrate.stop();
|
||||
process.exit(1);
|
||||
} finally {
|
||||
migrate.stop();
|
||||
await migrate.stop();
|
||||
}
|
||||
|
||||
if (migrationIds?.length === 0) {
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
declare module '@prisma/migrate' {
|
||||
export * from '@prisma/migrate/dist/src/index';
|
||||
}
|
||||
|
||||
declare module '@prisma/migrate/dist/utils/ensureDatabaseExists.js' {
|
||||
export * from '@prisma/migrate/dist/src/utils/ensureDatabaseExists';
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Worker } from 'worker_threads';
|
||||
import Logger, { log } from '../logger';
|
||||
import { config } from '../config';
|
||||
import { MAX_SAFE_TIMEOUT_MS } from '../config/validate';
|
||||
import { createWorker } from '../worker';
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
@@ -112,7 +113,7 @@ export class Tasks {
|
||||
task.data.config = config;
|
||||
}
|
||||
|
||||
const worker = new Worker(task.path, {
|
||||
const worker = createWorker(task.path, {
|
||||
workerData: task.data,
|
||||
});
|
||||
|
||||
|
||||
+12
-12
@@ -3,21 +3,21 @@ import { basename, join } from 'path';
|
||||
import { ZiplineTheme } from '.';
|
||||
import { exists } from '../fs';
|
||||
|
||||
import dark_gray from './builtins/dark_gray.theme.json';
|
||||
import light_gray from './builtins/light_gray.theme.json';
|
||||
import black_dark from './builtins/black_dark.theme.json';
|
||||
import dark_gray from './builtins/dark_gray.theme.json' with { type: 'json' };
|
||||
import light_gray from './builtins/light_gray.theme.json' with { type: 'json' };
|
||||
import black_dark from './builtins/black_dark.theme.json' with { type: 'json' };
|
||||
|
||||
import light_blue from './builtins/light_blue.theme.json';
|
||||
import dark_blue from './builtins/dark_blue.theme.json';
|
||||
import light_blue from './builtins/light_blue.theme.json' with { type: 'json' };
|
||||
import dark_blue from './builtins/dark_blue.theme.json' with { type: 'json' };
|
||||
|
||||
import cat_frappe from './builtins/catppuccin_frappe.theme.json';
|
||||
import cat_latte from './builtins/catppuccin_latte.theme.json';
|
||||
import cat_macchiato from './builtins/catppuccin_macchiato.theme.json';
|
||||
import cat_mocha from './builtins/catppuccin_mocha.theme.json';
|
||||
import cat_frappe from './builtins/catppuccin_frappe.theme.json' with { type: 'json' };
|
||||
import cat_latte from './builtins/catppuccin_latte.theme.json' with { type: 'json' };
|
||||
import cat_macchiato from './builtins/catppuccin_macchiato.theme.json' with { type: 'json' };
|
||||
import cat_mocha from './builtins/catppuccin_mocha.theme.json' with { type: 'json' };
|
||||
|
||||
import midnight_orange from './builtins/midnight_orange.theme.json';
|
||||
import midnight_blue from './builtins/midnight_blue.theme.json';
|
||||
import midnight_purple from './builtins/midnight_purple.theme.json';
|
||||
import midnight_orange from './builtins/midnight_orange.theme.json' with { type: 'json' };
|
||||
import midnight_blue from './builtins/midnight_blue.theme.json' with { type: 'json' };
|
||||
import midnight_purple from './builtins/midnight_purple.theme.json' with { type: 'json' };
|
||||
|
||||
import { log } from '../logger';
|
||||
|
||||
|
||||
+3
-1
@@ -1,9 +1,11 @@
|
||||
import { version } from '../../package.json';
|
||||
import packageJson from '../../package.json' with { type: 'json' };
|
||||
import { execSync } from 'child_process';
|
||||
import { log } from './logger';
|
||||
|
||||
const logger = log('version');
|
||||
|
||||
export const version = packageJson.version;
|
||||
|
||||
export function gitSha() {
|
||||
const envValue = process.env.ZIPLINE_GIT_SHA;
|
||||
if (envValue && envValue !== 'unknown') return envValue;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Worker, type WorkerOptions } from 'worker_threads';
|
||||
|
||||
export function workerUrl(pathFromRoot: string) {
|
||||
return new URL(pathFromRoot, new URL('../../build/', import.meta.url));
|
||||
}
|
||||
|
||||
export function createWorker(pathFromRoot: string, options: WorkerOptions) {
|
||||
return new Worker(workerUrl(pathFromRoot), {
|
||||
...options,
|
||||
type: 'module',
|
||||
} as WorkerOptions);
|
||||
}
|
||||
+1
-1
@@ -4,10 +4,10 @@ import { getDatasource } from '@/lib/datasource';
|
||||
import { runMigrations } from '@/lib/db/migration';
|
||||
import { log } from '@/lib/logger';
|
||||
import type { Tasks } from '@/lib/tasks';
|
||||
import { version } from '@/lib/version';
|
||||
import fastify from 'fastify';
|
||||
import { ZodTypeProvider } from 'fastify-type-provider-zod';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import { version } from '../../package.json';
|
||||
import { registerHandlers } from './startup/handlers';
|
||||
import { listenServer } from './startup/listen';
|
||||
import { startMemoryLog } from './startup/memory';
|
||||
|
||||
@@ -69,8 +69,6 @@ async function vitePlugin(fastify: FastifyInstance) {
|
||||
template = await readFile(resolve(`./src/client/ssr-${type}/`, 'index.html'), 'utf-8');
|
||||
template = await fastify.vite.transformIndexHtml(url, template);
|
||||
|
||||
// expose __dirname since dev modules are loaded in esm
|
||||
global.__dirname = __dirname;
|
||||
render = (await fastify.vite.ssrLoadModule(`/ssr-${type}/server.tsx`)).render;
|
||||
} else {
|
||||
template = await readFile(resolve('./build', `client/ssr-${type}/index.html`), 'utf-8');
|
||||
|
||||
@@ -9,7 +9,7 @@ import { zQsBoolean } from '@/lib/validation';
|
||||
import typedPlugin from '@/server/typedPlugin';
|
||||
import { cpus, hostname, platform, release } from 'os';
|
||||
import z from 'zod';
|
||||
import { version } from '../../../../../package.json';
|
||||
import { version } from '@/lib/version';
|
||||
|
||||
const exportCountsSchema = z.object({
|
||||
users: z.number(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { hashPassword } from '@/lib/crypto';
|
||||
import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { fileSelect } from '@/lib/db/models/file';
|
||||
import { userSelect } from '@/lib/db/models/user';
|
||||
import { sanitizeFilename } from '@/lib/fs';
|
||||
import { removeGps } from '@/lib/gps';
|
||||
import { log } from '@/lib/logger';
|
||||
@@ -117,7 +118,15 @@ export default typedPlugin(
|
||||
if (!files.length) throw new ApiError(1062);
|
||||
|
||||
const totalFileSize = files.reduce((acc, x) => acc + x.file.bytesRead, 0);
|
||||
const quotaCheck = await checkQuota(req.user, totalFileSize, files.length);
|
||||
|
||||
// use quota of user if anonymous
|
||||
const quotaUser = req.user
|
||||
? req.user
|
||||
: folder?.userId
|
||||
? await prisma.user.findUnique({ where: { id: folder.userId }, select: userSelect })
|
||||
: null;
|
||||
|
||||
const quotaCheck = await checkQuota(quotaUser, totalFileSize, files.length);
|
||||
if (quotaCheck !== true)
|
||||
throw new ApiError(5002, typeof quotaCheck === 'string' ? quotaCheck : undefined);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { bytes } from '@/lib/bytes';
|
||||
import { config } from '@/lib/config';
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { userSelect } from '@/lib/db/models/user';
|
||||
import { sanitizeFilename } from '@/lib/fs';
|
||||
import { log } from '@/lib/logger';
|
||||
import { guess } from '@/lib/mimes';
|
||||
@@ -15,7 +16,7 @@ import typedPlugin from '@/server/typedPlugin';
|
||||
import { z } from 'zod';
|
||||
import { readdir, rename, rm } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { Worker } from 'worker_threads';
|
||||
import { createWorker } from '@/lib/worker';
|
||||
import { ApiUploadResponse } from '.';
|
||||
|
||||
const logger = log('api').c('upload').c('partial');
|
||||
@@ -139,8 +140,15 @@ export default typedPlugin(
|
||||
const cache = partialsCache.get(options.partial.identifier);
|
||||
if (!cache) throw new ApiError(1003);
|
||||
|
||||
// use quota of user if anonymous
|
||||
const quotaUser = req.user
|
||||
? req.user
|
||||
: folder?.userId
|
||||
? await prisma.user.findUnique({ where: { id: folder.userId }, select: userSelect })
|
||||
: null;
|
||||
|
||||
// check quota, using the current added length, and only just adding one file
|
||||
const quotaCheck = await checkQuota(req.user, cache.length + fileSize, 1);
|
||||
const quotaCheck = await checkQuota(quotaUser, cache.length + fileSize, 1);
|
||||
if (quotaCheck !== true) {
|
||||
await deletePartial(options.partial.identifier);
|
||||
throw new ApiError(5002, typeof quotaCheck === 'string' ? quotaCheck : undefined);
|
||||
@@ -226,7 +234,7 @@ export default typedPlugin(
|
||||
config.files.route === '/' || config.files.route === '' ? '' : `${config.files.route}`
|
||||
}/${fileUpload.name}`;
|
||||
|
||||
const worker = new Worker('./build/offload/partial.js', {
|
||||
const worker = createWorker('offload/partial.js', {
|
||||
workerData: {
|
||||
user: {
|
||||
id: req.user ? req.user.id : options.folder ? folder?.userId : undefined,
|
||||
|
||||
@@ -147,7 +147,7 @@ export default typedPlugin(
|
||||
|
||||
const newFile = await prisma.file.update({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
id: file.id,
|
||||
},
|
||||
data,
|
||||
select: fileSelect,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { datasource } from '@/lib/datasource';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { sanitizeFilename } from '@/lib/fs';
|
||||
import { log } from '@/lib/logger';
|
||||
import { guess } from '@/lib/mimes';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { zQsBoolean } from '@/lib/validation';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
@@ -61,22 +62,37 @@ export default typedPlugin(
|
||||
if (thumbnail.file && thumbnail.file.userId !== req.user.id) {
|
||||
if (!canInteract(req.user.role, thumbnail.file.User?.role)) throw new ApiError(9002);
|
||||
}
|
||||
|
||||
const size = await datasource.size(thumbnail.path);
|
||||
if (!size) throw new ApiError(9002);
|
||||
|
||||
const buf = await datasource.get(thumbnail.path);
|
||||
if (!buf) throw new ApiError(9002);
|
||||
|
||||
return res
|
||||
.type(await guess(thumbnail.path.replace('.thumbnail-', '').split('.').pop() || 'jpg'))
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
})
|
||||
.status(200)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const file = await prisma.file.findFirst({
|
||||
where: {
|
||||
id,
|
||||
OR: [{ id }, { name: id }],
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
},
|
||||
});
|
||||
if (!file) throw new ApiError(9002);
|
||||
|
||||
if (file && file.userId !== req.user.id) {
|
||||
if (file.userId !== req.user.id) {
|
||||
if (!canInteract(req.user.role, file.User?.role)) throw new ApiError(9002);
|
||||
}
|
||||
|
||||
if (file?.deletesAt && file.deletesAt <= new Date()) {
|
||||
if (file.deletesAt && file.deletesAt <= new Date()) {
|
||||
try {
|
||||
await datasource.delete(file.name);
|
||||
await prisma.file.delete({
|
||||
@@ -95,7 +111,7 @@ export default typedPlugin(
|
||||
throw new ApiError(9002);
|
||||
}
|
||||
|
||||
if (file?.maxViews && file.views >= file.maxViews) {
|
||||
if (file.maxViews && file.views >= file.maxViews) {
|
||||
if (!config.features.deleteOnMaxViews) throw new ApiError(9002);
|
||||
|
||||
try {
|
||||
@@ -116,38 +132,40 @@ export default typedPlugin(
|
||||
throw new ApiError(9002);
|
||||
}
|
||||
|
||||
if (file?.password) {
|
||||
if (file.password) {
|
||||
const valid = verifyAccessToken(token, 'file', file.id);
|
||||
if (!valid) throw new ApiError(3018);
|
||||
}
|
||||
|
||||
const size = file?.size || (await datasource.size(file?.name ?? id));
|
||||
const fileType = file?.type || 'application/octet-stream';
|
||||
const size = file.size || (await datasource.size(file.name));
|
||||
const fileType = file.type || 'application/octet-stream';
|
||||
const contentType = fileType.startsWith('text/') ? `${fileType}; charset=utf-8` : fileType;
|
||||
|
||||
const commonHeaders = {
|
||||
'Content-Disposition': file.originalName
|
||||
? `${download ? 'attachment; ' : ''}filename*=utf-8''${encodeURIComponent(file.originalName)}`
|
||||
: download
|
||||
? 'attachment;'
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (req.headers.range) {
|
||||
const [start, end] = parseRange(req.headers.range, size);
|
||||
if (start >= size || end >= size) {
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
const buf = await datasource.get(file.name);
|
||||
if (!buf) throw new ApiError(9002);
|
||||
|
||||
return res
|
||||
.type(contentType)
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename*=utf-8''${encodeURIComponent(file.originalName)}`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
...commonHeaders,
|
||||
})
|
||||
.status(416)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.range(file?.name ?? id, start || 0, end);
|
||||
const buf = await datasource.range(file.name, start || 0, end);
|
||||
if (!buf) throw new ApiError(9002);
|
||||
|
||||
return res
|
||||
@@ -156,19 +174,13 @@ export default typedPlugin(
|
||||
'Content-Range': `bytes ${start}-${end}/${size}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': end - start + 1,
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename*=utf-8''${encodeURIComponent(file.originalName)}`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
...commonHeaders,
|
||||
})
|
||||
.status(206)
|
||||
.send(buf);
|
||||
}
|
||||
|
||||
const buf = await datasource.get(file?.name ?? id);
|
||||
const buf = await datasource.get(file.name);
|
||||
if (!buf) throw new ApiError(9002);
|
||||
|
||||
return res
|
||||
@@ -176,13 +188,7 @@ export default typedPlugin(
|
||||
.headers({
|
||||
'Content-Length': size,
|
||||
'Accept-Ranges': 'bytes',
|
||||
...(file?.originalName
|
||||
? {
|
||||
'Content-Disposition': `${download ? 'attachment; ' : ''}filename*=utf-8''${encodeURIComponent(file.originalName)}`,
|
||||
}
|
||||
: download && {
|
||||
'Content-Disposition': 'attachment;',
|
||||
}),
|
||||
...commonHeaders,
|
||||
})
|
||||
.status(200)
|
||||
.send(buf);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { hashPassword } from '@/lib/crypto';
|
||||
import { hashPassword, verifyPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User, userSchema, userSelect } from '@/lib/db/models/user';
|
||||
import { log } from '@/lib/logger';
|
||||
@@ -47,6 +47,7 @@ export default typedPlugin(
|
||||
body: z.object({
|
||||
username: zStringTrimmed.optional(),
|
||||
password: zStringTrimmed.optional(),
|
||||
currentPassword: z.string().optional(),
|
||||
avatar: z.string().nullish(),
|
||||
view: z
|
||||
.object({
|
||||
@@ -89,6 +90,26 @@ export default typedPlugin(
|
||||
if (existing) throw new ApiError(1038);
|
||||
}
|
||||
|
||||
const changingPassword = !!req.body.password;
|
||||
if (changingPassword) {
|
||||
if (req.user.password) {
|
||||
if (!req.body.currentPassword) throw new ApiError(1067);
|
||||
|
||||
const valid = await verifyPassword(req.body.currentPassword, req.user.password);
|
||||
if (!valid) {
|
||||
logger.warn('invalid current password on password change', {
|
||||
user: req.user.username,
|
||||
ip: req.ip ?? 'unknown',
|
||||
ua: req.headers['user-agent'],
|
||||
});
|
||||
|
||||
throw new ApiError(1066);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentSession = await getSession(req, res);
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
@@ -96,6 +117,13 @@ export default typedPlugin(
|
||||
data: {
|
||||
...(req.body.username && { username: req.body.username }),
|
||||
...(req.body.password && { password: await hashPassword(req.body.password) }),
|
||||
...(changingPassword && {
|
||||
sessions: {
|
||||
deleteMany: {
|
||||
NOT: { id: currentSession.sessionId ?? '' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(req.body.avatar !== undefined && { avatar: req.body.avatar || null }),
|
||||
...(req.body.view && {
|
||||
view: {
|
||||
@@ -144,8 +172,7 @@ export default typedPlugin(
|
||||
},
|
||||
});
|
||||
|
||||
const session = await getSession(req, res);
|
||||
await saveSession(session, user, false);
|
||||
await saveSession(currentSession, user, false);
|
||||
|
||||
delete (user as any).password;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { fastifyStatic } from '@fastify/static';
|
||||
import fastifySwagger from '@fastify/swagger';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { jsonSchemaTransform, serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
|
||||
import { version } from '../../../package.json';
|
||||
import { version } from '@/lib/version';
|
||||
import { checkRateLimit } from '../plugins/checkRateLimit';
|
||||
import oauthPlugin from '../plugins/oauth';
|
||||
import vitePlugin from '../plugins/vite';
|
||||
|
||||
@@ -32,7 +32,7 @@ export function startTasks(server: FastifyInstance) {
|
||||
for (let i = 0; i !== config.features.thumbnails.num_threads; ++i) {
|
||||
tasks.worker(
|
||||
`thumbnail-${i}`,
|
||||
'./build/offload/thumbnails.js',
|
||||
'offload/thumbnails.js',
|
||||
{
|
||||
id: `thumbnail-${i}`,
|
||||
enabled: config.features.thumbnails.enabled,
|
||||
|
||||
+4
-2
@@ -7,14 +7,14 @@ export default defineConfig(async (_) => {
|
||||
return [
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
format: 'esm',
|
||||
outExtension: () => ({ js: '.js' }),
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
entry: await glob('./src/**/*.ts', {
|
||||
ignore: ['./src/components/**/*.ts', './src/client/**/*.(ts|tsx|html)'],
|
||||
}),
|
||||
shims: true,
|
||||
shims: false,
|
||||
esbuildPlugins: [],
|
||||
outDir: 'build',
|
||||
bundle: false,
|
||||
@@ -23,6 +23,8 @@ export default defineConfig(async (_) => {
|
||||
await replaceTscAliasPaths({
|
||||
configFile: 'tsconfig.json',
|
||||
outDir: 'build',
|
||||
resolveFullPaths: true,
|
||||
resolveFullExtension: '.js',
|
||||
});
|
||||
|
||||
console.log('[built-ins] copying builtins...');
|
||||
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const isSSR = mode.startsWith('ssr');
|
||||
|
||||
@@ -18,7 +21,7 @@ export default defineConfig(({ mode }) => {
|
||||
'ssr-view-url': path.resolve(__dirname, 'src/client/ssr-view-url/index.html'),
|
||||
},
|
||||
output: {
|
||||
format: isSSR ? 'cjs' : 'esm',
|
||||
format: 'esm',
|
||||
entryFileNames: isSSR ? `${mode}.js` : 'assets/[name]-[hash].js',
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user