Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e64922b70 | |||
| 15042b16d1 | |||
| 5e4c4fc6c9 | |||
| 7194c53891 | |||
| 7eff77ccc4 | |||
| 1b78ffaa91 | |||
| 8e8bfd68d1 | |||
| b029505cdd | |||
| c5c862bee3 | |||
| 3c38d008f1 | |||
| dc52b00a00 | |||
| b5d2e7040e | |||
| 5818440721 | |||
| f1c46da47d | |||
| 212c69d303 | |||
| 9e4152e298 | |||
| 307f023e47 | |||
| 3451bd8762 | |||
| a9d0be8aae | |||
| d83f720631 | |||
| 1f3d396296 | |||
| 48f771f344 | |||
| 555bc6aa26 | |||
| 8bd0eaac1e | |||
| 3280c77002 | |||
| b39743a53a | |||
| 9a73da56e9 | |||
| c9b0d2664f | |||
| 6063c9efac | |||
| dd6f192d4a | |||
| d956f4ed3d | |||
| 4728258750 | |||
| ece3e16459 | |||
| 9208dbe2f3 | |||
| 636de18642 | |||
| ee48456291 | |||
| a06d5ffaed | |||
| 606821a2c0 | |||
| 5c980c21e5 | |||
| 771cc380df | |||
| 38217870fe | |||
| 5b82c96a43 | |||
| 6f5f9869ad | |||
| b29bfeb8b1 | |||
| cb40559e49 | |||
| 90c72f7ffe | |||
| 002bd2e6f7 | |||
| 7b44f17a64 | |||
| b5c83f92e3 | |||
| 51b4d64a93 | |||
| 62c9e0a22f | |||
| 3daac34d3e | |||
| d80d5d1632 | |||
| 912f716362 | |||
| 16ecdf41af | |||
| f0bb6b08fa | |||
| efb4e2ce9a | |||
| 03238d10bf | |||
| e71590b9fb | |||
| 4728f1cc46 | |||
| 794778dee2 | |||
| b5e882f07e | |||
| e7c58a4847 | |||
| bdb44db25e | |||
| e8b82ffe62 | |||
| 53c53c009e | |||
| 7e8cda4605 | |||
| dfa0419a0a | |||
| aeb2638d1e | |||
| c5cef56e2a | |||
| b9c9d98252 | |||
| 30083b6705 | |||
| 47db6cf1bd | |||
| f929f6ad7d | |||
| 7e16e0f30c | |||
| b2be4e51cc | |||
| 2c871be8c5 | |||
| 8c03e74979 | |||
| d5c0355fd4 | |||
| 386cad0474 | |||
| 474024ea55 | |||
| dacf13e46d | |||
| f37b4bb2ee | |||
| 034398e9fb | |||
| 2c605cb176 | |||
| 9a6673fe6d | |||
| 6733c9adba | |||
| 9d3443ceac | |||
| d628424b35 | |||
| dab444040e | |||
| ecef854d23 | |||
| 166087e33c | |||
| e9e30c4c46 | |||
| fd400aa850 |
@@ -1,7 +1,4 @@
|
||||
.github
|
||||
build
|
||||
node_modules
|
||||
uploads*
|
||||
.env
|
||||
.eslintcache
|
||||
src/prisma
|
||||
node_modules/
|
||||
.next/
|
||||
uploads/
|
||||
.git/
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
'extends': ['next', 'next/core-web-vitals'],
|
||||
'rules': {
|
||||
'indent': ['error', 2],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'quotes': ['error', 'single'],
|
||||
'semi': ['error', 'always'],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/jsx-uses-react': 'warn',
|
||||
'react/jsx-uses-vars': 'warn',
|
||||
'react/no-danger-with-children': 'warn',
|
||||
'react/no-deprecated': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-typos': 'error',
|
||||
'react/react-in-jsx-scope': 'error',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
'@next/next/no-img-element': 'off',
|
||||
},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
github: diced
|
||||
@@ -1,70 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Report a reproducible bug in Zipline
|
||||
title: 'Bug: [short description of the issue]'
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
Describe in detail what you were doing and what happened.
|
||||
Please include screenshots, logs, or error messages if possible, as they help diagnose the issue faster.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: runtime-type
|
||||
attributes:
|
||||
label: How is Zipline being run?
|
||||
description:
|
||||
options:
|
||||
- On docker (docker, docker compose, etc.)
|
||||
- Built from source (running it through `pnpm start` or `node`, etc.)
|
||||
- Other (please specify in the "Zipline Version" section)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: runtime-version
|
||||
attributes:
|
||||
label: Zipline Version
|
||||
description: |
|
||||
Provide the version of Zipline you are using:
|
||||
- If version checking is enabled (it is by default): paste the response from `http://<domain>/api/version`
|
||||
- If using docker (and can't do the above): specify the tag you are using (`latest`, `trunk`, or a tag digest)
|
||||
- A simple version number (e.g. "4.2.1") may also suffice
|
||||
placeholder: '4.2.1'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: If applicable, what browsers are you seeing this issue on?
|
||||
multiple: true
|
||||
options:
|
||||
- Chromium based (Chrome, Brave, Edge, Opera, etc.)
|
||||
- Firefox based (Firefox, Zen Browser, Waterfox, etc.)
|
||||
- Safari (On macOS and/or iOS)
|
||||
- Chromium based on Android/iOS
|
||||
- Firefox based on Android/iOS
|
||||
- Other (Please specify in the "Steps to Reproduce" section)
|
||||
|
||||
- type: textarea
|
||||
id: zipline-logs
|
||||
attributes:
|
||||
label: Relevant Logs
|
||||
description: |
|
||||
Paste any relevant logs from Zipline or the browser (if applicable).
|
||||
If logs don't look useful, you can enable debug mode by setting the environment variable `DEBUG=zipline` when starting Zipline.
|
||||
Then reproduce the issue and copy the logs here.
|
||||
**Note:** Debug logs may contain sensitive information.
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
Please list the exact steps required to reproduce the issue.
|
||||
Include any relevant configuration options, settings, or external services that may affect Zipline’s functionality.
|
||||
@@ -1,11 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request
|
||||
url: https://github.com/diced/zipline/discussions/new?category=ideas&title=Your%20brief%20description%20here&labels=feature
|
||||
about: Ask for a new feature
|
||||
- name: Documentation
|
||||
url: https://zipline.diced.sh
|
||||
about: Maybe take a look a the docs?
|
||||
- name: Zipline Discord
|
||||
url: https://discord.gg/EAhCRfGxCF
|
||||
about: Ask for help with anything related to Zipline!
|
||||
@@ -1,50 +1,33 @@
|
||||
name: 'Build'
|
||||
name: 'CI: Build'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [v4, trunk]
|
||||
branches: [ trunk ]
|
||||
pull_request:
|
||||
branches: [v4, trunk]
|
||||
branches: [ trunk ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
node: [22.x, 24.x]
|
||||
arch: [amd64, arm64]
|
||||
runs-on: ubuntu-24.04${{ matrix.arch == 'arm64' && '-arm' || '' }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use node@${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
node-version: '16.x'
|
||||
- name: 'Restore dependency cache'
|
||||
id: cache-restore
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
run_install: false
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "store_path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
- name: Create mock config
|
||||
run: echo -e "[core]\nsecret = '12345678'\ndatabase_url = 'postgres://postgres:postgres@postgres/postgres'\n[uploader]\nroute = '/u'\ndirectory = './uploads'\n[urls]\nroute = '/go'" > config.toml
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ steps.pnpm-cache.outputs.store_path }}
|
||||
key: ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.node }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.arch }}-${{ matrix.node }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Install
|
||||
run: pnpm install
|
||||
- name: Install dependencies
|
||||
if: steps.cache-restore.outputs.cache-hit != 'true'
|
||||
run: yarn install
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
ZIPLINE_BUILD: 'true'
|
||||
run: pnpm build
|
||||
run: yarn build
|
||||
@@ -1,112 +0,0 @@
|
||||
name: 'Push Release Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v4.*.*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
|
||||
name: push release
|
||||
runs-on: ubuntu-24.04${{ matrix.arch == 'arm64' && '-arm' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get commit sha
|
||||
id: sha
|
||||
run: |
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
build-args: |
|
||||
ZIPLINE_GIT_SHA=${{ steps.sha.outputs.short_sha }}
|
||||
tags: |
|
||||
ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-${{ matrix.arch }}
|
||||
ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-${{ steps.sha.outputs.short_sha }}-${{ matrix.arch }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-${{ matrix.arch }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-${{ steps.sha.outputs.short_sha }}-${{ matrix.arch }}
|
||||
|
||||
amend-builds:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: push
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get commit sha
|
||||
id: sha
|
||||
run: |
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: pull images
|
||||
run: |
|
||||
docker pull --platform=linux/amd64 ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-amd64
|
||||
docker pull --platform=linux/arm64 ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-arm64
|
||||
docker pull --platform=linux/amd64 ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-amd64
|
||||
docker pull --platform=linux/arm64 ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-arm64
|
||||
|
||||
- name: create manifests
|
||||
run: |
|
||||
docker manifest create ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }} \
|
||||
--amend ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-amd64 \
|
||||
--amend ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-arm64 && \
|
||||
docker manifest create ghcr.io/diced/zipline:latest \
|
||||
--amend ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-amd64 \
|
||||
--amend ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-arm64 && \
|
||||
docker manifest create ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }} \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-amd64 \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-arm64 && \
|
||||
docker manifest create ${{ secrets.DOCKERHUB_USERNAME }}/zipline:latest \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-amd64 \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-arm64
|
||||
|
||||
- name: push manifests
|
||||
run: |
|
||||
docker manifest push ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}
|
||||
docker manifest push ghcr.io/diced/zipline:latest
|
||||
docker manifest push ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}
|
||||
docker manifest push ${{ secrets.DOCKERHUB_USERNAME }}/zipline:latest
|
||||
@@ -1,104 +1,45 @@
|
||||
name: 'Push Docker Images'
|
||||
name: 'CD: Push Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [v4, trunk]
|
||||
branches: [ trunk ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'server/**'
|
||||
- 'prisma/**'
|
||||
- '.github/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
|
||||
name: push
|
||||
runs-on: ubuntu-24.04${{ matrix.arch == 'arm64' && '-arm' || '' }}
|
||||
push_to_ghcr:
|
||||
name: Push Image to GitHub Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get commit sha
|
||||
id: sha
|
||||
run: |
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
- name: Push to GitHub Packages
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
registry: docker.pkg.github.com
|
||||
repository: diced/zipline/zipline
|
||||
dockerfile: Dockerfile
|
||||
tag_with_ref: true
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
build-args: |
|
||||
ZIPLINE_GIT_SHA=${{ steps.sha.outputs.short_sha }}
|
||||
tags: |
|
||||
ghcr.io/diced/zipline:trunk-${{ matrix.arch }}
|
||||
ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}-${{ matrix.arch }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ matrix.arch }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ steps.sha.outputs.short_sha }}-${{ matrix.arch }}
|
||||
|
||||
amend-builds:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: push
|
||||
push_to_dockerhub:
|
||||
name: Push Image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get commit sha
|
||||
id: sha
|
||||
run: |
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
- name: Push to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: pull images
|
||||
run: |
|
||||
docker pull --platform=linux/amd64 ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}-amd64
|
||||
docker pull --platform=linux/arm64 ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}-arm64
|
||||
docker pull --platform=linux/amd64 ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ steps.sha.outputs.short_sha }}-amd64
|
||||
docker pull --platform=linux/arm64 ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ steps.sha.outputs.short_sha }}-arm64
|
||||
|
||||
- name: create manifests
|
||||
run: |
|
||||
docker manifest create ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }} \
|
||||
--amend ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}-amd64 \
|
||||
--amend ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}-arm64 && \
|
||||
docker manifest create ghcr.io/diced/zipline:trunk \
|
||||
--amend ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}-amd64 \
|
||||
--amend ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}-arm64
|
||||
docker manifest create ghcr.io/diced/zipline:v4 \
|
||||
--amend ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}-amd64 \
|
||||
--amend ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}-arm64
|
||||
docker manifest create ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ steps.sha.outputs.short_sha }}-amd64 \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ steps.sha.outputs.short_sha }}-arm64
|
||||
docker manifest create ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ steps.sha.outputs.short_sha }} \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ steps.sha.outputs.short_sha }}-amd64 \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ steps.sha.outputs.short_sha }}-arm64
|
||||
|
||||
- name: push manifests
|
||||
run: |
|
||||
docker manifest push ghcr.io/diced/zipline:trunk-${{ steps.sha.outputs.short_sha }}
|
||||
docker manifest push ghcr.io/diced/zipline:trunk
|
||||
docker manifest push ghcr.io/diced/zipline:v4
|
||||
docker manifest push ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk
|
||||
docker manifest push ${{ secrets.DOCKERHUB_USERNAME }}/zipline:trunk-${{ steps.sha.outputs.short_sha }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: diced/zipline
|
||||
dockerfile: Dockerfile
|
||||
tag_with_ref: true
|
||||
@@ -1,99 +0,0 @@
|
||||
name: Generate OpenAPI Spec
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [v4, trunk]
|
||||
pull_request:
|
||||
branches: [v4, trunk]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
gen-openapi:
|
||||
strategy:
|
||||
matrix:
|
||||
node: [24.x]
|
||||
arch: [amd64]
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_USER: zipline
|
||||
POSTGRES_PASSWORD: zipline
|
||||
POSTGRES_DB: zipline
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U zipline -d zipline"
|
||||
--health-interval=5s
|
||||
--health-timeout=5s
|
||||
--health-retries=10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use node@${{ matrix.node }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "store_path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ steps.pnpm-cache.outputs.store_path }}
|
||||
key: ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.node }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.arch }}-${{ matrix.node }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Install
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
ZIPLINE_BUILD: 'true'
|
||||
run: pnpm build
|
||||
|
||||
- name: Generate secret
|
||||
id: secret
|
||||
run: |
|
||||
SECRET=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9')
|
||||
echo "secret=$SECRET" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Wait for Postgres
|
||||
run: |
|
||||
until pg_isready -h localhost -p 5432 -U zipline; do
|
||||
echo "Waiting for postgres..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Run generator
|
||||
env:
|
||||
DATABASE_URL: postgres://zipline:zipline@localhost:5432/zipline
|
||||
CORE_SECRET: ${{ steps.secret.outputs.secret }}
|
||||
NODE_ENV: production
|
||||
run: pnpm openapi
|
||||
|
||||
- name: Verify openapi.json exists
|
||||
run: |
|
||||
if [ ! -f "./openapi.json" ]; then
|
||||
echo "openapi.json not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openapi-json
|
||||
path: ./openapi.json
|
||||
@@ -5,22 +5,20 @@
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
build/
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
@@ -28,26 +26,14 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# eslint
|
||||
.eslintcache
|
||||
|
||||
# nix dev env
|
||||
!.envrc
|
||||
.direnv
|
||||
.devenv
|
||||
|
||||
# zipline
|
||||
uploads*/
|
||||
*.crt
|
||||
*.key
|
||||
src/prisma
|
||||
.memory.log*
|
||||
openapi.json
|
||||
config.toml
|
||||
uploads/
|
||||
@@ -0,0 +1 @@
|
||||
_
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn commitlint --edit $1
|
||||
@@ -1 +0,0 @@
|
||||
pnpm-lock.yaml
|
||||
@@ -1,64 +1,33 @@
|
||||
FROM node:22-alpine3.21 AS base
|
||||
FROM node:16-alpine3.11 AS builder
|
||||
WORKDIR /build
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
RUN apk add --no-cache ffmpeg tzdata
|
||||
|
||||
WORKDIR /zipline
|
||||
|
||||
COPY prisma ./prisma
|
||||
COPY package.json .
|
||||
COPY pnpm-lock.yaml .
|
||||
|
||||
FROM base AS deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
COPY src ./src
|
||||
COPY .gitignore ./.gitignore
|
||||
|
||||
COPY postcss.config.cjs ./postcss.config.cjs
|
||||
COPY prettier.config.cjs ./prettier.config.cjs
|
||||
COPY eslint.config.mjs ./eslint.config.mjs
|
||||
COPY vite.config.ts ./vite.config.ts
|
||||
COPY tsup.config.ts ./tsup.config.ts
|
||||
COPY tsconfig.json ./tsconfig.json
|
||||
COPY mimes.json ./mimes.json
|
||||
COPY code.json ./code.json
|
||||
COPY vite-env.d.ts ./vite-env.d.ts
|
||||
COPY server ./server
|
||||
COPY scripts ./scripts
|
||||
COPY prisma ./prisma
|
||||
|
||||
RUN ZIPLINE_BUILD=true pnpm run build
|
||||
COPY package.json yarn.lock next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
|
||||
|
||||
FROM base
|
||||
RUN yarn install
|
||||
|
||||
COPY --from=deps /zipline/node_modules ./node_modules
|
||||
# create a mock config.toml to spoof next build!
|
||||
RUN echo -e "[core]\nsecret = '12345678'\ndatabase_url = 'postgres://postgres:postgres@postgres/postgres'\n[uploader]\nroute = '/u'\ndirectory = './uploads'\n[urls]\nroute = '/go'" > config.toml
|
||||
|
||||
COPY --from=builder /zipline/build ./build
|
||||
RUN yarn build
|
||||
|
||||
COPY --from=builder /zipline/mimes.json ./mimes.json
|
||||
COPY --from=builder /zipline/code.json ./code.json
|
||||
FROM node:16-alpine3.11 AS runner
|
||||
WORKDIR /zipline
|
||||
|
||||
RUN pnpm prisma generate
|
||||
COPY --from=builder /build/node_modules ./node_modules
|
||||
|
||||
# clean
|
||||
RUN rm -rf /tmp/* /root/*
|
||||
COPY --from=builder /build/src ./src
|
||||
COPY --from=builder /build/server ./server
|
||||
COPY --from=builder /build/scripts ./scripts
|
||||
COPY --from=builder /build/prisma ./prisma
|
||||
COPY --from=builder /build/.next ./.next
|
||||
COPY --from=builder /build/tsconfig.json ./tsconfig.json
|
||||
COPY --from=builder /build/package.json ./package.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV ZIPLINE_ROOT=/zipline
|
||||
|
||||
ARG ZIPLINE_GIT_SHA
|
||||
ENV ZIPLINE_GIT_SHA=${ZIPLINE_GIT_SHA:-"unknown"}
|
||||
|
||||
# add scripts
|
||||
COPY docker/entrypoint.sh /zipline/entrypoint
|
||||
COPY docker/ziplinectl.sh /zipline/ziplinectl
|
||||
|
||||
RUN ln -s /zipline/ziplinectl /usr/local/bin/ziplinectl
|
||||
|
||||
ENTRYPOINT ["/zipline/entrypoint"]
|
||||
CMD ["node", "server"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 dicedtomato
|
||||
Copyright (c) 2021 dicedtomato
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,333 +1,28 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
|
||||
|
||||
The next generation ShareX / File upload server
|
||||
|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/EAhCRfGxCF)
|
||||
|
||||

|
||||
|
||||
Documentation: [zipline.diced.sh](https://zipline.diced.sh)
|
||||
|
||||
|
||||
Zipline is a ShareX/file upload server that is easy to use, packed with features and can be setup in one command!
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/EAhCRfGxCF)
|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
- Configurable
|
||||
- Fast
|
||||
- Built with Next.js & React
|
||||
- Token protected uploading
|
||||
- Easy setup instructions on [docs](https://zipline.diced.cf) (One command install `docker-compose up`)
|
||||
|
||||
- Setup Quickly: [Get Started with Docker](https://zipline.diced.sh/docs/get-started/docker)
|
||||
- Configure
|
||||
- Upload any file
|
||||
- Folders
|
||||
- Tags
|
||||
- URL shortening
|
||||
- Embeds
|
||||
- Discord Webhooks
|
||||
- HTTP Webhooks
|
||||
- OAuth2
|
||||
- 2FA
|
||||
- Passkeys
|
||||
- Password Protection
|
||||
- Image Compression
|
||||
- Video Thumbnails
|
||||
- API
|
||||
- PWA
|
||||
- Partial Uploads
|
||||
- Invites
|
||||
- Quotas
|
||||
- Custom Themes
|
||||
- ... and more!
|
||||
## Installing
|
||||
[See how to install here](https://zipline.diced.cf/docs/get-started)
|
||||
|
||||
# Usage
|
||||
## Configuration
|
||||
[See how to configure here](https://zipline.diced.cf/docs/config/overview)
|
||||
|
||||
Visit [the docs](https://zipline.diced.sh/docs/get-started/docker) for a more in-depth guide on how to get started.
|
||||
|
||||
## Install and Run with Docker
|
||||
|
||||
This is the recommended way to run Zipline:
|
||||
|
||||
```yml
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRESQL_USER:-zipline}
|
||||
POSTGRES_PASSWORD: ${POSTGRESQL_PASSWORD:?POSTGRESSQL_PASSWORD is required}
|
||||
POSTGRES_DB: ${POSTGRESQL_DB:-zipline}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'pg_isready', '-U', 'zipline']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
image: ghcr.io/diced/zipline
|
||||
ports:
|
||||
- '3000:3000'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DATABASE_URL=postgres://${POSTGRESQL_USER:-zipline}:${POSTGRESQL_PASSWORD}@postgresql:5432/${POSTGRESQL_DB:-zipline}
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- './uploads:/zipline/uploads'
|
||||
- './public:/zipline/public'
|
||||
- './themes:/zipline/themes'
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/healthcheck']
|
||||
interval: 15s
|
||||
timeout: 2s
|
||||
retries: 2
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
### Volumes
|
||||
|
||||
- `./uploads` - The folder where all the user uploads are stored (the default is `./uploads`)
|
||||
- `./public` - The folder where all the public assets are stored (must mount to `/zipline/public`)
|
||||
- `./themes` - The folder where all the custom themes are stored (must mount to `/zipline/themes`)
|
||||
|
||||
### Generating Secrets
|
||||
|
||||
```bash
|
||||
echo "POSTGRESQL_PASSWORD=$(openssl rand -base64 42 | tr -dc A-Za-z0-9 | cut -c -32 | tr -d '\n')" > .env
|
||||
echo "CORE_SECRET=$(openssl rand -base64 42 | tr -dc A-Za-z0-9 | cut -c -32 | tr -d '\n')" >> .env
|
||||
```
|
||||
|
||||
Without the `CORE_SECRET` environment variable, Zipline will not start.
|
||||
|
||||
### Changing where uploads are stored
|
||||
|
||||
By default, Zipline will default to the `./uploads` folder, which is also reflected in the `docker-compose.yml` above. If you want to change this, you can set the `DATASOURCE_LOCAL_DIRECTORY` environment variable to a different path.
|
||||
|
||||
```bash
|
||||
DATASOURCE_LOCAL_DIRECTORY=/path/to/your/local/files
|
||||
# or relative to the working directory
|
||||
DATASOURCE_LOCAL_DIRECTORY=./relative/path/to/files
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Remember to change volume mappings in the docker-compose.yml file if you change this.
|
||||
|
||||
### Changing the port and hostname
|
||||
|
||||
By default, Zipline binds to `0.0.0.0:3000`, which is also reflected in the `docker-compose.yml` above. If you want to change this, you can set the `CORE_PORT` and `CORE_HOSTNAME` environment variables to a different port and hostname.
|
||||
|
||||
```bash
|
||||
CORE_PORT=80
|
||||
CORE_HOSTNAME=localhost
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you change the port, you will need to update the `ports` section in the `docker-compose.yml` file.
|
||||
|
||||
### Using S3
|
||||
|
||||
If you want to use S3 instead of the local filesystem, you can set the following environment variables:
|
||||
|
||||
```bash
|
||||
DATASOURCE_TYPE=s3
|
||||
|
||||
DATASOURCE_S3_ACCESS_KEY_ID=access_key_id
|
||||
DATASOURCE_S3_SECRET_ACCESS_KEY=secret
|
||||
DATASOURCE_S3_BUCKET=zipline
|
||||
DATASOURCE_S3_REGION=us-west-2
|
||||
```
|
||||
|
||||
For more information, like other providers, see the [docs](https://zipline.diced.sh/docs/config/datasource#s3-datasource).
|
||||
|
||||
### Starting Zipline
|
||||
|
||||
Simply run the following command to start the server:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
You should be able to access the website at `http://localhost:3000` or the port you specified.
|
||||
|
||||
## Manual Install
|
||||
|
||||
See [docs](https://zipline.diced.sh/docs/get-started/source) for more information.
|
||||
|
||||
# Migrating from v3
|
||||
|
||||
Zipline v4 was a complete rewrite, and as such, there is no upgrade path from v3 to v4. You will need to export your data from v3 and import it into v4. This process is made easier by the fact that v4 has a built-in importer to import data from v3.
|
||||
|
||||
See [migration](https://zipline.diced.sh/docs/migrate) for more information.
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions of any kind are welcome, whether they are bug reports, pull requests, or feature requests.
|
||||
|
||||
## Bug Reports
|
||||
|
||||
Create an issue on GitHub and use the template, please include the following (if one of them is not applicable to the issue then it's not needed):
|
||||
|
||||
- The steps to reproduce the bug
|
||||
- Logs of Zipline
|
||||
- The version of Zipline, and whether or not you are using Docker (include the image digest/tag if possible)
|
||||
- Your OS & Browser including server OS
|
||||
- What you were expecting to see
|
||||
- How it can be fixed (if you know)
|
||||
|
||||
## Feature Requests
|
||||
|
||||
Create a discussion on GitHub, and please include the following:
|
||||
|
||||
- Brief explanation of your feature in the title (very brief)
|
||||
- How it would work (be detailed)
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
|
||||
|
||||
### Development
|
||||
|
||||
Here's how to setup Zipline for development
|
||||
|
||||
#### Nix
|
||||
|
||||
If you have [Nix](https://nixos.org) and [direnv](https://direnv.net/) installed, you can simply cd into the cloned directory and run the following command:
|
||||
|
||||
```bash
|
||||
direnv allow
|
||||
```
|
||||
|
||||
After doing so, your shell will be setup for development.
|
||||
|
||||
If you aren't using direnv, you can run the following command to enter the nix shell:
|
||||
|
||||
```bash
|
||||
nix develop --no-pure-eval
|
||||
```
|
||||
|
||||
Useful commands regarding the postgres server:
|
||||
|
||||
| Command | Description |
|
||||
| --------------- | --------------------------------------------- |
|
||||
| `pgup` | Starts the postgres server in the background. |
|
||||
| `pg_ctl status` | See if the postgres server is running |
|
||||
| `minioup` | Start a Minio server for testing S3 |
|
||||
| `downall` | Stops any running postgres or minio service. |
|
||||
|
||||
After familiarizing yourself with the environment, you can continue below (skipping the prerequisites since they are already installed).
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- nodejs (lts -> 20.x, 22.x)
|
||||
- pnpm (10.x)
|
||||
- a postgresql server
|
||||
|
||||
#### Setup
|
||||
|
||||
You should probably use a `.env` file to manage your environment variables, here is an example .env file with every available environment variable:
|
||||
|
||||
```bash
|
||||
DEBUG=zipline
|
||||
|
||||
# required
|
||||
CORE_SECRET="a secret that is 32 characters long"
|
||||
|
||||
# required
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/zipline?schema=public"
|
||||
|
||||
# these are optional
|
||||
CORE_PORT=3000
|
||||
CORE_HOSTNAME=0.0.0.0
|
||||
|
||||
# one of these is required
|
||||
DATASOURCE_TYPE="local"
|
||||
# DATASOURCE_TYPE="s3"
|
||||
|
||||
# if DATASOURCE_TYPE=local
|
||||
DATASOURCE_LOCAL_DIRECTORY="/path/to/your/local/files"
|
||||
|
||||
# if DATASOURCE_TYPE=s3
|
||||
# DATASOURCE_S3_ACCESS_KEY_ID="your-access-key-id"
|
||||
# DATASOURCE_S3_SECRET_ACCESS_KEY="your-secret-access-key"
|
||||
# DATASOURCE_S3_REGION="your-region"
|
||||
# DATASOURCE_S3_BUCKET="your-bucket"
|
||||
# DATASOURCE_S3_ENDPOINT="your-endpoint"
|
||||
# ^ if using a custom endpoint other than aws s3
|
||||
```
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Finally you may start the development server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
If you wish to build the production version of Zipline, you can run the following command:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
And to run the production version of Zipline:
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
#### Making changes to the database schema
|
||||
|
||||
Zipline uses [prisma](https://www.prisma.io/) as its ORM, and as such, you will need to use the prisma CLI to facilitate any changes to the database schema.
|
||||
|
||||
Once you have made a change to `prisma.schema`, you can run the script `db:migrate` to generate a migration file. This script doesn't apply the migration, as Zipline handles applying migrations itself on startup.
|
||||
|
||||
```bash
|
||||
pnpm db:migrate
|
||||
```
|
||||
|
||||
If you wish to push changes to the database without generating a migration file, you can run the script `db:prototype`. This is only recommended for testing purposes, and should not be used in production.
|
||||
|
||||
```bash
|
||||
pnpm db:prototype
|
||||
```
|
||||
|
||||
#### Linting and Formatting
|
||||
|
||||
Zipline will fail to build unless the code is properly formatted and linted. To format the code, you can run the following command:
|
||||
|
||||
```bash
|
||||
pnpm validate
|
||||
```
|
||||
|
||||
#### Testing `zipline-ctl`
|
||||
|
||||
To build the ctl, you can run the following command:
|
||||
|
||||
```bash
|
||||
pnpm build:server
|
||||
```
|
||||
|
||||
then run any command you want
|
||||
|
||||
```bash
|
||||
pnpm ctl help
|
||||
```
|
||||
|
||||
# Documentation
|
||||
|
||||
Documentation is located at [zipline.diced.sh](https://zipline.diced.sh) and the source is located at [github.com/diced/zipline-docs](https://github.com/diced/zipline-docs).
|
||||
|
||||
# Security
|
||||
|
||||
Security issues are taken seriously, and should be reported via [GitHub Advisories](https://github.com/diced/zipline/security/advisories). For more information see the [security policy](SECURITY.md).
|
||||
## Theming
|
||||
[See how to theme here](https://zipline.diced.cf/docs/themes/reference)
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 4.4.x | :white_check_mark: |
|
||||
| < 3 | :x: |
|
||||
| 3.x.x | :white_check_mark: |
|
||||
| < 2 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Report a Vulnerability [here](https://github.com/diced/zipline/security/advisories) (click Report a Vulnerability). Please include exact details with how to reproduce the vulnerability, and if possible, a proof of concept that demonstrates the vulnerability.
|
||||
|
||||
<- Go [back](README.md#SECURITY)
|
||||
Report a Vulnerability by issuing a bug report, with exact details with how the vulnerability happened, what "exploits" can happen, and possible fixes (optional). Vulnerability reports are treated with high priority and will be resolved most of the time quickly.
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
[
|
||||
{
|
||||
"ext": "html",
|
||||
"mime": "text/x-zipline-html",
|
||||
"name": "HTML"
|
||||
},
|
||||
{
|
||||
"ext": "css",
|
||||
"mime": "text/x-zipline-css",
|
||||
"name": "CSS"
|
||||
},
|
||||
{
|
||||
"ext": "cpp",
|
||||
"mime": "text/x-zipline-c++src",
|
||||
"name": "C++"
|
||||
},
|
||||
{
|
||||
"ext": "js",
|
||||
"mime": "text/x-zipline-javascript",
|
||||
"name": "JavaScript"
|
||||
},
|
||||
{
|
||||
"ext": "py",
|
||||
"mime": "text/x-zipline-python",
|
||||
"name": "Python"
|
||||
},
|
||||
{
|
||||
"ext": "rb",
|
||||
"mime": "text/x-zipline-ruby",
|
||||
"name": "Ruby"
|
||||
},
|
||||
{
|
||||
"ext": "java",
|
||||
"mime": "text/x-zipline-java",
|
||||
"name": "Java"
|
||||
},
|
||||
{
|
||||
"ext": "md",
|
||||
"mime": "text/x-zipline-markdown",
|
||||
"name": "Markdown"
|
||||
},
|
||||
{
|
||||
"ext": "c",
|
||||
"mime": "text/x-zipline-csrc",
|
||||
"name": "C"
|
||||
},
|
||||
{
|
||||
"ext": "php",
|
||||
"mime": "text/x-zipline-httpd-php",
|
||||
"name": "PHP"
|
||||
},
|
||||
{
|
||||
"ext": "sass",
|
||||
"mime": "text/x-zipline-sass",
|
||||
"name": "Sass"
|
||||
},
|
||||
{
|
||||
"ext": "scss",
|
||||
"mime": "text/x-zipline-scss",
|
||||
"name": "SCSS"
|
||||
},
|
||||
{
|
||||
"ext": "swift",
|
||||
"mime": "text/x-zipline-swift",
|
||||
"name": "Swift"
|
||||
},
|
||||
{
|
||||
"ext": "ts",
|
||||
"mime": "text/x-zipline-typescript",
|
||||
"name": "TypeScript"
|
||||
},
|
||||
{
|
||||
"ext": "go",
|
||||
"mime": "text/x-zipline-go",
|
||||
"name": "Go"
|
||||
},
|
||||
{
|
||||
"ext": "rs",
|
||||
"mime": "text/x-zipline-rustsrc",
|
||||
"name": "Rust"
|
||||
},
|
||||
{
|
||||
"ext": "sh",
|
||||
"mime": "text/x-zipline-sh",
|
||||
"name": "Bash"
|
||||
},
|
||||
{
|
||||
"ext": "json",
|
||||
"mime": "text/x-zipline-json",
|
||||
"name": "JSON"
|
||||
},
|
||||
{
|
||||
"ext": "ps1",
|
||||
"mime": "text/x-zipline-powershell",
|
||||
"name": "PowerShell"
|
||||
},
|
||||
{
|
||||
"ext": "sql",
|
||||
"mime": "text/x-zipline-sql",
|
||||
"name": "SQL"
|
||||
},
|
||||
{
|
||||
"ext": "yaml",
|
||||
"mime": "text/x-zipline-yaml",
|
||||
"name": "YAML"
|
||||
},
|
||||
{
|
||||
"ext": "dockerfile",
|
||||
"mime": "text/x-zipline-dockerfile",
|
||||
"name": "Dockerfile"
|
||||
},
|
||||
{
|
||||
"ext": "lua",
|
||||
"mime": "text/x-zipline-lua",
|
||||
"name": "Lua"
|
||||
},
|
||||
{
|
||||
"ext": "conf",
|
||||
"mime": "text/x-zipline-nginx-conf",
|
||||
"name": "NGINX Config File"
|
||||
},
|
||||
{
|
||||
"ext": "pl",
|
||||
"mime": "text/x-zipline-perl",
|
||||
"name": "Perl"
|
||||
},
|
||||
{
|
||||
"ext": "r",
|
||||
"mime": "text/x-zipline-rsrc",
|
||||
"name": "R"
|
||||
},
|
||||
{
|
||||
"ext": "scala",
|
||||
"mime": "text/x-zipline-scala",
|
||||
"name": "Scala"
|
||||
},
|
||||
{
|
||||
"ext": "groovy",
|
||||
"mime": "text/x-zipline-groovy",
|
||||
"name": "Groovy"
|
||||
},
|
||||
{
|
||||
"ext": "kt",
|
||||
"mime": "text/x-zipline-kotlin",
|
||||
"name": "Kotlin"
|
||||
},
|
||||
{
|
||||
"ext": "hs",
|
||||
"mime": "text/x-zipline-haskell",
|
||||
"name": "Haskell"
|
||||
},
|
||||
{
|
||||
"ext": "ex",
|
||||
"mime": "text/x-zipline-elixir",
|
||||
"name": "Elixir"
|
||||
},
|
||||
{
|
||||
"ext": "vim",
|
||||
"mime": "text/x-zipline-vim",
|
||||
"name": "Vim"
|
||||
},
|
||||
{
|
||||
"ext": "m",
|
||||
"mime": "text/x-zipline-matlab",
|
||||
"name": "MATLAB"
|
||||
},
|
||||
{
|
||||
"ext": "dart",
|
||||
"mime": "text/x-zipline-dart",
|
||||
"name": "Dart"
|
||||
},
|
||||
{
|
||||
"ext": "hbs",
|
||||
"mime": "text/x-zipline-handlebars-template",
|
||||
"name": "Handlebars"
|
||||
},
|
||||
{
|
||||
"ext": "hcl",
|
||||
"mime": "text/x-zipline-hcl",
|
||||
"name": "HCL"
|
||||
},
|
||||
{
|
||||
"ext": "http",
|
||||
"mime": "text/x-zipline-http",
|
||||
"name": "HTTP"
|
||||
},
|
||||
{
|
||||
"ext": "ini",
|
||||
"mime": "text/x-zipline-ini",
|
||||
"name": "INI"
|
||||
},
|
||||
{
|
||||
"ext": "jsx",
|
||||
"mime": "text/x-zipline-jsx",
|
||||
"name": "JSX"
|
||||
},
|
||||
{
|
||||
"ext": "coffee",
|
||||
"mime": "text/x-zipline-coffeescript",
|
||||
"name": "CoffeeScript"
|
||||
},
|
||||
{
|
||||
"ext": "tex",
|
||||
"mime": "text/x-zipline-latex",
|
||||
"name": "LaTeX (KaTeX)"
|
||||
},
|
||||
{
|
||||
"name": "Plain Text",
|
||||
"mime": "text/x-zipline-plain",
|
||||
"ext": "txt"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
module.exports = {
|
||||
parserPreset: 'conventional-changelog-conventionalcommits',
|
||||
rules: {
|
||||
'body-leading-blank': [1, 'always'],
|
||||
'body-max-line-length': [2, 'always', 100],
|
||||
'footer-leading-blank': [1, 'always'],
|
||||
'footer-max-line-length': [2, 'always', 100],
|
||||
'header-max-length': [2, 'always', 100],
|
||||
'subject-case': [
|
||||
2,
|
||||
'never',
|
||||
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
|
||||
],
|
||||
'subject-empty': [2, 'never'],
|
||||
'subject-full-stop': [2, 'never', '.'],
|
||||
'type-case': [2, 'always', 'lower-case'],
|
||||
'type-empty': [2, 'never'],
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'build',
|
||||
'chore',
|
||||
'ci',
|
||||
'docs',
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'refactor',
|
||||
'revert',
|
||||
'style',
|
||||
'test',
|
||||
],
|
||||
],
|
||||
'scope-enum': [
|
||||
1,
|
||||
'always',
|
||||
[
|
||||
'prisma',
|
||||
'scripts',
|
||||
'server',
|
||||
'pages',
|
||||
'config',
|
||||
'api',
|
||||
'hooks',
|
||||
'components',
|
||||
'middleware',
|
||||
'redux',
|
||||
'themes',
|
||||
'lib',
|
||||
'assets'
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
[core]
|
||||
secure = true
|
||||
secret = 'some secret'
|
||||
host = '0.0.0.0'
|
||||
port = 3000
|
||||
database_url = 'postgres://postgres:postgres@postgres/postgres'
|
||||
|
||||
[urls]
|
||||
route = '/go'
|
||||
length = 6
|
||||
|
||||
[uploader]
|
||||
route = '/u'
|
||||
embed_route = '/a'
|
||||
length = 6
|
||||
directory = './uploads'
|
||||
user_limit = 104900000 # 100mb
|
||||
admin_limit = 104900000 # 100mb
|
||||
disabled_extentions = ['jpg']
|
||||
@@ -1,36 +0,0 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres2
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'pg_isready', '-U', 'postgres']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- '3000:3000'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:postgres@postgres/postgres2
|
||||
- CORE_HOSTNAME=0.0.0.0
|
||||
depends_on:
|
||||
- postgres
|
||||
volumes:
|
||||
- './uploads:/zipline/uploads'
|
||||
- './public:/zipline/public'
|
||||
- './themes:/zipline/themes'
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
@@ -1,42 +1,42 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRESQL_USER:-zipline}
|
||||
POSTGRES_PASSWORD: ${POSTGRESQL_PASSWORD:?POSTGRESQL_PASSWORD is required}
|
||||
POSTGRES_DB: ${POSTGRESQL_DB:-zipline}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
postgres:
|
||||
image: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'pg_isready', '-U', 'zipline']
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
image: ghcr.io/diced/zipline:latest
|
||||
restart: unless-stopped
|
||||
image: ghcr.io/diced/zipline/zipline:trunk
|
||||
ports:
|
||||
- '3000:3000'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DATABASE_URL=postgres://${POSTGRESQL_USER:-zipline}:${POSTGRESQL_PASSWORD}@postgresql:5432/${POSTGRESQL_DB:-zipline}
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SECURE=false
|
||||
- SECRET=changethis
|
||||
- HOST=0.0.0.0
|
||||
- PORT=3000
|
||||
- DATABASE_URL=postgresql://postgres:postgres@postgres/postgres/
|
||||
- UPLOADER_ROUTE=/u
|
||||
- UPLOADER_EMBED_ROUTE=/a
|
||||
- UPLOADER_LENGTH=6
|
||||
- UPLOADER_DIRECTORY=./uploads
|
||||
- UPLOADER_ADMIN_LIMIT=104900000
|
||||
- UPLOADER_USER_LIMIT=104900000
|
||||
- UPLOADER_DISABLED_EXTS=
|
||||
volumes:
|
||||
- './uploads:/zipline/uploads'
|
||||
- './public:/zipline/public'
|
||||
- './themes:/zipline/themes'
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '-q', '--spider', 'http://0.0.0.0:3000/api/healthcheck']
|
||||
interval: 15s
|
||||
timeout: 2s
|
||||
retries: 2
|
||||
- '$PWD/uploads:/zipline/uploads'
|
||||
- '$PWD/public:/zipline/public'
|
||||
depends_on:
|
||||
- 'postgres'
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
pg_data:
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
cd ${ZIPLINE_ROOT:-/zipline}
|
||||
exec node --enable-source-maps build/server
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
cd ${ZIPLINE_ROOT:-/zipline}
|
||||
exec node --enable-source-maps build/ctl "$@"
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
|
||||
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
|
||||
import prettier from 'eslint-plugin-prettier';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
import unusedImports from 'eslint-plugin-unused-imports';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const gitignorePath = path.resolve(__dirname, '.gitignore');
|
||||
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
||||
const gitignorePatterns = gitignoreContent
|
||||
.split('\n')
|
||||
.filter((line) => line.trim() && !line.startsWith('#'))
|
||||
.map((pattern) => pattern.trim());
|
||||
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export default defineConfig(
|
||||
tseslint.configs.recommended,
|
||||
|
||||
jsxA11yPlugin.flatConfigs.recommended,
|
||||
reactPlugin.configs.flat.recommended,
|
||||
reactHooksPlugin.configs.flat.recommended,
|
||||
reactRefreshPlugin.configs.vite,
|
||||
|
||||
{ ignores: gitignorePatterns },
|
||||
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,tsx}'],
|
||||
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
},
|
||||
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
'react-hooks': reactHooksPlugin,
|
||||
prettier,
|
||||
'unused-imports': unusedImports,
|
||||
},
|
||||
|
||||
rules: {
|
||||
...prettierConfig.rules,
|
||||
|
||||
'prettier/prettier': ['error', {}, { fileInfoOptions: { withNodeModules: false } }],
|
||||
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: ['error', 'single', { avoidEscape: true }],
|
||||
semi: ['error', 'always'],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
indent: 'off',
|
||||
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react-hooks/set-state-in-effect': 'warn',
|
||||
'react-refresh/only-export-components': 'off',
|
||||
|
||||
'react/jsx-uses-react': 'warn',
|
||||
'react/jsx-uses-vars': 'warn',
|
||||
'react/no-danger-with-children': 'warn',
|
||||
'react/no-deprecated': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-typos': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
'react/display-name': 'off',
|
||||
|
||||
'jsx-a11y/alt-text': 'off',
|
||||
'jsx-a11y/no-autofocus': 'off',
|
||||
'jsx-a11y/click-events-have-key-events': 'off',
|
||||
'jsx-a11y/no-static-element-interactions': 'off',
|
||||
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
|
||||
],
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: { version: 'detect' },
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,254 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cachix": {
|
||||
"inputs": {
|
||||
"devenv": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"git-hooks": [
|
||||
"devenv",
|
||||
"git-hooks"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1748883665,
|
||||
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "latest",
|
||||
"repo": "cachix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv": {
|
||||
"inputs": {
|
||||
"cachix": "cachix",
|
||||
"flake-compat": "flake-compat",
|
||||
"git-hooks": "git-hooks",
|
||||
"nix": "nix",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753888869,
|
||||
"narHash": "sha256-VRYrrUmvXnBzfzuJVoI3os1H/0l8cJQ2KnrrxWkTB3E=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "bdf26a4453eff6bae835f33d519a36f77e0ca257",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv-root": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"narHash": "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY=",
|
||||
"type": "file",
|
||||
"url": "file:///dev/null"
|
||||
},
|
||||
"original": {
|
||||
"type": "file",
|
||||
"url": "file:///dev/null"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1747046372,
|
||||
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"devenv",
|
||||
"nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733312601,
|
||||
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_2": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753121425,
|
||||
"narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "644e0fc48951a860279da645ba77fe4a6e814c5e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1750779888,
|
||||
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-parts": "flake-parts",
|
||||
"git-hooks-nix": [
|
||||
"devenv",
|
||||
"git-hooks"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-23-11": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs-regression": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752773918,
|
||||
"narHash": "sha256-dOi/M6yNeuJlj88exI+7k154z+hAhFcuB8tZktiW7rg=",
|
||||
"owner": "cachix",
|
||||
"repo": "nix",
|
||||
"rev": "031c3cf42d2e9391eee373507d8c12e0f9606779",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "devenv-2.30",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1752827260,
|
||||
"narHash": "sha256-noFjJbm/uWRcd2Lotr7ovedfhKVZT+LeJs9rU416lKQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b527e89270879aaaf584c41f26b2796be634bc9d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b527e89270879aaaf584c41f26b2796be634bc9d",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1751159883,
|
||||
"narHash": "sha256-urW/Ylk9FIfvXfliA1ywh75yszAbiTEVgpPeinFyVZo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "14a40a1d7fb9afa4739275ac642ed7301a9ba1ab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"devenv-root": "devenv-root",
|
||||
"flake-parts": "flake-parts_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
{
|
||||
inputs = {
|
||||
# required for some reason when entering the shell for devenv
|
||||
devenv-root = {
|
||||
url = "file+file:///dev/null";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
# node 24.4.1, postgres 17
|
||||
nixpkgs.url = "github:nixos/nixpkgs/b527e89270879aaaf584c41f26b2796be634bc9d";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
|
||||
devenv.url = "github:cachix/devenv";
|
||||
devenv.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
|
||||
extra-substituters = "https://devenv.cachix.org";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{ flake-parts, devenv-root, ... }:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [
|
||||
inputs.devenv.flakeModule
|
||||
];
|
||||
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-linux"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
|
||||
perSystem =
|
||||
{
|
||||
config,
|
||||
self',
|
||||
inputs',
|
||||
pkgs,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
let
|
||||
psqlConfig = {
|
||||
username = "postgres";
|
||||
password = "postgres";
|
||||
database = "zipline";
|
||||
};
|
||||
in
|
||||
{
|
||||
devenv.shells.default = {
|
||||
packages = with pkgs; [
|
||||
git
|
||||
|
||||
# to generate thumbnails
|
||||
ffmpeg
|
||||
|
||||
# for testing docker
|
||||
colima
|
||||
docker
|
||||
docker-compose
|
||||
];
|
||||
|
||||
scripts = {
|
||||
pgup.exec = ''
|
||||
process-compose up postgres -D
|
||||
'';
|
||||
|
||||
minioup.exec = ''
|
||||
process-compose up minio -D
|
||||
'';
|
||||
|
||||
downall.exec = ''
|
||||
process-compose down
|
||||
'';
|
||||
|
||||
# ensure that volumes are mounted with write access for docker containers
|
||||
start_colima.exec = ''
|
||||
colima start --mount $PWD/themes:w --mount $PWD/uploads:w --mount $PWD/public:w
|
||||
'';
|
||||
};
|
||||
|
||||
enterShell = ''
|
||||
export name="zipline-env";
|
||||
echo -e "\n[$name]: run 'pgup' to start services, 'pgdown' to stop services";
|
||||
'';
|
||||
|
||||
languages.javascript = {
|
||||
enable = true;
|
||||
package = pkgs.nodejs_24;
|
||||
|
||||
corepack.enable = true;
|
||||
};
|
||||
|
||||
services = {
|
||||
postgres = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_17;
|
||||
|
||||
initialScript = ''
|
||||
CREATE ROLE "${psqlConfig.username}" WITH LOGIN PASSWORD '${psqlConfig.password}' SUPERUSER;
|
||||
'';
|
||||
|
||||
initialDatabases = [
|
||||
{
|
||||
name = psqlConfig.database;
|
||||
user = psqlConfig.username;
|
||||
}
|
||||
];
|
||||
|
||||
listen_addresses = "0.0.0.0";
|
||||
port = 5432;
|
||||
};
|
||||
|
||||
minio = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
process.managers.process-compose = {
|
||||
tui.enable = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
@@ -1,130 +1,59 @@
|
||||
{
|
||||
"name": "zipline",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"version": "4.4.2",
|
||||
"name": "zip3",
|
||||
"version": "3.3.0",
|
||||
"scripts": {
|
||||
"build": "tsx scripts/build.ts",
|
||||
"dev": "cross-env NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --enable-source-maps ./src/server",
|
||||
"dev:nd": "cross-env NODE_ENV=development tsx --require dotenv/config --enable-source-maps ./src/server",
|
||||
"dev:inspector": "cross-env NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./src/server",
|
||||
"start": "cross-env NODE_ENV=production node --trace-warnings --require dotenv/config ./build/server",
|
||||
"start:inspector": "cross-env NODE_ENV=production node --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./build/server",
|
||||
"ctl": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/ctl",
|
||||
"validate": "tsx scripts/validate.ts",
|
||||
"openapi": "tsx scripts/openapi.ts",
|
||||
"db:prototype": "prisma db push --skip-generate && prisma generate --no-hints",
|
||||
"db:migrate": "prisma migrate dev --create-only",
|
||||
"docker:engine": "colima start --mount $PWD/themes:w --mount $PWD/uploads:w --mount $PWD/public:w",
|
||||
"docker:compose:dev:build": "docker compose --file docker-compose.dev.yml build --build-arg ZIPLINE_GIT_SHA=$(git rev-parse HEAD)",
|
||||
"docker:compose:dev:up": "docker compose --file docker-compose.dev.yml up -d",
|
||||
"docker:compose:dev:down": "docker compose --file docker-compose.dev.yml down",
|
||||
"docker:compose:dev:logs": "docker compose --file docker-compose.dev.yml logs -f"
|
||||
"prepare": "husky install",
|
||||
"dev": "NODE_ENV=development node server",
|
||||
"build": "npm-run-all build:schema build:next",
|
||||
"build:next": "next build",
|
||||
"build:schema": "prisma generate --schema=prisma/schema.prisma",
|
||||
"start": "node server",
|
||||
"lint": "next lint",
|
||||
"seed": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.726.1",
|
||||
"@aws-sdk/lib-storage": "3.726.1",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/cors": "^11.1.0",
|
||||
"@fastify/multipart": "^9.3.0",
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@fastify/sensible": "^6.0.4",
|
||||
"@fastify/static": "^8.3.0",
|
||||
"@fastify/swagger": "^9.6.1",
|
||||
"@mantine/charts": "^8.3.9",
|
||||
"@mantine/code-highlight": "^8.3.9",
|
||||
"@mantine/core": "^8.3.9",
|
||||
"@mantine/dates": "^8.3.9",
|
||||
"@mantine/dropzone": "^8.3.9",
|
||||
"@mantine/form": "^8.3.9",
|
||||
"@mantine/hooks": "^8.3.9",
|
||||
"@mantine/modals": "^8.3.9",
|
||||
"@mantine/notifications": "^8.3.9",
|
||||
"@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",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@smithy/node-http-handler": "^4.1.1",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"archiver": "^7.0.1",
|
||||
"argon2": "^0.44.0",
|
||||
"asciinema-player": "^3.12.1",
|
||||
"bytes": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"colorette": "^2.0.20",
|
||||
"commander": "^14.0.2",
|
||||
"cookie": "^1.1.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"detect-browser": "^5.3.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fastify": "^5.6.2",
|
||||
"fastify-plugin": "^5.1.0",
|
||||
"fastify-type-provider-zod": "^6.1.0",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"iron-session": "^8.0.4",
|
||||
"isomorphic-dompurify": "^2.33.0",
|
||||
"katex": "^0.16.27",
|
||||
"mantine-datatable": "^8.3.9",
|
||||
"ms": "^2.1.3",
|
||||
"multer": "2.0.2",
|
||||
"otplib": "^12.0.1",
|
||||
"prisma": "6.13.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.10.1",
|
||||
"react-window": "1.8.11",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sharp": "^0.34.5",
|
||||
"swr": "^2.3.7",
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"vite": "^7.2.7",
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.9"
|
||||
"@emotion/react": "^11.4.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@iarna/toml": "2.2.5",
|
||||
"@mui/icons-material": "^5.0.0",
|
||||
"@mui/material": "^5.0.2",
|
||||
"@mui/styles": "^5.0.1",
|
||||
"@prisma/client": "^3.7.0",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"argon2": "^0.28.2",
|
||||
"colorette": "^1.2.2",
|
||||
"cookie": "^0.4.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"fecha": "^4.2.1",
|
||||
"formik": "^2.2.9",
|
||||
"multer": "^1.4.2",
|
||||
"next": "^12.0.7",
|
||||
"prisma": "^3.7.0",
|
||||
"react": "17.0.2",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "^11.3.2",
|
||||
"react-redux": "^7.2.4",
|
||||
"redux": "^4.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/fluent-ffmpeg": "^2.1.28",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/ms": "^2.1.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.7.4",
|
||||
"sass": "^1.94.2",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsup": "^8.5.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
"@commitlint/cli": "^12.1.4",
|
||||
"@commitlint/config-conventional": "^12.1.4",
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/multer": "^1.4.6",
|
||||
"@types/node": "^15.12.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-next": "11.0.0",
|
||||
"husky": "^6.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"release": "^6.3.0",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^4.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/diced/zipline.git"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
ignoredBuiltDependencies:
|
||||
- unrs-resolver
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- '@prisma/client'
|
||||
- '@prisma/engines'
|
||||
- argon2
|
||||
- esbuild
|
||||
- prisma
|
||||
- sharp
|
||||
@@ -1,14 +0,0 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
/** @type {import('prettier').Config} */
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
jsxSingleQuote: true,
|
||||
printWidth: 110,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"administrator" BOOLEAN NOT NULL DEFAULT false,
|
||||
"embedTitle" TEXT,
|
||||
"embedColor" TEXT NOT NULL DEFAULT E'#2f3136',
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Image" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"file" TEXT NOT NULL,
|
||||
"mimetype" TEXT NOT NULL DEFAULT E'image/png',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InvisibleImage" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"invis" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Url" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"to" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InvisibleUrl" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"invis" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleImage.invis_unique" ON "InvisibleImage"("invis");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleImage_id_unique" ON "InvisibleImage"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl.invis_unique" ON "InvisibleUrl"("invis");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_id_unique" ON "InvisibleUrl"("id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD FOREIGN KEY ("id") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Url" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleUrl" ADD FOREIGN KEY ("id") REFERENCES "Url"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "systemTheme" TEXT NOT NULL DEFAULT E'dark_blue';
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Theme" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"primary" TEXT NOT NULL,
|
||||
"secondary" TEXT NOT NULL,
|
||||
"error" TEXT NOT NULL,
|
||||
"warning" TEXT NOT NULL,
|
||||
"info" TEXT NOT NULL,
|
||||
"border" TEXT NOT NULL,
|
||||
"mainBackground" TEXT NOT NULL,
|
||||
"paperBackground" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Theme_userId_unique" ON "Theme"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Theme" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "favorite" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[imageId]` on the table `InvisibleImage` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `imageId` to the `InvisibleImage` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_id_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "InvisibleImage_id_unique";
|
||||
|
||||
-- AlterTable
|
||||
CREATE SEQUENCE "invisibleimage_id_seq";
|
||||
ALTER TABLE "InvisibleImage" ADD COLUMN "imageId" INTEGER NOT NULL,
|
||||
ALTER COLUMN "id" SET DEFAULT nextval('invisibleimage_id_seq'),
|
||||
ADD PRIMARY KEY ("id");
|
||||
ALTER SEQUENCE "invisibleimage_id_seq" OWNED BY "InvisibleImage"."id";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleImage_imageId_unique" ON "InvisibleImage"("imageId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "embed" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `InvisibleUrl` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `Url` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Image" DROP CONSTRAINT "Image_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_imageId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleUrl" DROP CONSTRAINT "InvisibleUrl_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Url" DROP CONSTRAINT "Url_userId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "InvisibleUrl";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Url";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Theme" ADD CONSTRAINT "Theme_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD CONSTRAINT "InvisibleImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleImage.invis_unique" RENAME TO "InvisibleImage_invis_key";
|
||||
@@ -0,0 +1,34 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Url" (
|
||||
"id" TEXT NOT NULL,
|
||||
"destination" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Url_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InvisibleUrl" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"invis" TEXT NOT NULL,
|
||||
"urlId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "InvisibleUrl_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Url_id_key" ON "Url"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_invis_key" ON "InvisibleUrl"("invis");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_urlId_unique" ON "InvisibleUrl"("urlId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleUrl" ADD CONSTRAINT "InvisibleUrl_urlId_fkey" FOREIGN KEY ("urlId") REFERENCES "Url"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" ADD COLUMN "vanity" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "embedSiteName" TEXT DEFAULT E'{image.file} • {user.name}';
|
||||
@@ -0,0 +1,11 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "ratelimited" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleImage_imageId_unique" RENAME TO "InvisibleImage_imageId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleUrl_urlId_unique" RENAME TO "InvisibleUrl_urlId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Theme_userId_unique" RENAME TO "Theme_userId_key";
|
||||
@@ -0,0 +1,8 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Stats" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"data" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "Stats_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -1,370 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserFilesQuota" AS ENUM ('BY_BYTES', 'BY_FILES');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN', 'SUPERADMIN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OAuthProviderType" AS ENUM ('DISCORD', 'GOOGLE', 'GITHUB', 'OIDC');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IncompleteFileStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETE', 'FAILED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Zipline" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"firstSetup" BOOLEAN NOT NULL DEFAULT true,
|
||||
"coreReturnHttpsUrls" BOOLEAN NOT NULL DEFAULT false,
|
||||
"coreDefaultDomain" TEXT,
|
||||
"coreTempDirectory" TEXT NOT NULL,
|
||||
"chunksEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"chunksMax" INTEGER NOT NULL DEFAULT 99614720,
|
||||
"chunksSize" INTEGER NOT NULL DEFAULT 26214400,
|
||||
"tasksDeleteInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"tasksClearInvitesInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"tasksMaxViewsInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"tasksThumbnailsInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"tasksMetricsInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"filesRoute" TEXT NOT NULL DEFAULT '/u',
|
||||
"filesLength" INTEGER NOT NULL DEFAULT 6,
|
||||
"filesDefaultFormat" TEXT NOT NULL DEFAULT 'random',
|
||||
"filesDisabledExtensions" TEXT[],
|
||||
"filesMaxFileSize" INTEGER NOT NULL DEFAULT 104857600,
|
||||
"filesDefaultExpiration" INTEGER,
|
||||
"filesAssumeMimetypes" BOOLEAN NOT NULL DEFAULT false,
|
||||
"filesDefaultDateFormat" TEXT NOT NULL DEFAULT 'YYYY-MM-DD_HH:mm:ss',
|
||||
"filesRemoveGpsMetadata" BOOLEAN NOT NULL DEFAULT false,
|
||||
"urlsRoute" TEXT NOT NULL DEFAULT '/go',
|
||||
"urlsLength" INTEGER NOT NULL DEFAULT 6,
|
||||
"featuresImageCompression" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresRobotsTxt" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresHealthcheck" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresUserRegistration" BOOLEAN NOT NULL DEFAULT false,
|
||||
"featuresOauthRegistration" BOOLEAN NOT NULL DEFAULT false,
|
||||
"featuresDeleteOnMaxViews" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresThumbnailsEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresThumbnailsNumberThreads" INTEGER NOT NULL DEFAULT 4,
|
||||
"featuresMetricsEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresMetricsAdminOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"featuresMetricsShowUserSpecific" BOOLEAN NOT NULL DEFAULT true,
|
||||
"invitesEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"invitesLength" INTEGER NOT NULL DEFAULT 6,
|
||||
"websiteTitle" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"websiteTitleLogo" TEXT,
|
||||
"websiteExternalLinks" JSONB NOT NULL DEFAULT '[{ "name": "GitHub", "url": "https://github.com/diced/zipline"}, { "name": "Documentation", "url": "https://zipline.diced.sh/"}]',
|
||||
"websiteLoginBackground" TEXT,
|
||||
"websiteDefaultAvatar" TEXT,
|
||||
"websiteTos" TEXT,
|
||||
"websiteThemeDefault" TEXT NOT NULL DEFAULT 'system',
|
||||
"websiteThemeDark" TEXT NOT NULL DEFAULT 'builtin:dark_gray',
|
||||
"websiteThemeLight" TEXT NOT NULL DEFAULT 'builtin:light_gray',
|
||||
"oauthBypassLocalLogin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"oauthLoginOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"oauthDiscordClientId" TEXT,
|
||||
"oauthDiscordClientSecret" TEXT,
|
||||
"oauthDiscordRedirectUri" TEXT,
|
||||
"oauthGoogleClientId" TEXT,
|
||||
"oauthGoogleClientSecret" TEXT,
|
||||
"oauthGoogleRedirectUri" TEXT,
|
||||
"oauthGithubClientId" TEXT,
|
||||
"oauthGithubClientSecret" TEXT,
|
||||
"oauthGithubRedirectUri" TEXT,
|
||||
"oauthOidcClientId" TEXT,
|
||||
"oauthOidcClientSecret" TEXT,
|
||||
"oauthOidcAuthorizeUrl" TEXT,
|
||||
"oauthOidcTokenUrl" TEXT,
|
||||
"oauthOidcUserinfoUrl" TEXT,
|
||||
"oauthOidcRedirectUri" TEXT,
|
||||
"mfaTotpEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"mfaTotpIssuer" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"mfaPasskeys" BOOLEAN NOT NULL DEFAULT false,
|
||||
"ratelimitEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"ratelimitMax" INTEGER NOT NULL DEFAULT 10,
|
||||
"ratelimitWindow" INTEGER,
|
||||
"ratelimitAdminBypass" BOOLEAN NOT NULL DEFAULT true,
|
||||
"ratelimitAllowList" TEXT[],
|
||||
"httpWebhookOnUpload" TEXT,
|
||||
"httpWebhookOnShorten" TEXT,
|
||||
"discordWebhookUrl" TEXT,
|
||||
"discordUsername" TEXT,
|
||||
"discordAvatarUrl" TEXT,
|
||||
"discordOnUploadWebhookUrl" TEXT,
|
||||
"discordOnUploadUsername" TEXT,
|
||||
"discordOnUploadAvatarUrl" TEXT,
|
||||
"discordOnUploadContent" TEXT,
|
||||
"discordOnUploadEmbed" JSONB,
|
||||
"discordOnShortenWebhookUrl" TEXT,
|
||||
"discordOnShortenUsername" TEXT,
|
||||
"discordOnShortenAvatarUrl" TEXT,
|
||||
"discordOnShortenContent" TEXT,
|
||||
"discordOnShortenEmbed" JSONB,
|
||||
"pwaEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"pwaTitle" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"pwaShortName" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"pwaDescription" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"pwaThemeColor" TEXT NOT NULL DEFAULT '#000000',
|
||||
"pwaBackgroundColor" TEXT NOT NULL DEFAULT '#000000',
|
||||
|
||||
CONSTRAINT "Zipline_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,
|
||||
"role" "Role" NOT NULL DEFAULT 'USER',
|
||||
"view" JSONB NOT NULL DEFAULT '{}',
|
||||
"totpSecret" TEXT,
|
||||
"sessions" TEXT[],
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Export" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"completed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"path" TEXT NOT NULL,
|
||||
"files" INTEGER NOT NULL,
|
||||
"size" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Export_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserQuota" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"filesQuota" "UserFilesQuota" NOT NULL,
|
||||
"maxBytes" TEXT,
|
||||
"maxFiles" INTEGER,
|
||||
"maxUrls" INTEGER,
|
||||
"userId" TEXT,
|
||||
|
||||
CONSTRAINT "UserQuota_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserPasskey" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"lastUsed" TIMESTAMP(3),
|
||||
"name" TEXT NOT NULL,
|
||||
"reg" JSONB NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "UserPasskey_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,
|
||||
"username" TEXT NOT NULL,
|
||||
"accessToken" TEXT NOT NULL,
|
||||
"refreshToken" TEXT,
|
||||
"oauthId" TEXT,
|
||||
|
||||
CONSTRAINT "OAuthProvider_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,
|
||||
"size" BIGINT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxViews" INTEGER,
|
||||
"favorite" BOOLEAN NOT NULL DEFAULT false,
|
||||
"password" TEXT,
|
||||
"userId" TEXT,
|
||||
"folderId" TEXT,
|
||||
|
||||
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Thumbnail" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"fileId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Thumbnail_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,
|
||||
"public" BOOLEAN NOT NULL DEFAULT false,
|
||||
"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,
|
||||
"userId" TEXT,
|
||||
|
||||
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,
|
||||
"code" TEXT NOT NULL,
|
||||
"vanity" TEXT,
|
||||
"destination" TEXT NOT NULL,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxViews" INTEGER,
|
||||
"password" 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,
|
||||
|
||||
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,
|
||||
"uses" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxUses" INTEGER,
|
||||
"inviterId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Invite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_FileToTag" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_FileToTag_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_token_key" ON "User"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserQuota_userId_key" ON "UserQuota"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OAuthProvider_provider_oauthId_key" ON "OAuthProvider"("provider", "oauthId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Thumbnail_fileId_key" ON "Thumbnail"("fileId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Url_code_vanity_key" ON "Url"("code", "vanity");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Invite_code_key" ON "Invite"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_FileToTag_B_index" ON "_FileToTag"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Export" ADD CONSTRAINT "Export_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserQuota" ADD CONSTRAINT "UserQuota_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserPasskey" ADD CONSTRAINT "UserPasskey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("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 "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 "Thumbnail" ADD CONSTRAINT "Thumbnail_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE CASCADE 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 "Tag" ADD CONSTRAINT "Tag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL 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 "Invite" ADD CONSTRAINT "Invite_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES "User"("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;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ALTER COLUMN "filesDefaultExpiration" SET DATA TYPE TEXT;
|
||||
@@ -1,17 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ALTER COLUMN "chunksMax" SET DEFAULT '95mb',
|
||||
ALTER COLUMN "chunksMax" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "chunksSize" SET DEFAULT '25mb',
|
||||
ALTER COLUMN "chunksSize" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksDeleteInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksDeleteInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksClearInvitesInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksClearInvitesInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksMaxViewsInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksMaxViewsInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksThumbnailsInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksThumbnailsInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksMetricsInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksMetricsInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "filesMaxFileSize" SET DEFAULT '100mb',
|
||||
ALTER COLUMN "filesMaxFileSize" SET DATA TYPE TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "websiteLoginBackgroundBlur" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "filesRandomWordsNumAdjectives" INTEGER NOT NULL DEFAULT 2,
|
||||
ADD COLUMN "filesRandomWordsSeparator" TEXT NOT NULL DEFAULT '-';
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Folder" ADD COLUMN "allowUploads" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "featuresVersionAPI" TEXT NOT NULL DEFAULT 'https://zipline-version.diced.sh',
|
||||
ADD COLUMN "featuresVersionChecking" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "oauthDiscordWhitelistIds" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `oauthDiscordWhitelistIds` on the `Zipline` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" DROP COLUMN "oauthDiscordWhitelistIds",
|
||||
ADD COLUMN "oauthDiscordAllowedIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN "oauthDiscordDeniedIds" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "domains" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "filesDefaultCompressionFormat" TEXT DEFAULT 'jpg';
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "featuresThumbnailsFormat" TEXT NOT NULL DEFAULT 'jpg';
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "coreTrustProxy" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "filesMaxExpiration" TEXT;
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `mfaPasskeys` on the `Zipline` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" DROP COLUMN "mfaPasskeys",
|
||||
ADD COLUMN "mfaPasskeysEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "mfaPasskeysOrigin" TEXT,
|
||||
ADD COLUMN "mfaPasskeysRpID" TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "tasksCleanThumbnailsInterval" TEXT NOT NULL DEFAULT '1d';
|
||||
@@ -1,6 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Folder" ADD COLUMN "parentId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."Folder" ADD CONSTRAINT "Folder_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "public"."Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `sessions` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."User" DROP COLUMN "sessions";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "public"."UserSession" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"ua" TEXT NOT NULL,
|
||||
"client" TEXT NOT NULL,
|
||||
"device" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@@ -1,403 +1,83 @@
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/prisma"
|
||||
moduleFormat = "cjs"
|
||||
previewFeatures = ["queryCompiler", "driverAdapters"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Zipline {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
firstSetup Boolean @default(true)
|
||||
|
||||
coreReturnHttpsUrls Boolean @default(false)
|
||||
coreDefaultDomain String?
|
||||
coreTempDirectory String // default join(tmpdir(), 'zipline')
|
||||
coreTrustProxy Boolean @default(false)
|
||||
|
||||
chunksEnabled Boolean @default(true)
|
||||
chunksMax String @default("95mb")
|
||||
chunksSize String @default("25mb")
|
||||
|
||||
tasksDeleteInterval String @default("30m")
|
||||
tasksClearInvitesInterval String @default("30m")
|
||||
tasksMaxViewsInterval String @default("30m")
|
||||
tasksThumbnailsInterval String @default("30m")
|
||||
tasksMetricsInterval String @default("30m")
|
||||
tasksCleanThumbnailsInterval String @default("1d")
|
||||
|
||||
filesRoute String @default("/u")
|
||||
filesLength Int @default(6)
|
||||
filesDefaultFormat String @default("random")
|
||||
filesDisabledExtensions String[]
|
||||
filesMaxFileSize String @default("100mb")
|
||||
filesDefaultExpiration String?
|
||||
filesMaxExpiration String?
|
||||
filesAssumeMimetypes Boolean @default(false)
|
||||
filesDefaultDateFormat String @default("YYYY-MM-DD_HH:mm:ss")
|
||||
filesRemoveGpsMetadata Boolean @default(false)
|
||||
filesRandomWordsNumAdjectives Int @default(2)
|
||||
filesRandomWordsSeparator String @default("-")
|
||||
filesDefaultCompressionFormat String? @default("jpg")
|
||||
|
||||
urlsRoute String @default("/go")
|
||||
urlsLength Int @default(6)
|
||||
|
||||
featuresImageCompression Boolean @default(true)
|
||||
featuresRobotsTxt Boolean @default(true)
|
||||
featuresHealthcheck Boolean @default(true)
|
||||
featuresUserRegistration Boolean @default(false)
|
||||
featuresOauthRegistration Boolean @default(false)
|
||||
featuresDeleteOnMaxViews Boolean @default(true)
|
||||
|
||||
featuresThumbnailsEnabled Boolean @default(true)
|
||||
featuresThumbnailsNumberThreads Int @default(4)
|
||||
featuresThumbnailsFormat String @default("jpg")
|
||||
|
||||
featuresMetricsEnabled Boolean @default(true)
|
||||
featuresMetricsAdminOnly Boolean @default(false)
|
||||
featuresMetricsShowUserSpecific Boolean @default(true)
|
||||
|
||||
featuresVersionChecking Boolean @default(true)
|
||||
featuresVersionAPI String @default("https://zipline-version.diced.sh")
|
||||
|
||||
invitesEnabled Boolean @default(true)
|
||||
invitesLength Int @default(6)
|
||||
|
||||
websiteTitle String @default("Zipline")
|
||||
websiteTitleLogo String?
|
||||
websiteExternalLinks Json @default("[{ \"name\": \"GitHub\", \"url\": \"https://github.com/diced/zipline\"}, { \"name\": \"Documentation\", \"url\": \"https://zipline.diced.sh/\"}]")
|
||||
websiteLoginBackground String?
|
||||
websiteLoginBackgroundBlur Boolean @default(true)
|
||||
websiteDefaultAvatar String?
|
||||
websiteTos String?
|
||||
|
||||
websiteThemeDefault String @default("system")
|
||||
websiteThemeDark String @default("builtin:dark_gray")
|
||||
websiteThemeLight String @default("builtin:light_gray")
|
||||
|
||||
oauthBypassLocalLogin Boolean @default(false)
|
||||
oauthLoginOnly Boolean @default(false)
|
||||
|
||||
oauthDiscordClientId String?
|
||||
oauthDiscordClientSecret String?
|
||||
oauthDiscordRedirectUri String?
|
||||
oauthDiscordAllowedIds String[] @default([])
|
||||
oauthDiscordDeniedIds String[] @default([])
|
||||
|
||||
oauthGoogleClientId String?
|
||||
oauthGoogleClientSecret String?
|
||||
oauthGoogleRedirectUri String?
|
||||
|
||||
oauthGithubClientId String?
|
||||
oauthGithubClientSecret String?
|
||||
oauthGithubRedirectUri String?
|
||||
|
||||
oauthOidcClientId String?
|
||||
oauthOidcClientSecret String?
|
||||
oauthOidcAuthorizeUrl String?
|
||||
oauthOidcTokenUrl String?
|
||||
oauthOidcUserinfoUrl String?
|
||||
oauthOidcRedirectUri String?
|
||||
|
||||
mfaTotpEnabled Boolean @default(false)
|
||||
mfaTotpIssuer String @default("Zipline")
|
||||
|
||||
mfaPasskeysEnabled Boolean @default(false)
|
||||
mfaPasskeysRpID String?
|
||||
mfaPasskeysOrigin String?
|
||||
|
||||
ratelimitEnabled Boolean @default(true)
|
||||
ratelimitMax Int @default(10)
|
||||
ratelimitWindow Int?
|
||||
ratelimitAdminBypass Boolean @default(true)
|
||||
ratelimitAllowList String[]
|
||||
|
||||
httpWebhookOnUpload String?
|
||||
httpWebhookOnShorten String?
|
||||
|
||||
discordWebhookUrl String?
|
||||
discordUsername String?
|
||||
discordAvatarUrl String?
|
||||
|
||||
discordOnUploadWebhookUrl String?
|
||||
discordOnUploadUsername String?
|
||||
discordOnUploadAvatarUrl String?
|
||||
discordOnUploadContent String?
|
||||
discordOnUploadEmbed Json?
|
||||
|
||||
discordOnShortenWebhookUrl String?
|
||||
discordOnShortenUsername String?
|
||||
discordOnShortenAvatarUrl String?
|
||||
discordOnShortenContent String?
|
||||
discordOnShortenEmbed Json?
|
||||
|
||||
pwaEnabled Boolean @default(false)
|
||||
pwaTitle String @default("Zipline")
|
||||
pwaShortName String @default("Zipline")
|
||||
pwaDescription String @default("Zipline")
|
||||
pwaThemeColor String @default("#000000")
|
||||
pwaBackgroundColor String @default("#000000")
|
||||
|
||||
domains String[] @default([])
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
username String @unique
|
||||
password String?
|
||||
avatar String?
|
||||
token String @unique
|
||||
role Role @default(USER)
|
||||
view Json @default("{}")
|
||||
|
||||
totpSecret String?
|
||||
passkeys UserPasskey[]
|
||||
sessions UserSession[]
|
||||
|
||||
quota UserQuota?
|
||||
|
||||
files File[]
|
||||
urls Url[]
|
||||
folders Folder[]
|
||||
invites Invite[]
|
||||
tags Tag[]
|
||||
oauthProviders OAuthProvider[]
|
||||
IncompleteFile IncompleteFile[]
|
||||
exports Export[]
|
||||
id Int @id @default(autoincrement())
|
||||
username String
|
||||
password String
|
||||
token String
|
||||
administrator Boolean @default(false)
|
||||
systemTheme String @default("dark_blue")
|
||||
customTheme Theme?
|
||||
embedTitle String?
|
||||
embedColor String @default("#2f3136")
|
||||
embedSiteName String? @default("{image.file} • {user.name}")
|
||||
ratelimited Boolean @default(false)
|
||||
images Image[]
|
||||
urls Url[]
|
||||
}
|
||||
|
||||
model Export {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
completed Boolean @default(false)
|
||||
path String
|
||||
files Int
|
||||
size String
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
userId String
|
||||
model Theme {
|
||||
id Int @id @default(autoincrement())
|
||||
type String
|
||||
primary String
|
||||
secondary String
|
||||
error String
|
||||
warning String
|
||||
info String
|
||||
border String
|
||||
mainBackground String
|
||||
paperBackground String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
}
|
||||
|
||||
model UserSession {
|
||||
id String @id
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
ua String
|
||||
client String
|
||||
device String
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
userId String
|
||||
model Image {
|
||||
id Int @id @default(autoincrement())
|
||||
file String
|
||||
mimetype String @default("image/png")
|
||||
created_at DateTime @default(now())
|
||||
views Int @default(0)
|
||||
favorite Boolean @default(false)
|
||||
embed Boolean @default(false)
|
||||
invisible InvisibleImage?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
}
|
||||
|
||||
model UserQuota {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
filesQuota UserFilesQuota
|
||||
maxBytes String?
|
||||
maxFiles Int?
|
||||
|
||||
maxUrls Int?
|
||||
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String? @unique
|
||||
}
|
||||
|
||||
enum UserFilesQuota {
|
||||
BY_BYTES
|
||||
BY_FILES
|
||||
}
|
||||
|
||||
model UserPasskey {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
lastUsed DateTime?
|
||||
|
||||
name String
|
||||
reg Json
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
userId String
|
||||
}
|
||||
|
||||
enum Role {
|
||||
USER
|
||||
ADMIN
|
||||
SUPERADMIN
|
||||
}
|
||||
|
||||
model OAuthProvider {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
userId String
|
||||
provider OAuthProviderType
|
||||
|
||||
username String
|
||||
accessToken String
|
||||
refreshToken String?
|
||||
oauthId String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([provider, oauthId])
|
||||
}
|
||||
|
||||
enum OAuthProviderType {
|
||||
DISCORD
|
||||
GOOGLE
|
||||
GITHUB
|
||||
OIDC
|
||||
}
|
||||
|
||||
model File {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletesAt DateTime?
|
||||
|
||||
name String // name & file saved on datasource
|
||||
originalName String? // original name of file when uploaded
|
||||
size BigInt
|
||||
type String
|
||||
views Int @default(0)
|
||||
maxViews Int?
|
||||
favorite Boolean @default(false)
|
||||
password String?
|
||||
|
||||
tags Tag[]
|
||||
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
userId String?
|
||||
|
||||
Folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
folderId String?
|
||||
|
||||
thumbnail Thumbnail?
|
||||
}
|
||||
|
||||
model Thumbnail {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
path String
|
||||
|
||||
file File @relation(fields: [fileId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
fileId String
|
||||
|
||||
@@unique([fileId])
|
||||
}
|
||||
|
||||
model Folder {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String
|
||||
public Boolean @default(false)
|
||||
allowUploads Boolean @default(false)
|
||||
|
||||
files File[]
|
||||
|
||||
parentId String?
|
||||
parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
children Folder[] @relation("FolderToFolder")
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
userId String
|
||||
}
|
||||
|
||||
model IncompleteFile {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
status IncompleteFileStatus
|
||||
chunksTotal Int
|
||||
chunksComplete Int
|
||||
|
||||
metadata Json
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
userId String
|
||||
}
|
||||
|
||||
enum IncompleteFileStatus {
|
||||
PENDING
|
||||
PROCESSING
|
||||
COMPLETE
|
||||
FAILED
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String @unique
|
||||
color String
|
||||
|
||||
files File[]
|
||||
User User? @relation(fields: [userId], references: [id])
|
||||
userId String?
|
||||
model InvisibleImage {
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
imageId Int
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model Url {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
code String
|
||||
vanity String?
|
||||
id String @id @unique
|
||||
destination String
|
||||
views Int @default(0)
|
||||
maxViews Int?
|
||||
password String?
|
||||
enabled Boolean @default(true)
|
||||
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
userId String?
|
||||
|
||||
@@unique([code, vanity])
|
||||
vanity String?
|
||||
created_at DateTime @default(now())
|
||||
views Int @default(0)
|
||||
invisible InvisibleUrl?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
}
|
||||
|
||||
model Metric {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
data Json
|
||||
model InvisibleUrl {
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
urlId String
|
||||
url Url @relation(fields: [urlId], references: [id])
|
||||
}
|
||||
|
||||
model Invite {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
expiresAt DateTime?
|
||||
|
||||
code String @unique
|
||||
uses Int @default(0)
|
||||
maxUses Int?
|
||||
|
||||
inviter User @relation(fields: [inviterId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
inviterId String
|
||||
}
|
||||
model Stats {
|
||||
id Int @id @default(autoincrement())
|
||||
created_at DateTime @default(now())
|
||||
data Json
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { hashPassword, createToken } from '../src/lib/util';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username: 'administrator',
|
||||
password: await hashPassword('password'),
|
||||
token: createToken(),
|
||||
administrator: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`
|
||||
When logging into Zipline for the first time, use these credentials:
|
||||
|
||||
Username: "${user.username}"
|
||||
Password: "password"
|
||||
`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -1,24 +0,0 @@
|
||||
import { run, step } from '.';
|
||||
import { lintStep } from './lint';
|
||||
|
||||
run(
|
||||
'build',
|
||||
|
||||
lintStep,
|
||||
step('prisma', 'prisma generate'),
|
||||
step('typecheck', 'tsc', () => !process.argv.includes('--skip')),
|
||||
|
||||
// builds
|
||||
step('server', 'tsup'),
|
||||
|
||||
// client stuff
|
||||
step('client', 'vite build'),
|
||||
step(
|
||||
'client/ssr/view',
|
||||
'vite build --ssr ssr-view/server.tsx -m ssr-view --outDir ../../build/ssr --emptyOutDir=false',
|
||||
),
|
||||
step(
|
||||
'client/ssr/view-url',
|
||||
'vite build --ssr ssr-view-url/server.tsx -m ssr-view-url --outDir ../../build/ssr --emptyOutDir=false',
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
const Logger = require('../src/lib/logger');
|
||||
const prismaRun = require('./prisma-run');
|
||||
|
||||
module.exports = async (config) => {
|
||||
try {
|
||||
await prismaRun(config.core.database_url, ['migrate', 'deploy']);
|
||||
await prismaRun(config.core.database_url, ['generate'], true);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
Logger.get('db').error('there was an error.. exiting..');
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
type StepCommand = string | (() => void | Promise<void>);
|
||||
|
||||
export function step(name: string, command: StepCommand, condition: () => boolean = () => true) {
|
||||
return {
|
||||
name,
|
||||
command,
|
||||
condition,
|
||||
};
|
||||
}
|
||||
|
||||
export type Step = ReturnType<typeof step>;
|
||||
|
||||
function log(message: string) {
|
||||
console.log(`\n${message}\n`);
|
||||
}
|
||||
|
||||
export async function run(name: string, ...steps: Step[]) {
|
||||
const { execSync } = await import('child_process');
|
||||
|
||||
const runOne = process.argv[2];
|
||||
if (runOne) {
|
||||
const match = steps.find((s) => `${name}/${s.name}` === runOne);
|
||||
if (!match) {
|
||||
console.error(`x No step found with name "${runOne}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
steps = [match];
|
||||
}
|
||||
|
||||
const start = process.hrtime();
|
||||
for (const step of steps) {
|
||||
if (!step.condition()) {
|
||||
log(`- Skipping step "${name}/${step.name}"...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
log(`> Running step "${name}/${step.name}"...`);
|
||||
if (typeof step.command === 'string') {
|
||||
execSync(step.command, { stdio: 'inherit' });
|
||||
} else {
|
||||
await step.command();
|
||||
}
|
||||
} catch {
|
||||
console.error(`x Step "${name}/${step.name}" failed.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const diff = process.hrtime(start);
|
||||
const time = diff[0] * 1e9 + diff[1];
|
||||
const timeStr = time > 1e9 ? `${(time / 1e9).toFixed(2)}s` : `${(time / 1e6).toFixed(2)}ms`;
|
||||
log(`✓ Steps in "${name}" completed in ${timeStr}.`);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { step } from '.';
|
||||
|
||||
export const lintStep = step('lint', 'eslint .');
|
||||
@@ -0,0 +1,35 @@
|
||||
const { readdir } = require('fs/promises');
|
||||
const { extname } = require('path');
|
||||
const validateConfig = require('../server/validateConfig');
|
||||
const Logger = require('../src/lib/logger');
|
||||
const readConfig = require('../src/lib/readConfig');
|
||||
const mimes = require('./mimes');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
|
||||
(async () => {
|
||||
const config = readConfig();
|
||||
|
||||
await validateConfig(config);
|
||||
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
|
||||
const files = await readdir(process.argv[2]);
|
||||
const data = files.map(x => {
|
||||
const mime = mimes[extname(x)] ?? 'application/octet-stream';
|
||||
|
||||
return {
|
||||
file: x,
|
||||
mimetype: mime,
|
||||
userId: 1
|
||||
};
|
||||
});
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
Logger.get('migrator').info('starting migrations...');
|
||||
await prisma.image.createMany({
|
||||
data
|
||||
});
|
||||
Logger.get('migrator').info('finished migrations! It is recomended to move your old uploads folder (' + process.argv[2] + ') to the current one which is ' + config.uploader.directory);
|
||||
process.exit();
|
||||
})();
|
||||
@@ -0,0 +1,78 @@
|
||||
module.exports = {
|
||||
'.aac': 'audio/aac',
|
||||
'.abw': 'application/x-abiword',
|
||||
'.arc': 'application/x-freearc',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.azw': 'application/vnd.amazon.ebook',
|
||||
'.bin': 'application/octet-stream',
|
||||
'.bmp': 'image/bmp',
|
||||
'.bz': 'application/x-bzip',
|
||||
'.bz2': 'application/x-bzip2',
|
||||
'.cda': 'application/x-cdf',
|
||||
'.csh': 'application/x-csh',
|
||||
'.css': 'text/css',
|
||||
'.csv': 'text/csv',
|
||||
'.doc': 'application/msword',
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'.eot': 'application/vnd.ms-fontobject',
|
||||
'.epub': 'application/epub+zip',
|
||||
'.gz': 'application/gzip',
|
||||
'.gif': 'image/gif',
|
||||
'.htm': 'text/html',
|
||||
'.html': 'text/html',
|
||||
'.ico': 'image/vnd.microsoft.icon',
|
||||
'.ics': 'text/calendar',
|
||||
'.jar': 'application/java-archive',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.js': 'text/javascript',
|
||||
'.json': 'application/json',
|
||||
'.jsonld': 'application/ld+json',
|
||||
'.mid': 'audio/midi',
|
||||
'.midi': 'audio/midi',
|
||||
'.mjs': 'text/javascript',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.mp4': 'video/mp4',
|
||||
'.mpeg': 'video/mpeg',
|
||||
'.mpkg': 'application/vnd.apple.installer+xml',
|
||||
'.odp': 'application/vnd.oasis.opendocument.presentation',
|
||||
'.ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'.odt': 'application/vnd.oasis.opendocument.text',
|
||||
'.oga': 'audio/ogg',
|
||||
'.ogv': 'video/ogg',
|
||||
'.ogx': 'application/ogg',
|
||||
'.opus': 'audio/opus',
|
||||
'.otf': 'font/otf',
|
||||
'.png': 'image/png',
|
||||
'.pdf': 'application/pdf',
|
||||
'.php': 'application/x-httpd-php',
|
||||
'.ppt': 'application/vnd.ms-powerpoint',
|
||||
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'.rar': 'application/vnd.rar',
|
||||
'.rtf': 'application/rtf',
|
||||
'.sh': 'application/x-sh',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.swf': 'application/x-shockwave-flash',
|
||||
'.tar': 'application/x-tar',
|
||||
'.tif': 'image/tiff',
|
||||
'.tiff': 'image/tiff',
|
||||
'.ts': 'video/mp2t',
|
||||
'.ttf': 'font/ttf',
|
||||
'.txt': 'text/plain',
|
||||
'.vsd': 'application/vnd.visio',
|
||||
'.wav': 'audio/wav',
|
||||
'.weba': 'audio/webm',
|
||||
'.webm': 'video/webm',
|
||||
'.webp': 'image/webp',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.xhtml': 'application/xhtml+xml',
|
||||
'.xls': 'application/vnd.ms-excel',
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.xml': 'application/xml',
|
||||
'.xul': 'application/vnd.mozilla.xul+xml',
|
||||
'.zip': 'application/zip',
|
||||
'.3gp': 'video/3gpp',
|
||||
'.3g2': 'video/3gpp2',
|
||||
'.7z': 'application/x-7z-compressed'
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
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 ALL_ERRORS = Object.keys(API_ERRORS)
|
||||
.map((code) => new ApiError(Number(code) as ApiErrorCode).toJSON())
|
||||
.sort((a, b) => a.code - b.code);
|
||||
|
||||
const ERROR_SCHEMA = {
|
||||
type: 'object',
|
||||
description: 'Generic error for API endpoints.',
|
||||
properties: {
|
||||
error: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Message for the error. This may differ from the standard message for the error code, but the error code should be used to figure out the type of error.',
|
||||
},
|
||||
code: {
|
||||
type: 'integer',
|
||||
format: 'int32',
|
||||
description:
|
||||
'Zipline API error code. Ranges: 1xxx validation, 2xxx session, 3xxx permission, 4xxx not-found, 5xxx constraint, 6xxx internal, 9xxx generic.',
|
||||
enum: ALL_ERRORS.map((entry) => entry.code),
|
||||
'x-enumDescriptions': ALL_ERRORS.map((entry) => entry.message),
|
||||
},
|
||||
statusCode: {
|
||||
type: 'integer',
|
||||
format: 'int32',
|
||||
description: 'HTTP status code returned alongside this error payload.',
|
||||
},
|
||||
},
|
||||
required: ['error', 'code', 'statusCode'],
|
||||
additionalProperties: true,
|
||||
};
|
||||
|
||||
const ERROR_EXAMPLES = ALL_ERRORS.reduce<Record<string, unknown>>((examples, entry) => {
|
||||
examples[`E${entry.code}`] = {
|
||||
summary: `${entry.error}`,
|
||||
value: entry,
|
||||
};
|
||||
|
||||
return examples;
|
||||
}, {});
|
||||
|
||||
const generic4xxResponse = {
|
||||
description: 'API error response (4xx)',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: ERROR_SCHEMA,
|
||||
examples: ERROR_EXAMPLES,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function addErrorResponse(responses: Record<string, any>): void {
|
||||
const response = (responses['4xx'] ??= structuredClone(generic4xxResponse));
|
||||
|
||||
response.description ??= generic4xxResponse.description;
|
||||
response.content ??= {};
|
||||
|
||||
const jsonContent = (response.content['application/json'] ??= {});
|
||||
jsonContent.schema ??= structuredClone(ERROR_SCHEMA);
|
||||
jsonContent.examples ??= structuredClone(generic4xxResponse.content['application/json'].examples);
|
||||
}
|
||||
|
||||
function filterRoutes(paths = {}): Record<string, any> {
|
||||
return Object.fromEntries(Object.entries(paths).filter(([route]) => route.startsWith('/api')));
|
||||
}
|
||||
|
||||
async function fixSpec() {
|
||||
const spec = JSON.parse(await readFile(GEN_PATH, 'utf8'));
|
||||
|
||||
spec.paths = filterRoutes(spec.paths);
|
||||
|
||||
for (const [, pathItem] of Object.entries(spec.paths ?? {})) {
|
||||
if (!pathItem) continue;
|
||||
|
||||
for (const method of ALL_METHODS) {
|
||||
const operation = (<any>pathItem)[method];
|
||||
if (!operation) continue;
|
||||
|
||||
operation.responses ??= {};
|
||||
addErrorResponse(operation.responses);
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(GEN_PATH, JSON.stringify(spec));
|
||||
}
|
||||
|
||||
process.env.ZIPLINE_OUTPUT_OPENAPI = 'true';
|
||||
|
||||
run(
|
||||
'openapi',
|
||||
step('run-prod', 'pnpm start', () => process.env.NODE_ENV === 'production'),
|
||||
step('run-dev', 'pnpm dev', () => process.env.NODE_ENV !== 'production'),
|
||||
step('check', async () => {
|
||||
try {
|
||||
await readFile(GEN_PATH);
|
||||
} catch (e) {
|
||||
console.error('\nSomething went wrong...', e);
|
||||
|
||||
throw new Error('No OpenAPI spec found at ./openapi.json');
|
||||
}
|
||||
}),
|
||||
step('fix', fixSpec),
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
const { spawn } = require('child_process');
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = (url, args, nostdout = false) => {
|
||||
return new Promise((res, rej) => {
|
||||
const proc = spawn(join(process.cwd(), 'node_modules', '.bin', 'prisma'), args, {
|
||||
env: {
|
||||
DATABASE_URL: url,
|
||||
...process.env
|
||||
},
|
||||
});
|
||||
|
||||
let a = '';
|
||||
|
||||
proc.stdout.on('data', d => {
|
||||
if (!nostdout) console.log(d.toString());
|
||||
a += d.toString();
|
||||
});
|
||||
proc.stderr.on('data', d => {
|
||||
if (!nostdout) console.log(d.toString());
|
||||
rej(d.toString());
|
||||
});
|
||||
proc.stdout.on('end', () => res(a));
|
||||
proc.stdout.on('close', () => res(a));
|
||||
});
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { run, step } from '.';
|
||||
import { lintStep } from './lint';
|
||||
|
||||
run(
|
||||
'validate',
|
||||
|
||||
lintStep,
|
||||
step('format', 'prettier --write --ignore-path .gitignore .'),
|
||||
);
|
||||
@@ -0,0 +1,260 @@
|
||||
const next = require('next');
|
||||
const { createServer } = require('http');
|
||||
const { stat, mkdir, readdir } = require('fs/promises');
|
||||
const { execSync } = require('child_process');
|
||||
const { extname, join } = require('path');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const validateConfig = require('./validateConfig');
|
||||
const Logger = require('../src/lib/logger');
|
||||
const getFile = require('./static');
|
||||
const prismaRun = require('../scripts/prisma-run');
|
||||
const readConfig = require('../src/lib/readConfig');
|
||||
const mimes = require('../scripts/mimes');
|
||||
const deployDb = require('../scripts/deploy-db');
|
||||
const { version } = require('../package.json');
|
||||
|
||||
Logger.get('server').info(`starting zipline@${version} server`);
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
|
||||
function log(url, status) {
|
||||
if (url.startsWith('/_next') || url.startsWith('/__nextjs')) return;
|
||||
return Logger.get('url').info(url);
|
||||
}
|
||||
|
||||
function shouldUseYarn() {
|
||||
try {
|
||||
execSync('yarnpkg --version', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const a = readConfig();
|
||||
const config = await validateConfig(a);
|
||||
|
||||
const data = await prismaRun(config.core.database_url, ['migrate', 'status'], true);
|
||||
if (data.match(/Following migrations? have not yet been applied/)) {
|
||||
Logger.get('database').info('some migrations are not applied, applying them now...');
|
||||
await deployDb(config);
|
||||
Logger.get('database').info('finished applying migrations');
|
||||
} else Logger.get('database').info('migrations up to date');
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
|
||||
await mkdir(config.uploader.directory, { recursive: true });
|
||||
|
||||
const app = next({
|
||||
dir: '.',
|
||||
dev,
|
||||
quiet: dev,
|
||||
}, config.core.port, config.core.host);
|
||||
|
||||
await app.prepare();
|
||||
await stat('./.next');
|
||||
|
||||
const handle = app.getRequestHandler();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const srv = createServer(async (req, res) => {
|
||||
if (req.url.startsWith('/r')) {
|
||||
const parts = req.url.split('/');
|
||||
if (!parts[2] || parts[2] === '') return;
|
||||
|
||||
let image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ file: parts[2] },
|
||||
{ invisible:{ invis: decodeURI(parts[2]) } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
const data = await getFile(config.uploader.directory, parts[2]);
|
||||
if (!data) return app.render404(req, res);
|
||||
|
||||
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimetype);
|
||||
res.end(data);
|
||||
} else {
|
||||
const data = await getFile(config.uploader.directory, image.file);
|
||||
if (!data) return app.render404(req, res);
|
||||
|
||||
await prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
res.end(data);
|
||||
}
|
||||
} else if (req.url.startsWith(config.uploader.route)) {
|
||||
const parts = req.url.split('/');
|
||||
if (!parts[2] || parts[2] === '') return;
|
||||
|
||||
let image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ file: parts[2] },
|
||||
{ invisible:{ invis: decodeURI(parts[2]) } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
embed: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
const data = await getFile(config.uploader.directory, parts[2]);
|
||||
if (!data) return app.render404(req, res);
|
||||
|
||||
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimetype);
|
||||
res.end(data);
|
||||
} else if (image.embed) {
|
||||
handle(req, res);
|
||||
} else {
|
||||
const data = await getFile(config.uploader.directory, image.file);
|
||||
if (!data) return app.render404(req, res);
|
||||
|
||||
await prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
res.end(data);
|
||||
}
|
||||
} else {
|
||||
handle(req, res);
|
||||
}
|
||||
|
||||
if (config.core.logger) log(req.url, res.statusCode);
|
||||
});
|
||||
|
||||
srv.on('error', (e) => {
|
||||
Logger.get('server').error(e);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
srv.on('listening', () => {
|
||||
Logger.get('server').info(`listening on ${config.core.host}:${config.core.port}`);
|
||||
if (process.platform === 'linux' && dev) execSync(`xdg-open ${config.core.secure ? 'https' : 'http'}://${config.core.host === '0.0.0.0' ? 'localhost' : config.core.host}:${config.core.port}`);
|
||||
});
|
||||
|
||||
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');
|
||||
|
||||
const stats = await getStats(prisma, config);
|
||||
await prisma.stats.create({
|
||||
data: {
|
||||
data: stats,
|
||||
},
|
||||
});
|
||||
setInterval(async () => {
|
||||
const stats = await getStats(prisma, config);
|
||||
await prisma.stats.create({
|
||||
data: {
|
||||
data: stats,
|
||||
},
|
||||
});
|
||||
if (config.core.logger) Logger.get('server').info('stats updated');
|
||||
}, config.core.stats_interval * 1000);
|
||||
} catch (e) {
|
||||
if (e.message && e.message.startsWith('Could not find a production')) {
|
||||
Logger.get('web').error(`there is no production build - run \`${shouldUseYarn() ? 'yarn build' : 'npm build'}\``);
|
||||
} else if (e.code && e.code === 'ENOENT') {
|
||||
if (e.path === './.next') Logger.get('web').error(`there is no production build - run \`${shouldUseYarn() ? 'yarn build' : 'npm build'}\``);
|
||||
} else {
|
||||
Logger.get('server').error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
async function sizeOfDir(directory) {
|
||||
const files = await readdir(directory);
|
||||
|
||||
let size = 0;
|
||||
for (let i = 0, L = files.length; i !== L; ++i) {
|
||||
const sta = await stat(join(directory, files[i]));
|
||||
size += sta.size;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
function bytesToRead(bytes) {
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||
let num = 0;
|
||||
|
||||
while (bytes > 1024) {
|
||||
bytes /= 1024;
|
||||
++num;
|
||||
}
|
||||
|
||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||
}
|
||||
|
||||
|
||||
async function getStats(prisma, config) {
|
||||
const size = await sizeOfDir(join(process.cwd(), config.uploader.directory));
|
||||
const byUser = await prisma.image.groupBy({
|
||||
by: ['userId'],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
});
|
||||
const count_users = await prisma.user.count();
|
||||
|
||||
const count_by_user = [];
|
||||
for (let i = 0, L = byUser.length; i !== L; ++i) {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: byUser[i].userId,
|
||||
},
|
||||
});
|
||||
|
||||
count_by_user.push({
|
||||
username: user.username,
|
||||
count: byUser[i]._count._all,
|
||||
});
|
||||
}
|
||||
|
||||
const count = await prisma.image.count();
|
||||
const viewsCount = await prisma.image.groupBy({
|
||||
by: ['views'],
|
||||
_sum: {
|
||||
views: true,
|
||||
},
|
||||
});
|
||||
|
||||
const typesCount = await prisma.image.groupBy({
|
||||
by: ['mimetype'],
|
||||
_count: {
|
||||
mimetype: true,
|
||||
},
|
||||
});
|
||||
const types_count = [];
|
||||
for (let i = 0, L = typesCount.length; i !== L; ++i) types_count.push({ mimetype: typesCount[i].mimetype, count: typesCount[i]._count.mimetype });
|
||||
|
||||
return {
|
||||
size: bytesToRead(size),
|
||||
size_num: size,
|
||||
count,
|
||||
count_by_user: count_by_user.sort((a,b) => b.count-a.count),
|
||||
count_users,
|
||||
views_count: (viewsCount[0]?._sum?.views ?? 0),
|
||||
types_count: types_count.sort((a,b) => b.count-a.count),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
const { readFile } = require('fs/promises');
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = async (dir, file) => {
|
||||
try {
|
||||
const data = await readFile(join(process.cwd(), dir, file));
|
||||
return data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
const Logger = require('../src/lib/logger');
|
||||
const yup = require('yup');
|
||||
|
||||
|
||||
const validator = yup.object({
|
||||
core: yup.object({
|
||||
secure: yup.bool().default(false),
|
||||
secret: yup.string().min(8).required(),
|
||||
host: yup.string().default('0.0.0.0'),
|
||||
port: yup.number().default(3000),
|
||||
database_url: yup.string().required(),
|
||||
logger: yup.boolean().default(true),
|
||||
stats_interval: yup.number().default(1800),
|
||||
}).required(),
|
||||
uploader: yup.object({
|
||||
route: yup.string().required(),
|
||||
length: yup.number().default(6),
|
||||
directory: yup.string().required(),
|
||||
admin_limit: yup.number().default(104900000),
|
||||
user_limit: yup.number().default(104900000),
|
||||
disabled_extensions: yup.array().default([]),
|
||||
}).required(),
|
||||
urls: yup.object({
|
||||
route: yup.string().required(),
|
||||
length: yup.number().default(6),
|
||||
}).required(),
|
||||
ratelimit: yup.object({
|
||||
user: yup.number().default(0),
|
||||
admin: yup.number().default(0),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
module.exports = config => {
|
||||
try {
|
||||
return validator.validateSync(config, { abortEarly: false });
|
||||
} catch (e) {
|
||||
throw `${e.errors.length} errors occured\n${e.errors.map(x => '\t' + x).join('\n')}`;
|
||||
}
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ContextModalProps, ModalsProvider } from '@mantine/modals';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { SWRConfig } from 'swr';
|
||||
import ThemeProvider from '@/components/ThemeProvider';
|
||||
import { type ZiplineTheme } from '@/lib/theme';
|
||||
import { type Config } from '@/lib/config/validate';
|
||||
import { Button, Text } from '@mantine/core';
|
||||
|
||||
const AlertModal = ({ context, id, innerProps }: ContextModalProps<{ modalBody: string }>) => (
|
||||
<>
|
||||
<Text size='sm'>{innerProps.modalBody}</Text>
|
||||
|
||||
<Button fullWidth mt='md' onClick={() => context.closeModal(id)}>
|
||||
OK
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
const contextModals = {
|
||||
alert: AlertModal,
|
||||
};
|
||||
|
||||
declare module '@mantine/modals' {
|
||||
export interface MantineModalsOverride {
|
||||
modals: typeof contextModals;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Root({
|
||||
themes,
|
||||
defaultTheme,
|
||||
}: {
|
||||
themes?: ZiplineTheme[];
|
||||
defaultTheme?: Config['website']['theme'];
|
||||
}) {
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: async (url: RequestInfo | URL) => {
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
|
||||
throw new Error(json.message);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ThemeProvider ssrThemes={themes} ssrDefaultTheme={defaultTheme}>
|
||||
<ModalsProvider
|
||||
modalProps={{
|
||||
overlayProps: {
|
||||
blur: 6,
|
||||
},
|
||||
centered: true,
|
||||
}}
|
||||
modals={contextModals}
|
||||
>
|
||||
<Notifications position='top-center' zIndex={10000000} />
|
||||
<Outlet />
|
||||
</ModalsProvider>
|
||||
</ThemeProvider>
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import GenericError from './GenericError';
|
||||
|
||||
export default function DashboardErrorBoundary(props: Record<string, any>) {
|
||||
return (
|
||||
<GenericError
|
||||
title='Dashboard Client Error'
|
||||
message='Something went wrong while loading the dashboard. Please try again later, or report this issue if it persists.'
|
||||
details={{ ...props, type: 'dashboard' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Container, Paper, ScrollArea, Stack, Text, Title } from '@mantine/core';
|
||||
import { useRouteError } from 'react-router-dom';
|
||||
import FourOhFour from '../pages/404';
|
||||
|
||||
export default function GenericError({
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
}: {
|
||||
title?: string;
|
||||
message?: string;
|
||||
details?: Record<string, any>;
|
||||
}) {
|
||||
const routerError: any = useRouteError();
|
||||
if (routerError?.status === 404) return <FourOhFour />;
|
||||
|
||||
const routeError = JSON.parse(JSON.stringify(routerError, Object.getOwnPropertyNames(routerError)));
|
||||
|
||||
console.error(routerError);
|
||||
|
||||
return (
|
||||
<Container my='lg'>
|
||||
<Stack gap='xs'>
|
||||
<Title order={5}>{title || 'An error occurred'}</Title>
|
||||
<Text c='dimmed'>
|
||||
{message || 'Something went wrong. Please try again later, or report this issue if it persists.'}
|
||||
</Text>
|
||||
{details && (
|
||||
<Paper withBorder px={3} py={3}>
|
||||
<ScrollArea>
|
||||
<pre style={{ margin: 0 }}>{JSON.stringify({ routeError, details }, null, 2)}</pre>
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import GenericError from './GenericError';
|
||||
|
||||
export default function RootErrorBoundary(props: Record<string, any>) {
|
||||
return (
|
||||
<GenericError
|
||||
title='Dashboard Client Error'
|
||||
message='Something went wrong while loading the dashboard. Please try again later, or report this issue if it persists.'
|
||||
details={{ ...props, type: 'root' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<title>Zipline</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './routes';
|
||||
|
||||
import '@mantine/charts/styles.css';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/dropzone/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import 'mantine-datatable/styles.css';
|
||||
import './styles/global.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
import { Button, Center, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconArrowLeft } from '@tabler/icons-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function FourOhFour() {
|
||||
useTitle('404');
|
||||
|
||||
return (
|
||||
<Center h='100vh'>
|
||||
<Stack>
|
||||
<Title order={1}>404</Title>
|
||||
<Text c='dimmed' mt='-md'>
|
||||
Page not found
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to='/auth/login'
|
||||
color='blue'
|
||||
fullWidth
|
||||
leftSection={<IconArrowLeft size='1rem' />}
|
||||
>
|
||||
Go home
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
import ExternalAuthButton from '@/components/pages/login/ExternalAuthButton';
|
||||
import LocalLogin from '@/components/pages/login/LocalLogin';
|
||||
import PasskeyAuthButton from '@/components/pages/login/PasskeyAuthButton';
|
||||
import SecureWarningModal from '@/components/pages/login/SecureWarningModal';
|
||||
import TotpModal from '@/components/pages/login/TotpModal';
|
||||
import { getWebClient } from '@/lib/api/detect';
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import useLogin from '@/lib/hooks/useLogin';
|
||||
import useObjectState from '@/lib/hooks/useObjectState';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
import {
|
||||
Anchor,
|
||||
Box,
|
||||
Center,
|
||||
Divider,
|
||||
Group,
|
||||
Image,
|
||||
LoadingOverlay,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';
|
||||
import {
|
||||
IconBrandDiscordFilled,
|
||||
IconBrandGithubFilled,
|
||||
IconBrandGoogleFilled,
|
||||
IconCheck,
|
||||
IconCircleKeyFilled,
|
||||
} from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
import GenericError from '../../error/GenericError';
|
||||
|
||||
export default function Login() {
|
||||
useTitle('Login');
|
||||
|
||||
const query = new URLSearchParams(location.search);
|
||||
const navigate = useNavigate();
|
||||
const { user, mutate } = useLogin();
|
||||
|
||||
const isHttps = window.location.protocol === 'https:';
|
||||
const webClient = JSON.stringify(getWebClient());
|
||||
|
||||
const { data: config, error: configError, isLoading: configLoading } = useSWR('/api/server/public');
|
||||
|
||||
const showLocalLogin =
|
||||
query.get('local') === 'true' ||
|
||||
!(
|
||||
config?.oauth?.bypassLocalLogin &&
|
||||
Object.values(config?.oauthEnabled ?? {}).filter((x) => x === true).length > 0
|
||||
);
|
||||
|
||||
const willRedirect =
|
||||
config?.oauth?.bypassLocalLogin &&
|
||||
Object.values(config?.oauthEnabled ?? {}).filter((x) => x === true).length === 1 &&
|
||||
query.get('local') !== 'true';
|
||||
|
||||
useEffect(() => {
|
||||
if (willRedirect && config) {
|
||||
const provider = Object.keys(config.oauthEnabled).find(
|
||||
(x) => config.oauthEnabled[x as keyof typeof config.oauthEnabled] === true,
|
||||
);
|
||||
|
||||
if (provider) window.location.href = `/api/auth/oauth/${provider.toLowerCase()}`;
|
||||
}
|
||||
}, [willRedirect, config]);
|
||||
|
||||
const [totp, setTotp] = useObjectState({
|
||||
open: false,
|
||||
disabled: false,
|
||||
error: '',
|
||||
pin: '',
|
||||
});
|
||||
|
||||
const [secureModal, setSecureModal] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: { username: '', password: '' },
|
||||
validate: {
|
||||
username: (v) => (v.length >= 1 ? null : 'Username is required'),
|
||||
password: (v) => (v.length >= 1 ? null : 'Password is required'),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) navigate('/dashboard');
|
||||
if (config?.firstSetup) navigate('/auth/setup');
|
||||
}, [user, config, navigate]);
|
||||
|
||||
const handleLoginSubmit = async (values: any, code?: string) => {
|
||||
setTotp({ disabled: true, error: '' });
|
||||
|
||||
const { data, error } = await fetchApi(
|
||||
'/api/auth/login',
|
||||
'POST',
|
||||
{ ...values, code },
|
||||
{ 'x-zipline-client': webClient },
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (ApiError.check(error, 1044)) {
|
||||
form.setFieldError('username', 'Invalid username');
|
||||
form.setFieldError('password', 'Invalid password');
|
||||
} else {
|
||||
setTotp('error', error.error || 'Login failed');
|
||||
}
|
||||
setTotp('disabled', false);
|
||||
} else if (data?.totp) {
|
||||
setTotp({ open: true, disabled: false });
|
||||
} else {
|
||||
showNotification({
|
||||
message: 'Logging in...',
|
||||
icon: <IconCheck size='1rem' />,
|
||||
autoClose: 700,
|
||||
});
|
||||
mutate(data);
|
||||
}
|
||||
};
|
||||
|
||||
if (configLoading || !config) return <LoadingOverlay visible />;
|
||||
if (configError) return <GenericError title='Error' message='Config load failed' details={configError} />;
|
||||
|
||||
const hasBg = !!config.website.loginBackground;
|
||||
|
||||
return (
|
||||
<>
|
||||
{willRedirect && !showLocalLogin && <LoadingOverlay visible />}
|
||||
|
||||
<TotpModal
|
||||
state={totp}
|
||||
onPinChange={(val) => setTotp('pin', val)}
|
||||
onVerify={() => handleLoginSubmit(form.values, totp.pin)}
|
||||
onCancel={() => {
|
||||
setTotp('open', false);
|
||||
form.reset();
|
||||
}}
|
||||
/>
|
||||
|
||||
<SecureWarningModal
|
||||
opened={secureModal}
|
||||
onClose={() => setSecureModal(false)}
|
||||
returnHttps={config.returnHttps}
|
||||
/>
|
||||
|
||||
{isHttps && !config.returnHttps && (
|
||||
<Box pos='absolute' top={10} left='50%' style={{ transform: 'translateX(-50%)' }}>
|
||||
<Text size='sm' c='red' ta='center'>
|
||||
You are accessing this instance through a <b>secure</b> context but the server is not configured
|
||||
to use HTTPS. Click <Anchor onClick={() => setSecureModal(true)}> here</Anchor> to learn more.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isHttps && config.returnHttps && (
|
||||
<Box pos='absolute' top={10} left='50%' style={{ transform: 'translateX(-50%)' }}>
|
||||
<Text size='sm' c='red' ta='center'>
|
||||
You are accessing this instance through an <b>insecure</b> context but the server is configured to
|
||||
use HTTPS. This may cause issues when logging in. Click{' '}
|
||||
<Anchor onClick={() => setSecureModal(true)}> here</Anchor> to learn more.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Center h='100vh'>
|
||||
{hasBg && (
|
||||
<Image
|
||||
src={config.website.loginBackground}
|
||||
pos='absolute'
|
||||
inset={0}
|
||||
w='100%'
|
||||
h='100%'
|
||||
fit='cover'
|
||||
style={{ filter: config.website.loginBackgroundBlur ? 'blur(10px)' : undefined }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Paper
|
||||
w='350px'
|
||||
p='xl'
|
||||
shadow='xl'
|
||||
withBorder
|
||||
pos='relative'
|
||||
style={{
|
||||
backgroundColor: hasBg ? 'transparent' : undefined,
|
||||
backdropFilter: hasBg ? 'blur(35px)' : undefined,
|
||||
}}
|
||||
>
|
||||
<Title order={1} ta='center' mb='md'>
|
||||
<b>{config.website.title ?? 'Zipline'}</b>
|
||||
</Title>
|
||||
|
||||
<Stack>
|
||||
{showLocalLogin && (
|
||||
<LocalLogin
|
||||
form={form}
|
||||
onSubmit={handleLoginSubmit}
|
||||
loading={totp.disabled}
|
||||
hasBackground={hasBg}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider label='or' />
|
||||
|
||||
{config.mfa.passkeys && browserSupportsWebAuthn() && <PasskeyAuthButton onAuthSuccess={mutate} />}
|
||||
|
||||
<Group grow>
|
||||
{config.oauthEnabled.discord && (
|
||||
<ExternalAuthButton
|
||||
provider='Discord'
|
||||
leftSection={<IconBrandDiscordFilled stroke={4} size='1.1rem' />}
|
||||
/>
|
||||
)}
|
||||
{config.oauthEnabled.github && (
|
||||
<ExternalAuthButton provider='GitHub' leftSection={<IconBrandGithubFilled size='1.1rem' />} />
|
||||
)}
|
||||
{config.oauthEnabled.google && (
|
||||
<ExternalAuthButton
|
||||
provider='Google'
|
||||
leftSection={<IconBrandGoogleFilled stroke={4} size='1.1rem' />}
|
||||
/>
|
||||
)}
|
||||
{config.oauthEnabled.oidc && (
|
||||
<ExternalAuthButton provider='OIDC' leftSection={<IconCircleKeyFilled size='1.1rem' />} />
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Image,
|
||||
LoadingOverlay,
|
||||
Paper,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications, showNotification } from '@mantine/notifications';
|
||||
import { IconLogin, IconPlus, IconUserPlus, IconX } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import GenericError from '../../error/GenericError';
|
||||
import { getWebClient } from '@/lib/api/detect';
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Register');
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const {
|
||||
data: config,
|
||||
error: configError,
|
||||
isLoading: configLoading,
|
||||
} = useSWR<Response['/api/server/public']>('/api/server/public', {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenHidden: false,
|
||||
revalidateIfStale: false,
|
||||
});
|
||||
|
||||
const code = new URLSearchParams(location.search).get('code') ?? undefined;
|
||||
const {
|
||||
data: invite,
|
||||
error: inviteError,
|
||||
isLoading: inviteLoading,
|
||||
} = useSWR<Response['/api/auth/invites/web']>(
|
||||
location.search.includes('code') ? `/api/auth/invites/web${location.search}` : null,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenHidden: false,
|
||||
revalidateIfStale: false,
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
tos: false,
|
||||
},
|
||||
validate: {
|
||||
username: (value) => (value.length >= 1 ? null : 'Username is required'),
|
||||
password: (value) => (value.length >= 1 ? null : 'Password is required'),
|
||||
},
|
||||
enhanceGetInputProps: ({ field }) => ({
|
||||
name: field,
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('/api/user');
|
||||
if (res.ok) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) return;
|
||||
|
||||
if (!config?.features.userRegistration && !code) {
|
||||
navigate('/auth/login');
|
||||
}
|
||||
}, [code, config]);
|
||||
|
||||
const onSubmit = async (values: typeof form.values) => {
|
||||
const { username, password, tos } = values;
|
||||
|
||||
if (tos === false && config!.website.tos) {
|
||||
form.setFieldError('tos', 'You must agree to the Terms of Service to continue');
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await fetchApi(
|
||||
'/api/auth/register',
|
||||
'POST',
|
||||
{
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
},
|
||||
{
|
||||
'x-zipline-client': JSON.stringify(getWebClient()),
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (ApiError.check(error, 1039)) {
|
||||
form.setFieldError('username', 'Username is taken');
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Failed to register',
|
||||
message: error.error,
|
||||
color: 'red',
|
||||
icon: <IconX size='1rem' />,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Complete!',
|
||||
message: `Your "${data?.user?.username}" account has been created.`,
|
||||
color: 'green',
|
||||
icon: <IconPlus size='1rem' />,
|
||||
});
|
||||
|
||||
mutate('/api/user');
|
||||
navigate('/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || configLoading) return <LoadingOverlay visible />;
|
||||
|
||||
if (!config || configError) {
|
||||
return (
|
||||
<GenericError
|
||||
title='Error loading configuration'
|
||||
message='Could not load server configuration...'
|
||||
details={configError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (code && inviteError) {
|
||||
if (inviteError) {
|
||||
showNotification({
|
||||
id: 'invalid-invite',
|
||||
message: 'Invalid or expired invite. Please try again later.',
|
||||
color: 'red',
|
||||
});
|
||||
|
||||
navigate('/auth/login');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inviteLoading) return <LoadingOverlay visible />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Center h='100vh'>
|
||||
{config.website.loginBackground && (
|
||||
<Image
|
||||
src={config.website.loginBackground}
|
||||
alt='Background'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
...(config.website.loginBackgroundBlur && { filter: 'blur(10px)' }),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Paper
|
||||
w='350px'
|
||||
p='xl'
|
||||
shadow='xl'
|
||||
withBorder
|
||||
style={{
|
||||
backgroundColor: config.website.loginBackground ? 'rgba(0, 0, 0, 0)' : undefined,
|
||||
backdropFilter: config.website.loginBackgroundBlur ? 'blur(35px)' : undefined,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%', overflowWrap: 'break-word' }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta='center'
|
||||
style={{
|
||||
whiteSpace: 'normal',
|
||||
fontSize: `clamp(20px, ${Math.max(50 - (config.website.title?.length ?? 0) / 2, 20)}px, 50px)`,
|
||||
}}
|
||||
>
|
||||
<b>{config.website.title ?? 'Zipline'}</b>
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
{invite && (
|
||||
<Text ta='center' size='sm' c='dimmed'>
|
||||
You’ve been invited to join <b>{config?.website?.title ?? 'Zipline'}</b>
|
||||
{invite.inviter && (
|
||||
<>
|
||||
{' '}
|
||||
by <b>{invite.inviter.username}</b>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack my='sm'>
|
||||
<TextInput
|
||||
size='md'
|
||||
placeholder='Enter your username...'
|
||||
autoComplete='username'
|
||||
styles={{
|
||||
input: {
|
||||
backgroundColor: config.website.loginBackground ? 'transparent' : undefined,
|
||||
},
|
||||
}}
|
||||
{...form.getInputProps('username', { withError: true })}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
size='md'
|
||||
placeholder='Enter your password...'
|
||||
autoComplete='new-password'
|
||||
styles={{
|
||||
input: {
|
||||
backgroundColor: config.website.loginBackground ? 'transparent' : undefined,
|
||||
},
|
||||
}}
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
|
||||
{config.website.tos && (
|
||||
<Checkbox
|
||||
label={
|
||||
<Text size='xs'>
|
||||
I agree to the{' '}
|
||||
<Link to='/auth/tos' target='_blank'>
|
||||
Terms of Service
|
||||
</Link>
|
||||
</Text>
|
||||
}
|
||||
required
|
||||
{...form.getInputProps('tos', { type: 'checkbox' })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size='md'
|
||||
fullWidth
|
||||
type='submit'
|
||||
variant={config.website.loginBackground ? 'outline' : 'filled'}
|
||||
leftSection={<IconUserPlus size='1rem' />}
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
<Stack my='xs'>
|
||||
<Divider label='or' />
|
||||
<Button
|
||||
component={Link}
|
||||
to='/auth/login'
|
||||
size='md'
|
||||
fullWidth
|
||||
variant='outline'
|
||||
leftSection={<IconLogin size='1rem' />}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
Component.displayName = 'Register';
|
||||