Compare commits

..

46 Commits

Author SHA1 Message Date
diced b5c83f92e3 fix(api): fix images not showing 2021-09-02 21:46:10 -07:00
diced 51b4d64a93 fix(build): fix some docker stuff 2021-09-02 21:27:12 -07:00
diced 62c9e0a22f feat(api): add support for invisible images 2021-09-02 21:00:16 -07:00
diced 3daac34d3e feat(api): add support for invisible images 2021-09-02 20:59:30 -07:00
dependabot[bot] d80d5d1632 build(deps): bump next from 11.1.0 to 11.1.1 (#95)
Bumps [next](https://github.com/vercel/next.js) from 11.1.0 to 11.1.1.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v11.1.0...v11.1.1)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-02 06:51:13 -07:00
diced 912f716362 feat(api): add support for zws images 2021-08-30 20:56:34 -07:00
diced 16ecdf41af feat(components): added sharex config generator (#93) 2021-08-30 13:58:06 -07:00
Nguyen Thanh Quang f0bb6b08fa feat(components): added sharex config generator (#93) 2021-08-30 07:59:08 -07:00
Nguyen Thanh Quang efb4e2ce9a feat(sharex): added sharex config generator 2021-08-30 20:19:12 +07:00
diced 03238d10bf fix(config): notify which vars are missing 2021-08-28 21:41:04 -07:00
diced e71590b9fb fix(config): new opts: admin_limit, user_limit, disabled_extensions (#68) 2021-08-28 21:02:04 -07:00
diced 4728f1cc46 fix(api): accidently sending images as a part of the user object 2021-08-28 19:37:38 -07:00
diced 794778dee2 feat(config): database section removed 2021-08-28 11:32:09 -07:00
Nguyen Thanh Quang b5e882f07e feat(themes): added dracula theme (#92)
* added dracula theme

* change border color from white

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
2021-08-27 15:33:32 -07:00
dicedtomato e7c58a4847 licence 2021-08-27 15:24:37 -07:00
dicedtomato bdb44db25e security policy 2021-08-27 15:23:28 -07:00
diced e8b82ffe62 feat(api): image favoriting (#67) 2021-08-27 13:48:22 -07:00
diced 53c53c009e fix(components): white text on links when light theme 2021-08-26 21:41:30 -07:00
Nguyen Thanh Quang 7e8cda4605 feat(components): ayu_mirage, ayu_light, nord, polar themes (#90) 2021-08-26 19:44:04 -07:00
diced dfa0419a0a fix(api): fix many bugs 2021-08-26 15:18:14 -07:00
diced aeb2638d1e feat: v3.2.0 - custom themes & curated themes 2021-08-26 12:33:18 -07:00
diced c5cef56e2a feat: v3.2.0 - custom themes & curated themes 2021-08-26 12:32:51 -07:00
Nguyen Thanh Quang b9c9d98252 removed redundant code (#86) 2021-08-25 19:41:49 -07:00
Nguyen Thanh Quang 30083b6705 fixed the menu doesn't close when clicking outside (#87) 2021-08-25 19:41:32 -07:00
diced 47db6cf1bd fix(build): prisma not being copied over 2021-08-25 15:16:39 -07:00
diced f929f6ad7d fix(build): schema.shared.prisma -> schema.prisma 2021-08-25 10:10:23 -07:00
diced 7e16e0f30c fix(build): add migrations to gitignore 2021-08-25 10:05:35 -07:00
diced b2be4e51cc fix(build): remove schema from gitignore 2021-08-25 10:01:57 -07:00
diced 2c871be8c5 feat(prisma): remove multi-db support in favor of psql \w easier setup 2021-08-25 09:58:48 -07:00
diced 8c03e74979 fix(build): fix 2021-08-24 10:27:20 -07:00
diced d5c0355fd4 fix(build): maybe fix 2021-08-24 10:20:47 -07:00
diced 386cad0474 feat(components): copy url to clipboard when uploading image 2021-08-24 10:17:04 -07:00
Nguyen Thanh Quang 474024ea55 Fixed error/url not showing properly (#85)
* Fixed error not showing.

* Update Upload.tsx
2021-08-23 08:39:31 -07:00
diced dacf13e46d fix(docker): docker image 2021-08-21 14:57:54 -07:00
dependabot[bot] f37b4bb2ee build(deps): bump next from 11.0.0 to 11.1.0 (#83)
* build(deps): bump next from 11.0.0 to 11.1.0

Bumps [next](https://github.com/vercel/next.js) from 11.0.0 to 11.1.0.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v11.0.0...v11.1.0)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(deps): nextjs 11 -> 11.1

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: diced <pranaco2@gmail.com>
2021-08-13 12:57:05 -07:00
diced 034398e9fb fix(api): no admin user creat 2021-08-07 20:38:45 -07:00
diced 2c605cb176 fix(api): no admin user creat 2021-08-07 20:38:15 -07:00
dicedtomato 9a6673fe6d Update README.md 2021-07-22 16:23:42 -07:00
diced 6733c9adba fix(pages): fix embed route not loading images 2021-06-23 21:44:01 -07:00
diced 9d3443ceac fix(server): add mimetype for non db entries 2021-06-23 21:34:38 -07:00
diced d628424b35 fix(pages): fix average size of uploads 2021-06-23 14:24:42 -07:00
diced dab444040e fix(api): fix broken uploads 2021-06-23 14:21:34 -07:00
diced ecef854d23 fix(pages): stats being null 2021-06-23 12:18:10 -07:00
diced 166087e33c fix: make migrations use readConfig 2021-06-23 11:47:05 -07:00
diced e9e30c4c46 fix: change compose to use dockerhub image 2021-06-23 11:40:52 -07:00
diced fd400aa850 Release 3.0.0 2021-06-23 11:20:20 -07:00
522 changed files with 10083 additions and 60295 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"presets": [
"next/babel"
],
"plugins": [
[
"babel-plugin-transform-imports",
{
"@material-ui/core": {
"transform": "@material-ui/core/${member}",
"preventFullImport": true
},
"@material-ui/icons": {
"transform": "@material-ui/icons/${member}",
"preventFullImport": true
}
}
]
]
}
-7
View File
@@ -1,7 +0,0 @@
.github
build
node_modules
uploads*
.env
.eslintcache
src/prisma
-1
View File
@@ -1 +0,0 @@
use flake . --no-pure-eval
+24
View File
@@ -0,0 +1,24 @@
module.exports = {
'extends': ['next', 'next/core-web-vitals'],
'rules': {
'indent': ['error', 2],
'linebreak-style': ['error', 'unix'],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'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
View File
@@ -1 +0,0 @@
github: diced
-70
View File
@@ -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 Ziplines functionality.
-11
View File
@@ -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!
+21 -37
View File
@@ -1,50 +1,34 @@
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
- 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
path: node_modules
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
- name: Create mock config
run: echo -e "[uploader]\nroute = '/u'\nembed_route = '/a'\nlength = 6\ndirectory = './uploads'" > config.toml
- 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
-112
View File
@@ -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
+29 -89
View File
@@ -1,104 +1,44 @@
name: 'Push Docker Images'
name: 'CD: Push Docker Images'
on:
push:
branches: [v4, trunk]
branches: [ trunk ]
paths:
- 'src/**'
- 'server/**'
- 'prisma/**'
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
-99
View File
@@ -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
+12 -26
View File
@@ -5,22 +5,19 @@
/.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 +25,15 @@ 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/
data.db*
+1
View File
@@ -0,0 +1 @@
_
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit $1
-1
View File
@@ -1 +0,0 @@
pnpm-lock.yaml
+20 -53
View File
@@ -1,64 +1,31 @@
FROM node:22-alpine3.21 AS base
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
FROM node:16-alpine3.11 AS builder
WORKDIR /build
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 "[uploader]\nroute = '/u'\nembed_route = '/a'\nlength = 6\ndirectory = './uploads'" > 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"]
+3
View File
@@ -0,0 +1,3 @@
prisma/migrations
node_modules
.next
+1 -1
View File
@@ -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
+18 -326
View File
@@ -1,333 +1,25 @@
<div align="center">
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
<p align="center"><img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/></p>
The next generation ShareX / File upload server
![Version](https://img.shields.io/github/package-json/v/diced/zipline)
![LICENCE](https://img.shields.io/github/license/diced/zipline)
[![Discord](https://img.shields.io/discord/729771078196527176)](https://discord.gg/EAhCRfGxCF)
![Stars](https://img.shields.io/github/stars/diced/zipline)
![GitHub repo size](https://img.shields.io/github/repo-size/diced/zipline)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/diced/zipline/trunk)
<br>
![Stars](https://img.shields.io/github/stars/diced/zipline?logo=github&style=for-the-badge)
![Version](https://img.shields.io/github/package-json/v/diced/zipline?logo=git&logoColor=white&style=for-the-badge)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/diced/zipline/trunk?logo=git&logoColor=white&style=for-the-badge)
[![Discord](https://img.shields.io/discord/729771078196527176?color=%23777ed3&label=discord&logo=discord&logoColor=white&style=for-the-badge)](https://discord.gg/EAhCRfGxCF)
# Zipline
![Build](https://img.shields.io/github/actions/workflow/status/diced/zipline/build.yml?logo=github&style=for-the-badge&branch=trunk)
Fast & lightweight file uploading.
Documentation: [zipline.diced.sh](https://zipline.diced.sh)
# Features
</div>
- Configurable
- Fast
- Built with Next.js & React
- Token protected uploading
- Easy setup instructions on [docs](https://zipline.diced.me) (One command install `docker-compose up`)
## Features
# Installing
- 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!
# Usage
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).
[See how to install here](https://zipline.diced.me/docs/getting-started)
+2 -6
View File
@@ -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.
-212
View File
@@ -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"
}
]
+54
View File
@@ -0,0 +1,54 @@
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'
],
],
},
};
+15
View File
@@ -0,0 +1,15 @@
[core]
secure = true
secret = 'some secret'
host = '0.0.0.0'
port = 3000
database_url = 'postgres://postgres:postgres@postgres/postgres'
[uploader]
route = '/u'
embed_route = '/a'
length = 6
directory = './uploads'
user_limit = 104900000 # 100mb
admin_limit = 104900000 # 100mb
disabled_extentions = ['jpg']
-36
View File
@@ -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:
+30 -30
View File
@@ -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:
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env sh
set -e
cd ${ZIPLINE_ROOT:-/zipline}
exec node --enable-source-maps build/server
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env sh
set -e
cd ${ZIPLINE_ROOT:-/zipline}
exec node --enable-source-maps build/ctl "$@"
-107
View File
@@ -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' },
},
},
);
Generated
-254
View File
@@ -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
}
-128
View File
@@ -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;
};
};
};
};
}
-1385
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
reactStrictMode: true,
};
+53 -122
View File
@@ -1,130 +1,61 @@
{
"name": "zipline",
"private": true,
"license": "MIT",
"version": "4.4.2",
"name": "zip3",
"version": "3.2.1",
"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",
"ts-node": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only",
"semantic-release": "semantic-release"
},
"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",
"@material-ui/core": "^5.0.0-alpha.37",
"@material-ui/data-grid": "^4.0.0-alpha.32",
"@material-ui/icons": "^5.0.0-alpha.37",
"@material-ui/styles": "^5.0.0-alpha.35",
"@prisma/client": "^2.30.3",
"@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": "11.1.1",
"prisma": "^2.30.3",
"react": "17.0.2",
"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",
"babel-plugin-transform-imports": "^2.0.0",
"eslint": "7.28.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/workflow-testing.git"
}
}
-11198
View File
File diff suppressed because it is too large Load Diff
-10
View File
@@ -1,10 +0,0 @@
ignoredBuiltDependencies:
- unrs-resolver
onlyBuiltDependencies:
- '@parcel/watcher'
- '@prisma/client'
- '@prisma/engines'
- argon2
- esbuild
- prisma
- sharp
-14
View File
@@ -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',
},
},
},
};
-6
View File
@@ -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;
@@ -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;
+2 -2
View File
@@ -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"
+53 -381
View File
@@ -1,403 +1,75 @@
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")
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
model InvisibleImage {
id Int @id @default(autoincrement())
invis String @unique
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?
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?
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])
id Int @id @default(autoincrement())
to 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
model InvisibleUrl {
id Int
url Url @relation(fields: [id], references: [id])
data Json
}
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
invis String @unique
}
+31
View File
@@ -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();
});
File diff suppressed because it is too large Load Diff
-1750
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

