Migrate from old middleware, to proxy functions, update all deps, fix deprecations

This commit is contained in:
2026-03-16 13:25:48 +01:00
parent d745a25b65
commit 00e6407633
25 changed files with 4134 additions and 8389 deletions

View File

@@ -1,9 +0,0 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
prettier
);

View File

@@ -1,9 +1,14 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import prettier from "eslint-config-prettier";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
prettier
{
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
prettier,
);

5426
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,85 +7,86 @@
"prod": "prisma migrate deploy && prisma db seed && node dist/src/main",
"lint": "eslint 'src/**/*.ts'",
"format": "prettier --end-of-line=auto --write 'src/**/*.ts'",
"test:system": "prisma migrate reset -f && nest start & wait-on http://localhost:8080/api/configs && newman run ./test/newman-system-tests.json"
"test:system": "prisma migrate reset -f && nest start & wait-on http://localhost:8080/api/configs && npx newman run ./test/newman-system-tests.json"
},
"prisma": {
"seed": "ts-node prisma/seed/config.seed.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.940.0",
"@keyv/redis": "^4.4.0",
"@nestjs/cache-manager": "^3.0.1",
"@nestjs/common": "^11.0.17",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.17",
"@nestjs/jwt": "^11.0.0",
"@aws-sdk/client-s3": "^3.1009.0",
"@prisma/adapter-better-sqlite3": "^7.5.0",
"@keyv/redis": "^5.1.6",
"@nestjs/cache-manager": "^3.1.0",
"@nestjs/common": "^11.1.16",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.16",
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.17",
"@nestjs/schedule": "^5.0.1",
"@nestjs/swagger": "^11.1.3",
"@nestjs/throttler": "^6.4.0",
"@prisma/client": "^6.19.0",
"@nestjs/platform-express": "^11.1.16",
"@nestjs/schedule": "^6.1.1",
"@nestjs/swagger": "^11.2.6",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^7.5.0",
"@types/jmespath": "^0.15.2",
"archiver": "^7.0.1",
"argon2": "^0.41.1",
"body-parser": "^2.2.0",
"cache-manager": "^6.4.2",
"cacheable": "^1.9.0",
"argon2": "^0.44.0",
"body-parser": "^2.2.2",
"cache-manager": "^7.2.8",
"cacheable": "^2.3.3",
"clamscan": "^2.4.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"content-disposition": "^0.5.4",
"class-validator": "^0.15.1",
"content-disposition": "^1.0.1",
"cookie-parser": "^1.4.7",
"jmespath": "^0.16.0",
"ldapts": "^7.4.0",
"mime-types": "^3.0.1",
"ldapts": "^8.1.7",
"mime-types": "^3.0.2",
"moment": "^2.30.1",
"nanoid": "^3.3.7",
"nodemailer": "^6.10.1",
"otplib": "^12.0.1",
"nanoid": "^5.1.6",
"nodemailer": "^8.0.2",
"otplib": "^13.3.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"qrcode-svg": "^1.1.0",
"reflect-metadata": "^0.2.2",
"rimraf": "^6.0.1",
"rimraf": "^6.1.3",
"rxjs": "^7.8.2",
"sharp": "^0.34.5",
"ts-node": "^10.9.2",
"uuid": "^11.1.0",
"yaml": "^2.7.1"
"uuid": "^13.0.0",
"yaml": "^2.8.2"
},
"devDependencies": {
"@nestjs/cli": "^11.0.6",
"@nestjs/schematics": "^11.0.5",
"@nestjs/testing": "^11.0.17",
"@types/archiver": "^6.0.3",
"@eslint/js": "^9.39.1",
"@nestjs/cli": "^11.0.16",
"@nestjs/schematics": "^11.0.9",
"@nestjs/testing": "^11.1.16",
"@types/archiver": "^7.0.0",
"@types/clamscan": "^2.4.1",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^5.0.1",
"@types/mime-types": "^2.1.4",
"@types/multer": "^1.4.12",
"@types/node": "^22.14.1",
"@types/nodemailer": "^6.4.17",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.6",
"@types/mime-types": "^3.0.1",
"@types/multer": "^2.1.0",
"@types/node": "^25.5.0",
"@types/nodemailer": "^7.0.11",
"@types/passport-jwt": "^4.0.1",
"@types/qrcode-svg": "^1.1.5",
"@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^8.48.0",
"@typescript-eslint/parser": "^8.48.0",
"cross-env": "^7.0.3",
"@types/supertest": "^7.2.0",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"newman": "^6.2.1",
"prettier": "^3.5.3",
"prisma": "^6.6.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"prettier": "^3.8.1",
"prisma": "^7.5.0",
"source-map-support": "^0.5.21",
"ts-loader": "^9.5.2",
"ts-loader": "^9.5.4",
"tsconfig-paths": "4.2.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.48.0",
"wait-on": "^8.0.3"
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.0",
"wait-on": "^9.0.4"
},
"overrides": {
"glob": "^11.0.0",

12
backend/prisma.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import "dotenv/config";
import { defineConfig } from "prisma/config";
const DATABASE_URL =
process.env.DATABASE_URL || "file:../data/swiss-datashare.db?connection_limit=1";
export default defineConfig({
schema: "prisma/schema.prisma",
datasource: {
url: DATABASE_URL,
},
});

View File

@@ -4,7 +4,6 @@ generator client {
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {

View File

@@ -1,3 +1,4 @@
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { Prisma, PrismaClient } from "@prisma/client";
import * as crypto from "crypto";
@@ -83,7 +84,7 @@ export const configVariables = {
},
"redis-url": {
type: "string",
defaultValue: "redis://pingvin-redis:6379",
defaultValue: "redis://swiss-datashare-redis:6379",
secret: true,
},
ttl: {
@@ -427,13 +428,11 @@ type ConfigVariables = {
};
const prisma = new PrismaClient({
datasources: {
db: {
url:
process.env.DATABASE_URL ||
"file:../data/swiss-datashare.db?connection_limit=1",
},
},
adapter: new PrismaBetterSqlite3({
url:
process.env.DATABASE_URL ||
"file:../data/swiss-datashare.db?connection_limit=1",
}),
});
async function seedConfigVariables() {

View File

@@ -8,8 +8,7 @@ import {
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { User } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { Prisma, User } from "@prisma/client";
import * as argon from "argon2";
import { Request, Response } from "express";
import * as moment from "moment";
@@ -58,7 +57,7 @@ export class AuthService {
this.logger.log(`User ${user.email} signed up from IP ${ip}`);
return { accessToken, refreshToken, user };
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0];
throw new BadRequestException(

View File

@@ -5,7 +5,7 @@ import {
UnauthorizedException,
} from "@nestjs/common";
import { User } from "@prisma/client";
import { authenticator, totp } from "otplib";
import { generateSecret, generateSync, generateURI, verifySync } from "otplib";
import * as qrcode from "qrcode-svg";
import { ConfigService } from "src/config/config.service";
import { PrismaService } from "src/prisma/prisma.service";
@@ -43,7 +43,12 @@ export class AuthTotpService {
throw new BadRequestException("TOTP is not enabled");
}
if (!authenticator.check(dto.totp, totpSecret)) {
const verifyResult = verifySync({
secret: totpSecret,
token: dto.totp,
});
if (!verifyResult.valid) {
throw new BadRequestException("Invalid code");
}
@@ -78,9 +83,12 @@ export class AuthTotpService {
}
const issuer = this.configService.get("general.appName");
const secret = authenticator.generateSecret();
const otpURL = totp.keyuri(user.username || user.email, issuer, secret);
const secret = generateSecret();
const otpURL = generateURI({
issuer,
label: user.username || user.email,
secret,
});
await this.prisma.user.update({
where: { id: user.id },
@@ -118,7 +126,7 @@ export class AuthTotpService {
throw new BadRequestException("TOTP is not in progress");
}
const expected = authenticator.generate(totpSecret);
const expected = generateSync({ secret: totpSecret });
if (code !== expected) {
throw new BadRequestException("Invalid code");
@@ -147,7 +155,7 @@ export class AuthTotpService {
throw new BadRequestException("TOTP is not enabled");
}
const expected = authenticator.generate(totpSecret);
const expected = generateSync({ secret: totpSecret });
if (code !== expected) {
throw new BadRequestException("Invalid code");

View File

@@ -92,7 +92,6 @@ export class ConfigService extends EventEmitter {
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(key: `${string}.${string}`): any {
const configVariable = this.configVariables.filter(
(variable) => `${variable.category}.${variable.name}` == key,

View File

@@ -1,4 +1,5 @@
import { Injectable, Logger } from "@nestjs/common";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "@prisma/client";
import { DATABASE_URL } from "../constants";
@@ -7,12 +8,10 @@ export class PrismaService extends PrismaClient {
private readonly logger = new Logger(PrismaService.name);
constructor() {
const adapter = new PrismaBetterSqlite3({ url: DATABASE_URL });
super({
datasources: {
db: {
url: DATABASE_URL,
},
},
adapter,
});
super.$connect().then(() => this.logger.log("Connected to the database"));
}

View File

@@ -54,14 +54,12 @@ export class ShareController {
@Get(":id")
@UseGuards(ShareSecurityGuard)
async get(@Param("id") id: string) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new ShareDTO().from(await this.shareService.get(id) as any);
}
@Get(":id/from-owner")
@UseGuards(ShareOwnerGuard)
async getFromOwner(@Param("id") id: string) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new ShareDTO().from(await this.shareService.get(id) as any);
}

View File

@@ -1,5 +1,5 @@
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import { Prisma } from "@prisma/client";
import * as argon from "argon2";
import * as crypto from "crypto";
import { Entry } from "ldapts";
@@ -51,7 +51,7 @@ export class UserSevice {
},
});
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0];
throw new BadRequestException(
@@ -71,7 +71,7 @@ export class UserSevice {
data: { ...user, password: hash },
});
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0];
throw new BadRequestException(
@@ -217,7 +217,7 @@ export class UserSevice {
return user;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0];
throw new BadRequestException(

View File

@@ -12,5 +12,13 @@ services:
- "./data/images:/opt/app/frontend/public/img:Z"
# - "./config.yaml:/opt/app/config.yaml" # Add this line, if you want to configure swiss-datashare via config file and not via UI
# Optional Redis cache backend for multi-instance deployments
swiss-datashare-redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- "./data/redis:/data:Z"
# To add ClamAV, to scan your shares for malicious files,
# see https://github.com/swissmakers/swiss-datashare

View File

@@ -0,0 +1,21 @@
import nextVitals from "eslint-config-next/core-web-vitals";
import prettier from "eslint-config-prettier";
export default [
...nextVitals,
prettier,
{
rules: {
quotes: "off",
"react-hooks/exhaustive-deps": "off",
"react-hooks/immutability": "off",
"react-hooks/purity": "off",
"react-hooks/set-state-in-effect": "off",
"react-hooks/preserve-manual-memoization": "off",
"import/no-anonymous-default-export": "off",
"no-unused-vars": "off",
"react/no-unescaped-entities": "off",
"@next/next/no-img-element": "off",
},
},
];

View File

@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

View File

@@ -1,21 +1,11 @@
/** @type {import('next').NextConfig} */
const { version } = require('./package.json');
const withPWA = require("next-pwa")({
dest: "public",
disable: process.env.NODE_ENV === "development",
reloadOnOnline: false,
runtimeCaching: [
{
urlPattern: /^https?.*/,
handler: 'NetworkOnly',
},
],
reloadOnOnline: false,
});
module.exports = withPWA({
output: "standalone", env: {
module.exports = {
output: "standalone",
turbopack: {
root: __dirname,
},
env: {
VERSION: version,
},
});
};

File diff suppressed because it is too large Load Diff

View File

@@ -5,48 +5,47 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"format": "prettier --end-of-line=auto --write \"src/**/*.ts*\"",
"postinstall": "npx update-browserslist-db@latest"
},
"dependencies": {
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0",
"axios": "^1.7.9",
"axios": "^1.13.6",
"clsx": "^2.1.1",
"cookies-next": "^4.2.1",
"cookies-next": "^6.1.1",
"file-saver": "^2.0.5",
"framer-motion": "^12.23.24",
"jose": "^5.9.2",
"framer-motion": "^12.36.0",
"jose": "^6.2.1",
"jwt-decode": "^4.0.0",
"markdown-to-jsx": "^7.5.0",
"mime-types": "^2.1.35",
"markdown-to-jsx": "^9.7.9",
"mime-types": "^3.0.2",
"moment": "^2.30.1",
"next": "^14.2.26",
"next-http-proxy-middleware": "^1.2.6",
"next-pwa": "^5.6.0",
"p-limit": "^6.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-intl": "^6.6.8",
"next": "^16.1.6",
"next-http-proxy-middleware": "^1.2.7",
"p-limit": "^7.3.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-icons": "^5.6.0",
"react-intl": "^8.1.3",
"sharp": "^0.34.5",
"yup": "^1.4.0"
"yup": "^1.7.1"
},
"devDependencies": {
"@types/mime-types": "^2.1.4",
"@types/node": "22.14.1",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@typescript-eslint/parser": "^8.48.0",
"autoprefixer": "^10.4.22",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.33",
"eslint-config-prettier": "^9.1.0",
"postcss": "^8.5.6",
"prettier": "^3.7.3",
"@types/mime-types": "^3.0.1",
"@types/node": "25.5.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@typescript-eslint/parser": "^8.57.0",
"autoprefixer": "^10.4.27",
"eslint": "^9.39.1",
"eslint-config-next": "^16.1.6",
"eslint-config-prettier": "^10.1.8",
"postcss": "^8.5.8",
"prettier": "^3.8.1",
"tailwindcss": "^3.4.17",
"tar": "^7.4.3",
"tar": "^7.5.11",
"typescript": "^5.9.3"
},
"overrides": {

View File

@@ -106,7 +106,6 @@ const ImagePreview = () => {
const { shareId, fileId, setIsNotSupported } =
React.useContext(FilePreviewContext);
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={`/api/shares/${shareId}/files/${fileId}?download=false`}
alt={`${fileId}_preview`}

View File

@@ -103,7 +103,6 @@ const Share = ({ shareId }: { shareId: string }) => {
// Only load share once when component mounts or shareId changes
// Don't reload when modals or t change
loadShare();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shareId]);
if (isLoading) {

View File

@@ -1,7 +1,7 @@
import { jwtDecode } from "jwt-decode";
import { NextRequest, NextResponse } from "next/server";
// This middleware redirects based on different conditions:
// This proxy redirects based on different conditions:
// - Authentication state
// - Setup status
// - Admin privileges
@@ -10,7 +10,7 @@ export const config = {
matcher: "/((?!api|static|.*\\..*|_next).*)",
};
export async function middleware(request: NextRequest) {
export async function proxy(request: NextRequest) {
const routes = {
unauthenticated: new Routes(["/auth/*", "/"]),
public: new Routes([
@@ -28,7 +28,7 @@ export async function middleware(request: NextRequest) {
// Get config from backend
const apiUrl = process.env.API_URL || "http://localhost:8080";
const config = (await (
const appConfig = (await (
await fetch(`${apiUrl}/api/configs`)
).json()) as Array<{
key: string;
@@ -38,7 +38,7 @@ export async function middleware(request: NextRequest) {
}>;
const getConfig = (key: string) => {
const variable = config.find((entry) => entry.key === key);
const variable = appConfig.find((entry) => entry.key === key);
if (!variable) return null;
const value = variable.value ?? variable.defaultValue;
@@ -146,7 +146,6 @@ export async function middleware(request: NextRequest) {
// Helper class to check if a route matches a list of routes
class Routes {
// eslint-disable-next-line no-unused-vars
constructor(public routes: string[]) {}
contains(_route: string) {

View File

@@ -16,7 +16,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true
},
"include": [

1497
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@
"deploy:dev": "docker buildx build --push --tag swissmakers/swiss-datashare:development --platform linux/amd64,linux/arm64 ."
},
"devDependencies": {
"conventional-changelog-cli": "^3.0.0"
"conventional-changelog-cli": "^5.0.0"
},
"prettier": {
"singleQuote": false,