+8
View File
@@ -0,0 +1,8 @@
module.exports = {
branches: ['trunk'],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/github',
'@semantic-release/changelog'
]
};
-24
View File
@@ -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',
),
);
+13
View File
@@ -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);
}
};
-55
View File
@@ -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}.`);
}
-3
View File
@@ -1,3 +0,0 @@
import { step } from '.';
export const lintStep = step('lint', 'eslint .');
+35
View File
@@ -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();
})();
+78
View File
@@ -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'
};
-110
View File
@@ -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),
);
+26
View File
@@ -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));
});
};
-9
View File
@@ -1,9 +0,0 @@
import { run, step } from '.';
import { lintStep } from './lint';
run(
'validate',
lintStep,
step('format', 'prettier --write --ignore-path .gitignore .'),
);
+137
View File
@@ -0,0 +1,137 @@
const next = require('next');
const { createServer } = require('http');
const { stat, mkdir } = require('fs/promises');
const { execSync } = require('child_process');
const { extname } = require('path');
const { red, green, bold } = require('colorette');
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');
Logger.get('server').info('starting zipline 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(`${status === 200 ? bold(green(status)) : bold(red(status))}: ${url}`);
}
function shouldUseYarn() {
try {
execSync('yarnpkg --version', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
(async () => {
try {
const config = readConfig();
await validateConfig(config);
const data = await prismaRun(config.core.database_url, ['migrate', 'status'], true);
if (data.includes('Following migration 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');
}
process.env.DATABASE_URL = config.core.database_url;
await stat('./.next');
await mkdir(config.uploader.directory, { recursive: true });
const app = next({
dir: '.',
dev,
quiet: dev
}, config.core.port, config.core.host);
await app.prepare();
const handle = app.getRequestHandler();
const prisma = new PrismaClient();
const srv = createServer(async (req, res) => {
if (req.url.startsWith('/raw')) {
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 {
if (image) {
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 {
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 {
handle(req, res);
}
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}`);
});
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');
} 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);
}
}
})();
+11
View File
@@ -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;
}
};
+45
View File
@@ -0,0 +1,45 @@
const Logger = require('../src/lib/logger');
function dot(str, obj) {
return str.split('.').reduce((a,b) => a[b], obj);
}
const path = (path, type) => ({ path, type });
module.exports = async config => {
const paths = [
path('core.secure', 'boolean'),
path('core.secret', 'string'),
path('core.host', 'string'),
path('core.port', 'number'),
path('core.database_url', 'string'),
path('uploader.route', 'string'),
path('uploader.length', 'number'),
path('uploader.directory', 'string'),
path('uploader.admin_limit', 'number'),
path('uploader.user_limit', 'number'),
path('uploader.disabled_extentions', 'object'),
];
let errors = 0;
for (let i = 0, L = paths.length; i !== L; ++i) {
const path = paths[i];
const value = dot(path.path, config);
if (value === undefined) {
Logger.get('config').error(`there was no ${path.path} in config which was required`);
++errors;
}
const type = typeof value;
if (value !== undefined && type !== path.type) {
Logger.get('config').error(`expected ${path.type} on ${path.path}, but got ${type}`);
++errors;
}
}
if (errors !== 0) {
Logger.get('config').error(`exiting due to ${errors} errors`);
process.exit(1);
}
};
-69
View File
@@ -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' }}
/>
);
}
-38
View File
@@ -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>
);
}
-11
View File
@@ -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' }}
/>
);
}
-14
View File
@@ -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>
-18
View File
@@ -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>,
);
-29
View File
@@ -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>
);
}
-237
View File
@@ -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>
</>
);
}
-295
View File
@@ -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'>
Youve 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';
-254
View File
@@ -1,254 +0,0 @@
import { type Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { useTitle } from '@/lib/hooks/useTitle';
import {
Anchor,
Button,
Code,
Group,
Paper,
PasswordInput,
SimpleGrid,
Stack,
Stepper,
Text,
TextInput,
Title,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import { IconArrowBackUp, IconArrowForwardUp, IconCheck, IconX } from '@tabler/icons-react';
import { useState } from 'react';
import { redirect, useNavigate } from 'react-router-dom';
import { mutate } from 'swr';
function LinkToDoc({ href, title, children }: { href: string; title: string; children: React.ReactNode }) {
return (
<Text>
<Anchor href={href} target='_blank' rel='noopener noreferrer'>
{title}
</Anchor>{' '}
{children}
</Text>
);
}
export async function loader() {
const res = await fetch('/api/server/public');
if (!res.ok) {
throw new Response('Failed to fetch server settings', { status: res.status });
}
const data = await res.json();
if (!data.firstSetup) return redirect('/auth/login');
return {};
}
export function Component() {
useTitle('Setup');
const navigate = useNavigate();
const [active, setActive] = useState(0);
const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current));
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
const [loading, setLoading] = useState(false);
const form = useForm({
initialValues: {
username: '',
password: '',
},
validate: {
username: (value) => (value.length >= 1 ? null : 'Username is required'),
password: (value) => (value.length >= 1 ? null : 'Password is required'),
},
enhanceGetInputProps: ({ field }) => ({
name: field,
}),
});
const onSubmit = async (values: typeof form.values) => {
setLoading(true);
const { error } = await fetchApi('/api/setup', 'POST', {
username: values.username,
password: values.password,
});
if (error) {
notifications.show({
title: 'Error',
message: error.error,
color: 'red',
icon: <IconX size='1rem' />,
});
setLoading(false);
setActive(2);
} else {
notifications.show({
title: 'Setup complete!',
message: 'Logging in to new user...',
color: 'green',
loading: true,
});
const { data, error } = await fetchApi<Response['/api/auth/login']>('/api/auth/login', 'POST', {
username: values.username,
password: values.password,
});
if (error) {
notifications.show({
title: 'Error',
message: error.error,
color: 'red',
icon: <IconX size='1rem' />,
});
setLoading(false);
setActive(2);
} else {
mutate('/api/user', data as Response['/api/user']);
navigate('/dashboard');
}
}
};
return (
<>
<Paper withBorder p='xs' m='sm'>
<Stepper active={active} onStepClick={setActive} m='md'>
<Stepper.Step label='Welcome!' description='Setup Zipline'>
<Title>Welcome to Zipline!</Title>
<SimpleGrid spacing='md' cols={{ base: 1, sm: 1 }}>
<Paper withBorder p='sm' my='sm' h='100%'>
<Title order={2}>Documentation</Title>
<Text>Here are a couple of useful documentation links to get you started with Zipline:</Text>
<Stack mt='xs'>
<LinkToDoc href='https://zipline.diced.sh/docs/config' title='Configuration'>
Configuring Zipline to your needs
</LinkToDoc>
<LinkToDoc href='https://zipline.diced.sh/docs/migrate' title='Migrate from v3 to v4'>
Upgrading from a previous version of Zipline
</LinkToDoc>
</Stack>
</Paper>
<Paper withBorder p='sm' my='sm' h='100%'>
<Title order={2}>Configuration</Title>
<Text>
Most of Zipline&apos;s configuration is now managed through the dashboard. Once you login as
a super-admin, you can click on your username in the top right corner and select
&quot;Server Settings&quot; to configure your instance. The only exception to this is a few
sensitive environment variables that must be set in order for Zipline to run. To change
this, depending on the setup, you can either edit the <Code>.env</Code> or{' '}
<Code>docker-compose.yml</Code> file.
</Text>
<Text>
To see all of the available environment variables, please refer to the documentation{' '}
<Anchor
href='https://zipline.diced.sh/docs/config'
target='_blank'
rel='noopener noreferrer'
>
here.
</Anchor>
</Text>
</Paper>
</SimpleGrid>
<Button
mt='xl'
fullWidth
rightSection={<IconArrowForwardUp size='1.25rem' />}
size='lg'
variant='default'
onClick={nextStep}
>
Continue
</Button>
</Stepper.Step>
<Stepper.Step label='Create user' description='Create a super-admin account'>
<Stack gap='lg'>
<Title order={2}>Create your super-admin account</Title>
<TextInput
label='Username'
placeholder='Enter a username...'
autoComplete='username'
{...form.getInputProps('username')}
/>
<PasswordInput
label='Password'
placeholder='Enter a password...'
autoComplete='new-password'
{...form.getInputProps('password')}
/>
</Stack>
<Group justify='space-between' my='lg'>
<Button
leftSection={<IconArrowBackUp size='1.25rem' />}
size='lg'
variant='default'
onClick={prevStep}
>
Back
</Button>
<Button
rightSection={<IconArrowForwardUp size='1.25rem' />}
size='lg'
variant='default'
onClick={nextStep}
disabled={!form.isValid()}
>
Continue
</Button>
</Group>
</Stepper.Step>
<Stepper.Completed>
<Title order={2}>Setup complete!</Title>
<Text>
Clicking &quot;Finish&quot; below will create your super-admin account and log you in. You will
be redirected to the dashboard shortly after that.
</Text>
<Group justify='space-between' my='lg'>
<Button
leftSection={<IconArrowBackUp size='1.25rem' />}
size='lg'
variant='default'
onClick={prevStep}
loading={loading}
>
Back
</Button>
<Button
rightSection={<IconCheck size='1.25rem' />}
size='lg'
variant='default'
loading={loading}
onClick={() => form.onSubmit(onSubmit)()}
>
Finish
</Button>
</Group>
</Stepper.Completed>
</Stepper>
</Paper>
</>
);
}
Component.displayName = 'Setup';
-41
View File
@@ -1,41 +0,0 @@
import Markdown from '@/components/render/Markdown';
import { Response } from '@/lib/api/response';
import { Container, LoadingOverlay } from '@mantine/core';
import useSWR from 'swr';
import GenericError from '../../error/GenericError';
import { useTitle } from '@/lib/hooks/useTitle';
export function Component() {
useTitle('Terms of Service');
const {
data: config,
error,
isLoading,
} = useSWR<Response['/api/server/public']>('/api/server/public', {
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenHidden: false,
revalidateIfStale: false,
});
if (isLoading) return <LoadingOverlay visible />;
if (error) {
return (
<GenericError
title='Error loading TOS'
message='Could not load Terms of Service file...'
details={error}
/>
);
}
return (
<Container my='lg'>
<Markdown md={config?.tos || ''} />
</Container>
);
}
Component.displayName = 'Tos';
@@ -1,10 +0,0 @@
import DashboardServerActions from '@/components/pages/serverActions';
import { useTitle } from '@/lib/hooks/useTitle';
export function Component() {
useTitle('Server Actions');
return <DashboardServerActions />;
}
Component.displayName = 'Dashboard/Admin/Actions';

Some files were not shown because too many files have changed in this diff Show More