Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 353122c169 | |||
| cea5092fd6 | |||
| 3d4625f531 | |||
| f5ab288bb3 | |||
| 41565b3a62 | |||
| ef7fbaf1dc | |||
| 87cf861648 | |||
| d414c85efd | |||
| 5b24a8e415 | |||
| 1d348db4dd | |||
| ececc3ab0e | |||
| a71bde9730 | |||
| 3417a84789 | |||
| 080c92a968 | |||
| 94b0220db8 | |||
| dc29ad42f5 | |||
| 4cce4718a2 | |||
| 8fe1de013f | |||
| 7a6766e9cc | |||
| 5537d28849 | |||
| a420d830e3 | |||
| 43657a8cb0 | |||
| 9f6e7717df | |||
| da1bfe5567 | |||
| a88fa14d5b | |||
| 58f934ba30 | |||
| bfaca477a8 | |||
| 278aa66e00 | |||
| 2c179c8668 | |||
| f3aba4c17d | |||
| bf4e02d794 | |||
| 5869e1d753 | |||
| 13406a2f67 | |||
| cc99001697 | |||
| 79146cb0e2 | |||
| f5b13a1738 | |||
| c15f20e97d | |||
| 26dbbc239d | |||
| 1c7d64662b | |||
| 6f92938b22 | |||
| 6790397893 | |||
| 1dd7f0267e | |||
| e82879a780 | |||
| a72bbd1373 | |||
| 27d557bf4e | |||
| a9aba8f933 | |||
| 6bddd5be1e | |||
| a13fea369c | |||
| a4d73fcb66 | |||
| a869c3f6ef | |||
| f0a846a9e1 | |||
| 7d037d89bf | |||
| 2c6ca79efa | |||
| 72c45ce55a | |||
| 17943a7fb9 | |||
| 472bf6076a | |||
| d7c7c9da18 | |||
| e6efff28dd | |||
| 194472683b | |||
| 81ffadbb3e | |||
| 026b9b0880 | |||
| b4d0379bde | |||
| a038fc1603 | |||
| fafc6762c6 | |||
| d2dd7f6b0c | |||
| 7f07a14ae9 | |||
| 8463633a72 | |||
| 360d0c1b7a | |||
| 7ad85e99b3 | |||
| d9ea43d71a | |||
| 4a3266c683 | |||
| 7d80a677fc | |||
| 56f5927bc9 | |||
| ff5b1252eb | |||
| 593792023c | |||
| ed3ae6433a | |||
| eba64828cc | |||
| e1c414b6bf | |||
| a30e5a04c6 | |||
| 610e76ee64 | |||
| 34616489b7 | |||
| 779be3edaf | |||
| d51aa0f198 | |||
| fe9892b959 | |||
| 09319dfd21 | |||
| 43edf93eb8 | |||
| 382e99113c | |||
| c26c72d36e | |||
| f5a4743057 | |||
| e908d10472 | |||
| 8102024f00 | |||
| 880dd6534d | |||
| fadb07102b | |||
| bd8e14a868 | |||
| 590bf7e890 | |||
| 7b4c8152d1 | |||
| f10cfa59a3 | |||
| 1201a80455 | |||
| 61a299bcd1 | |||
| bc2efcc07b | |||
| df2f39f876 | |||
| a2e4e1489a | |||
| 5f6b26adbf | |||
| dc6f3001f3 | |||
| edb9033aff | |||
| b0e105d46d | |||
| 16b993159d | |||
| fa6e7e9ca9 | |||
| 75f8bc38c8 | |||
| a0dec9c2f8 | |||
| ca97971f04 | |||
| 0a8d85677f | |||
| d537a00e54 | |||
| 89eca95dfe | |||
| 961b2721c8 | |||
| 239dadaefd | |||
| 2921b3c908 | |||
| e4ea84e3d6 | |||
| facc548ab1 | |||
| 56302c66af | |||
| 84ebc13acd | |||
| 5efe492db0 | |||
| dda4d3a742 | |||
| c166572c68 | |||
| 38b5f1ab9a | |||
| d96b633fae | |||
| 5875108936 | |||
| 7418339676 | |||
| 12e6064414 | |||
| be2764d587 | |||
| d9013f2280 | |||
| 587cdbc1a3 | |||
| 1671296f76 | |||
| bc54e00a14 | |||
| e028afeb3f | |||
| 21bc4eca48 | |||
| 698e687fbc | |||
| 801eddd9a8 | |||
| c1683d8367 | |||
| 91c37007a9 | |||
| 349c4d0119 | |||
| 10b1d018f3 |
@@ -1,7 +1,3 @@
|
||||
.github
|
||||
build
|
||||
node_modules
|
||||
uploads*
|
||||
.env
|
||||
.eslintcache
|
||||
src/prisma
|
||||
node_modules/
|
||||
.next/
|
||||
uploads/
|
||||
@@ -0,0 +1,3 @@
|
||||
.next
|
||||
dist
|
||||
node_modules
|
||||
@@ -0,0 +1,34 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es2021: true,
|
||||
node: true
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
ecmaVersion: 12,
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ['react', '@typescript-eslint'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'react/prop-types': 'off',
|
||||
indent: ['error', 2],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: ['error', 'single'],
|
||||
semi: ['error', 'always'],
|
||||
'comma-dangle': ['error', 'never']
|
||||
}
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
github: diced
|
||||
@@ -1,70 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Report a reproducible bug in Zipline
|
||||
title: 'Bug: [short description of the issue]'
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
Describe in detail what you were doing and what happened.
|
||||
Please include screenshots, logs, or error messages if possible, as they help diagnose the issue faster.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: runtime-type
|
||||
attributes:
|
||||
label: How is Zipline being run?
|
||||
description:
|
||||
options:
|
||||
- On docker (docker, docker compose, etc.)
|
||||
- Built from source (running it through `pnpm start` or `node`, etc.)
|
||||
- Other (please specify in the "Zipline Version" section)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: runtime-version
|
||||
attributes:
|
||||
label: Zipline Version
|
||||
description: |
|
||||
Provide the version of Zipline you are using:
|
||||
- If version checking is enabled (it is by default): paste the response from `http://<domain>/api/version`
|
||||
- If using docker (and can't do the above): specify the tag you are using (`latest`, `trunk`, or a tag digest)
|
||||
- A simple version number (e.g. "4.2.1") may also suffice
|
||||
placeholder: '4.2.1'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: If applicable, what browsers are you seeing this issue on?
|
||||
multiple: true
|
||||
options:
|
||||
- Chromium based (Chrome, Brave, Edge, Opera, etc.)
|
||||
- Firefox based (Firefox, Zen Browser, Waterfox, etc.)
|
||||
- Safari (On macOS and/or iOS)
|
||||
- Chromium based on Android/iOS
|
||||
- Firefox based on Android/iOS
|
||||
- Other (Please specify in the "Steps to Reproduce" section)
|
||||
|
||||
- type: textarea
|
||||
id: zipline-logs
|
||||
attributes:
|
||||
label: Relevant Logs
|
||||
description: |
|
||||
Paste any relevant logs from Zipline or the browser (if applicable).
|
||||
If logs don't look useful, you can enable debug mode by setting the environment variable `DEBUG=zipline` when starting Zipline.
|
||||
Then reproduce the issue and copy the logs here.
|
||||
**Note:** Debug logs may contain sensitive information.
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
Please list the exact steps required to reproduce the issue.
|
||||
Include any relevant configuration options, settings, or external services that may affect Zipline’s functionality.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: Loading forever [BUG]
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information if desktop):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser: [e.g. chrome, safari, firefox]
|
||||
- Zipline Version: [e.g. 22]
|
||||
|
||||
**Mobile (please complete the following information if mobile):**
|
||||
- Device: [e.g. iPhone12]
|
||||
- OS: [e.g. iOS14, Android 11]
|
||||
- Browser: [e.g. chrome, firefox]
|
||||
- Zipline Version: [e.g. 22]
|
||||
|
||||
**Browser Logs (for desktop browsers)**
|
||||
```
|
||||
logs
|
||||
```
|
||||
@@ -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!
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,50 +0,0 @@
|
||||
name: 'Build'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [v4, trunk]
|
||||
pull_request:
|
||||
branches: [v4, trunk]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
node: [22.x, 24.x]
|
||||
arch: [amd64, arm64]
|
||||
runs-on: ubuntu-24.04${{ matrix.arch == 'arm64' && '-arm' || '' }}
|
||||
|
||||
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
|
||||
@@ -1,112 +0,0 @@
|
||||
name: 'Push Release Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v4.*.*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
|
||||
name: push release
|
||||
runs-on: ubuntu-24.04${{ matrix.arch == 'arm64' && '-arm' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get commit sha
|
||||
id: sha
|
||||
run: |
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
build-args: |
|
||||
ZIPLINE_GIT_SHA=${{ steps.sha.outputs.short_sha }}
|
||||
tags: |
|
||||
ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-${{ matrix.arch }}
|
||||
ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-${{ steps.sha.outputs.short_sha }}-${{ matrix.arch }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-${{ matrix.arch }}
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-${{ steps.sha.outputs.short_sha }}-${{ matrix.arch }}
|
||||
|
||||
amend-builds:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: push
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get commit sha
|
||||
id: sha
|
||||
run: |
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: pull images
|
||||
run: |
|
||||
docker pull --platform=linux/amd64 ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-amd64
|
||||
docker pull --platform=linux/arm64 ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-arm64
|
||||
docker pull --platform=linux/amd64 ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-amd64
|
||||
docker pull --platform=linux/arm64 ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-arm64
|
||||
|
||||
- name: create manifests
|
||||
run: |
|
||||
docker manifest create ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }} \
|
||||
--amend ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-amd64 \
|
||||
--amend ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-arm64 && \
|
||||
docker manifest create ghcr.io/diced/zipline:latest \
|
||||
--amend ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-amd64 \
|
||||
--amend ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}-arm64 && \
|
||||
docker manifest create ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }} \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-amd64 \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-arm64 && \
|
||||
docker manifest create ${{ secrets.DOCKERHUB_USERNAME }}/zipline:latest \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-amd64 \
|
||||
--amend ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}-arm64
|
||||
|
||||
- name: push manifests
|
||||
run: |
|
||||
docker manifest push ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}
|
||||
docker manifest push ghcr.io/diced/zipline:latest
|
||||
docker manifest push ${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}
|
||||
docker manifest push ${{ secrets.DOCKERHUB_USERNAME }}/zipline:latest
|
||||
@@ -1,104 +0,0 @@
|
||||
name: 'Push Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [v4, trunk]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
|
||||
name: push
|
||||
runs-on: ubuntu-24.04${{ matrix.arch == 'arm64' && '-arm' || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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: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
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
|
||||
- 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 }}
|
||||
@@ -0,0 +1,28 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI for Next
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [next]
|
||||
pull_request:
|
||||
branches: [next]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [15.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm i -g yarn
|
||||
- run: yarn
|
||||
- run: yarn build
|
||||
@@ -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
|
||||
@@ -1,53 +1,29 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
build/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.idea
|
||||
.vscode
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*
|
||||
# Next.js
|
||||
/.next
|
||||
node_modules
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
# Typescript
|
||||
dist
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
# Zipline
|
||||
Zipline.toml
|
||||
|
||||
# eslint
|
||||
.eslintcache
|
||||
|
||||
# nix dev env
|
||||
!.envrc
|
||||
.direnv
|
||||
.devenv
|
||||
|
||||
# zipline
|
||||
uploads*/
|
||||
*.crt
|
||||
*.key
|
||||
src/prisma
|
||||
.memory.log*
|
||||
openapi.json
|
||||
# dev
|
||||
uploads/
|
||||
testImage.*
|
||||
# this is ignored because setup.js can generate a docker-compose.yml
|
||||
docker-compose.yml
|
||||
@@ -1 +1,4 @@
|
||||
pnpm-lock.yaml
|
||||
.next
|
||||
dist
|
||||
node_modules
|
||||
Zipline.toml
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/prettierrc",
|
||||
"jsxSingleQuote": true,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid",
|
||||
"trailingComma": "none",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -1,64 +1,12 @@
|
||||
FROM node:22-alpine3.21 AS base
|
||||
FROM node:14
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
WORKDIR /opt/zipline
|
||||
|
||||
RUN corepack enable
|
||||
COPY . /opt/zipline
|
||||
|
||||
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
|
||||
|
||||
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 scripts ./scripts
|
||||
|
||||
RUN ZIPLINE_BUILD=true pnpm run build
|
||||
|
||||
FROM base
|
||||
|
||||
COPY --from=deps /zipline/node_modules ./node_modules
|
||||
|
||||
COPY --from=builder /zipline/build ./build
|
||||
|
||||
COPY --from=builder /zipline/mimes.json ./mimes.json
|
||||
COPY --from=builder /zipline/code.json ./code.json
|
||||
|
||||
RUN pnpm prisma generate
|
||||
|
||||
# clean
|
||||
RUN rm -rf /tmp/* /root/*
|
||||
RUN npm i
|
||||
RUN npm run build
|
||||
|
||||
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"]
|
||||
EXPOSE 8000
|
||||
CMD ["node", "dist"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 dicedtomato
|
||||
Copyright (c) 2020 dicedtomato
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,333 +1,36 @@
|
||||
<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/ZiplineProject/zipline/next/public/zipline_small.png"/></p>
|
||||
|
||||

|
||||

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

|
||||

|
||||

|
||||
<br>
|
||||

|
||||

|
||||

|
||||
|
||||
# Zipline
|
||||
The best and only **React + Next.js** ShareX / File Uploader you would ever want.
|
||||
|
||||
# Comparison
|
||||
Wondering how Zipline compares to other popular uploaders? We have done some benchmarking on other popular upload servers, see how Zipline compares.
|
||||
|
||||
| Uploader | Average ms (3 batches/1.5k files) |
|
||||
|-|-|
|
||||
| **[Zipline](https://github.com/dicedtomatoreal/zipline)** | **61 ms** |
|
||||
| [ShareX-Upload-Server](https://github.com/TannerReynolds/ShareX-Upload-Server) | 86 ms |
|
||||
|
||||
*Note: there were 3 batches of 1.5k requests, the average ms of each was averaged again*<br>
|
||||
*Note 2: results will vary because its very dependent on the server, location, and your internet (these tests were run on the same machine with local dbs)*
|
||||
|
||||
# Features
|
||||
- Configurable
|
||||
- Fast (API)
|
||||
- Built with Next.js & React
|
||||
- Support for **multible database types** (*literally the only one that supports multiple dbs*, mongo soon)
|
||||
|
||||
# Installing
|
||||
[See how to install here](https://zipline.diced.wtf/docs/)
|
||||
|
||||
The next generation ShareX / File upload server
|
||||
|
||||

|
||||

|
||||

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

|
||||
|
||||
Documentation: [zipline.diced.sh](https://zipline.diced.sh)
|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
- 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).
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 4.4.x | :white_check_mark: |
|
||||
| < 3 | :x: |
|
||||
| < 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)
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -1,36 +0,0 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres2
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'pg_isready', '-U', 'postgres']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- '3000:3000'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:postgres@postgres/postgres2
|
||||
- CORE_HOSTNAME=0.0.0.0
|
||||
depends_on:
|
||||
- postgres
|
||||
volumes:
|
||||
- './uploads:/zipline/uploads'
|
||||
- './public:/zipline/public'
|
||||
- './themes:/zipline/themes'
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
@@ -1,42 +0,0 @@
|
||||
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
|
||||
healthcheck:
|
||||
test: ['CMD', 'pg_isready', '-U', 'zipline']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
zipline:
|
||||
image: ghcr.io/diced/zipline:latest
|
||||
restart: unless-stopped
|
||||
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://0.0.0.0:3000/api/healthcheck']
|
||||
interval: 15s
|
||||
timeout: 2s
|
||||
retries: 2
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
cd ${ZIPLINE_ROOT:-/zipline}
|
||||
exec node --enable-source-maps build/server
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
cd ${ZIPLINE_ROOT:-/zipline}
|
||||
exec node --enable-source-maps build/ctl "$@"
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
|
||||
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
|
||||
import prettier from 'eslint-plugin-prettier';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
import unusedImports from 'eslint-plugin-unused-imports';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const gitignorePath = path.resolve(__dirname, '.gitignore');
|
||||
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
||||
const gitignorePatterns = gitignoreContent
|
||||
.split('\n')
|
||||
.filter((line) => line.trim() && !line.startsWith('#'))
|
||||
.map((pattern) => pattern.trim());
|
||||
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
export default defineConfig(
|
||||
tseslint.configs.recommended,
|
||||
|
||||
jsxA11yPlugin.flatConfigs.recommended,
|
||||
reactPlugin.configs.flat.recommended,
|
||||
reactHooksPlugin.configs.flat.recommended,
|
||||
reactRefreshPlugin.configs.vite,
|
||||
|
||||
{ ignores: gitignorePatterns },
|
||||
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,tsx}'],
|
||||
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
},
|
||||
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
'react-hooks': reactHooksPlugin,
|
||||
prettier,
|
||||
'unused-imports': unusedImports,
|
||||
},
|
||||
|
||||
rules: {
|
||||
...prettierConfig.rules,
|
||||
|
||||
'prettier/prettier': ['error', {}, { fileInfoOptions: { withNodeModules: false } }],
|
||||
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: ['error', 'single', { avoidEscape: true }],
|
||||
semi: ['error', 'always'],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
indent: 'off',
|
||||
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react-hooks/set-state-in-effect': 'warn',
|
||||
'react-refresh/only-export-components': 'off',
|
||||
|
||||
'react/jsx-uses-react': 'warn',
|
||||
'react/jsx-uses-vars': 'warn',
|
||||
'react/no-danger-with-children': 'warn',
|
||||
'react/no-deprecated': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-typos': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
'react/display-name': 'off',
|
||||
|
||||
'jsx-a11y/alt-text': 'off',
|
||||
'jsx-a11y/no-autofocus': 'off',
|
||||
'jsx-a11y/click-events-have-key-events': 'off',
|
||||
'jsx-a11y/no-static-element-interactions': 'off',
|
||||
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
|
||||
],
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: { version: 'detect' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
After Width: | Height: | Size: 279 KiB |
@@ -1,254 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cachix": {
|
||||
"inputs": {
|
||||
"devenv": [
|
||||
"devenv"
|
||||
],
|
||||
"flake-compat": [
|
||||
"devenv"
|
||||
],
|
||||
"git-hooks": [
|
||||
"devenv",
|
||||
"git-hooks"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1748883665,
|
||||
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "latest",
|
||||
"repo": "cachix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv": {
|
||||
"inputs": {
|
||||
"cachix": "cachix",
|
||||
"flake-compat": "flake-compat",
|
||||
"git-hooks": "git-hooks",
|
||||
"nix": "nix",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753888869,
|
||||
"narHash": "sha256-VRYrrUmvXnBzfzuJVoI3os1H/0l8cJQ2KnrrxWkTB3E=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "bdf26a4453eff6bae835f33d519a36f77e0ca257",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devenv-root": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"narHash": "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY=",
|
||||
"type": "file",
|
||||
"url": "file:///dev/null"
|
||||
},
|
||||
"original": {
|
||||
"type": "file",
|
||||
"url": "file:///dev/null"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1747046372,
|
||||
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"devenv",
|
||||
"nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733312601,
|
||||
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_2": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1753121425,
|
||||
"narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "644e0fc48951a860279da645ba77fe4a6e814c5e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1750779888,
|
||||
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-parts": "flake-parts",
|
||||
"git-hooks-nix": [
|
||||
"devenv",
|
||||
"git-hooks"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-23-11": [
|
||||
"devenv"
|
||||
],
|
||||
"nixpkgs-regression": [
|
||||
"devenv"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752773918,
|
||||
"narHash": "sha256-dOi/M6yNeuJlj88exI+7k154z+hAhFcuB8tZktiW7rg=",
|
||||
"owner": "cachix",
|
||||
"repo": "nix",
|
||||
"rev": "031c3cf42d2e9391eee373507d8c12e0f9606779",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "devenv-2.30",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1752827260,
|
||||
"narHash": "sha256-noFjJbm/uWRcd2Lotr7ovedfhKVZT+LeJs9rU416lKQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b527e89270879aaaf584c41f26b2796be634bc9d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b527e89270879aaaf584c41f26b2796be634bc9d",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1751159883,
|
||||
"narHash": "sha256-urW/Ylk9FIfvXfliA1ywh75yszAbiTEVgpPeinFyVZo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "14a40a1d7fb9afa4739275ac642ed7301a9ba1ab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"devenv-root": "devenv-root",
|
||||
"flake-parts": "flake-parts_2",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
{
|
||||
inputs = {
|
||||
# required for some reason when entering the shell for devenv
|
||||
devenv-root = {
|
||||
url = "file+file:///dev/null";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
# node 24.4.1, postgres 17
|
||||
nixpkgs.url = "github:nixos/nixpkgs/b527e89270879aaaf584c41f26b2796be634bc9d";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
|
||||
devenv.url = "github:cachix/devenv";
|
||||
devenv.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
|
||||
extra-substituters = "https://devenv.cachix.org";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{ flake-parts, devenv-root, ... }:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [
|
||||
inputs.devenv.flakeModule
|
||||
];
|
||||
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-linux"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
|
||||
perSystem =
|
||||
{
|
||||
config,
|
||||
self',
|
||||
inputs',
|
||||
pkgs,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
let
|
||||
psqlConfig = {
|
||||
username = "postgres";
|
||||
password = "postgres";
|
||||
database = "zipline";
|
||||
};
|
||||
in
|
||||
{
|
||||
devenv.shells.default = {
|
||||
packages = with pkgs; [
|
||||
git
|
||||
|
||||
# to generate thumbnails
|
||||
ffmpeg
|
||||
|
||||
# for testing docker
|
||||
colima
|
||||
docker
|
||||
docker-compose
|
||||
];
|
||||
|
||||
scripts = {
|
||||
pgup.exec = ''
|
||||
process-compose up postgres -D
|
||||
'';
|
||||
|
||||
minioup.exec = ''
|
||||
process-compose up minio -D
|
||||
'';
|
||||
|
||||
downall.exec = ''
|
||||
process-compose down
|
||||
'';
|
||||
|
||||
# ensure that volumes are mounted with write access for docker containers
|
||||
start_colima.exec = ''
|
||||
colima start --mount $PWD/themes:w --mount $PWD/uploads:w --mount $PWD/public:w
|
||||
'';
|
||||
};
|
||||
|
||||
enterShell = ''
|
||||
export name="zipline-env";
|
||||
echo -e "\n[$name]: run 'pgup' to start services, 'pgdown' to stop services";
|
||||
'';
|
||||
|
||||
languages.javascript = {
|
||||
enable = true;
|
||||
package = pkgs.nodejs_24;
|
||||
|
||||
corepack.enable = true;
|
||||
};
|
||||
|
||||
services = {
|
||||
postgres = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_17;
|
||||
|
||||
initialScript = ''
|
||||
CREATE ROLE "${psqlConfig.username}" WITH LOGIN PASSWORD '${psqlConfig.password}' SUPERUSER;
|
||||
'';
|
||||
|
||||
initialDatabases = [
|
||||
{
|
||||
name = psqlConfig.database;
|
||||
user = psqlConfig.username;
|
||||
}
|
||||
];
|
||||
|
||||
listen_addresses = "0.0.0.0";
|
||||
port = 5432;
|
||||
};
|
||||
|
||||
minio = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
process.managers.process-compose = {
|
||||
tui.enable = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
@@ -1,130 +1,64 @@
|
||||
{
|
||||
"name": "zipline",
|
||||
"name": "zipline-next",
|
||||
"version": "2.4.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"version": "4.4.2",
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
"@dicedtomato/colors": "^1.0.3",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
||||
"bcrypt": "^5.0.0",
|
||||
"clsx": "^1.1.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"fastify": "^3.5.0",
|
||||
"fastify-cookie": "^4.1.0",
|
||||
"fastify-cors": "^4.1.0",
|
||||
"fastify-decorators": "^3.2.3",
|
||||
"fastify-favicon": "^3.0.0",
|
||||
"fastify-mongodb": "^2.0.1",
|
||||
"fastify-multipart": "^3.2.0",
|
||||
"fastify-rate-limit": "github:dicedtomatoreal/fastify-rate-limit",
|
||||
"fastify-static": "^3.2.1",
|
||||
"fastify-typeorm-plugin": "^2.1.2",
|
||||
"figlet": "^1.5.0",
|
||||
"inquirer": "^7.3.3",
|
||||
"material-ui-dropzone": "^3.5.0",
|
||||
"next": "^9.5.4",
|
||||
"pg": "^8.4.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-redux": "^7.2.1",
|
||||
"redux": "^4.0.5",
|
||||
"redux-persist": "^6.0.0",
|
||||
"toml-patch": "^0.2.3",
|
||||
"typeorm": "^0.2.28"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"dev": "ts-node src",
|
||||
"dev:verbose": "VERBOSE=true ts-node src",
|
||||
"build": "next build && tsc -p .",
|
||||
"start": "NODE_ENV=production node dist",
|
||||
"start:verbose": "NODE_ENV=production VERBOSE=true node dist"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/crypto-js": "^3.1.47",
|
||||
"@types/mongodb": "^3.5.27",
|
||||
"@types/node": "^14.11.2",
|
||||
"@types/react": "^16.9.49",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
||||
"@typescript-eslint/parser": "^4.4.0",
|
||||
"eslint": "^7.10.0",
|
||||
"eslint-plugin-react": "^7.21.3",
|
||||
"mongodb": "^3.6.2",
|
||||
"prettier": "2.1.2",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
ignoredBuiltDependencies:
|
||||
- unrs-resolver
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- '@prisma/client'
|
||||
- '@prisma/engines'
|
||||
- argon2
|
||||
- esbuild
|
||||
- prisma
|
||||
- sharp
|
||||
@@ -1,14 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
/** @type {import('prettier').Config} */
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
jsxSingleQuote: true,
|
||||
printWidth: 110,
|
||||
};
|
||||
@@ -1,370 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserFilesQuota" AS ENUM ('BY_BYTES', 'BY_FILES');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN', 'SUPERADMIN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OAuthProviderType" AS ENUM ('DISCORD', 'GOOGLE', 'GITHUB', 'OIDC');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IncompleteFileStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETE', 'FAILED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Zipline" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"firstSetup" BOOLEAN NOT NULL DEFAULT true,
|
||||
"coreReturnHttpsUrls" BOOLEAN NOT NULL DEFAULT false,
|
||||
"coreDefaultDomain" TEXT,
|
||||
"coreTempDirectory" TEXT NOT NULL,
|
||||
"chunksEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"chunksMax" INTEGER NOT NULL DEFAULT 99614720,
|
||||
"chunksSize" INTEGER NOT NULL DEFAULT 26214400,
|
||||
"tasksDeleteInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"tasksClearInvitesInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"tasksMaxViewsInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"tasksThumbnailsInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"tasksMetricsInterval" INTEGER NOT NULL DEFAULT 1800000,
|
||||
"filesRoute" TEXT NOT NULL DEFAULT '/u',
|
||||
"filesLength" INTEGER NOT NULL DEFAULT 6,
|
||||
"filesDefaultFormat" TEXT NOT NULL DEFAULT 'random',
|
||||
"filesDisabledExtensions" TEXT[],
|
||||
"filesMaxFileSize" INTEGER NOT NULL DEFAULT 104857600,
|
||||
"filesDefaultExpiration" INTEGER,
|
||||
"filesAssumeMimetypes" BOOLEAN NOT NULL DEFAULT false,
|
||||
"filesDefaultDateFormat" TEXT NOT NULL DEFAULT 'YYYY-MM-DD_HH:mm:ss',
|
||||
"filesRemoveGpsMetadata" BOOLEAN NOT NULL DEFAULT false,
|
||||
"urlsRoute" TEXT NOT NULL DEFAULT '/go',
|
||||
"urlsLength" INTEGER NOT NULL DEFAULT 6,
|
||||
"featuresImageCompression" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresRobotsTxt" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresHealthcheck" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresUserRegistration" BOOLEAN NOT NULL DEFAULT false,
|
||||
"featuresOauthRegistration" BOOLEAN NOT NULL DEFAULT false,
|
||||
"featuresDeleteOnMaxViews" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresThumbnailsEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresThumbnailsNumberThreads" INTEGER NOT NULL DEFAULT 4,
|
||||
"featuresMetricsEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"featuresMetricsAdminOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"featuresMetricsShowUserSpecific" BOOLEAN NOT NULL DEFAULT true,
|
||||
"invitesEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"invitesLength" INTEGER NOT NULL DEFAULT 6,
|
||||
"websiteTitle" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"websiteTitleLogo" TEXT,
|
||||
"websiteExternalLinks" JSONB NOT NULL DEFAULT '[{ "name": "GitHub", "url": "https://github.com/diced/zipline"}, { "name": "Documentation", "url": "https://zipline.diced.sh/"}]',
|
||||
"websiteLoginBackground" TEXT,
|
||||
"websiteDefaultAvatar" TEXT,
|
||||
"websiteTos" TEXT,
|
||||
"websiteThemeDefault" TEXT NOT NULL DEFAULT 'system',
|
||||
"websiteThemeDark" TEXT NOT NULL DEFAULT 'builtin:dark_gray',
|
||||
"websiteThemeLight" TEXT NOT NULL DEFAULT 'builtin:light_gray',
|
||||
"oauthBypassLocalLogin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"oauthLoginOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||
"oauthDiscordClientId" TEXT,
|
||||
"oauthDiscordClientSecret" TEXT,
|
||||
"oauthDiscordRedirectUri" TEXT,
|
||||
"oauthGoogleClientId" TEXT,
|
||||
"oauthGoogleClientSecret" TEXT,
|
||||
"oauthGoogleRedirectUri" TEXT,
|
||||
"oauthGithubClientId" TEXT,
|
||||
"oauthGithubClientSecret" TEXT,
|
||||
"oauthGithubRedirectUri" TEXT,
|
||||
"oauthOidcClientId" TEXT,
|
||||
"oauthOidcClientSecret" TEXT,
|
||||
"oauthOidcAuthorizeUrl" TEXT,
|
||||
"oauthOidcTokenUrl" TEXT,
|
||||
"oauthOidcUserinfoUrl" TEXT,
|
||||
"oauthOidcRedirectUri" TEXT,
|
||||
"mfaTotpEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"mfaTotpIssuer" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"mfaPasskeys" BOOLEAN NOT NULL DEFAULT false,
|
||||
"ratelimitEnabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"ratelimitMax" INTEGER NOT NULL DEFAULT 10,
|
||||
"ratelimitWindow" INTEGER,
|
||||
"ratelimitAdminBypass" BOOLEAN NOT NULL DEFAULT true,
|
||||
"ratelimitAllowList" TEXT[],
|
||||
"httpWebhookOnUpload" TEXT,
|
||||
"httpWebhookOnShorten" TEXT,
|
||||
"discordWebhookUrl" TEXT,
|
||||
"discordUsername" TEXT,
|
||||
"discordAvatarUrl" TEXT,
|
||||
"discordOnUploadWebhookUrl" TEXT,
|
||||
"discordOnUploadUsername" TEXT,
|
||||
"discordOnUploadAvatarUrl" TEXT,
|
||||
"discordOnUploadContent" TEXT,
|
||||
"discordOnUploadEmbed" JSONB,
|
||||
"discordOnShortenWebhookUrl" TEXT,
|
||||
"discordOnShortenUsername" TEXT,
|
||||
"discordOnShortenAvatarUrl" TEXT,
|
||||
"discordOnShortenContent" TEXT,
|
||||
"discordOnShortenEmbed" JSONB,
|
||||
"pwaEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"pwaTitle" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"pwaShortName" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"pwaDescription" TEXT NOT NULL DEFAULT 'Zipline',
|
||||
"pwaThemeColor" TEXT NOT NULL DEFAULT '#000000',
|
||||
"pwaBackgroundColor" TEXT NOT NULL DEFAULT '#000000',
|
||||
|
||||
CONSTRAINT "Zipline_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT,
|
||||
"avatar" TEXT,
|
||||
"token" TEXT NOT NULL,
|
||||
"role" "Role" NOT NULL DEFAULT 'USER',
|
||||
"view" JSONB NOT NULL DEFAULT '{}',
|
||||
"totpSecret" TEXT,
|
||||
"sessions" TEXT[],
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Export" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"completed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"path" TEXT NOT NULL,
|
||||
"files" INTEGER NOT NULL,
|
||||
"size" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Export_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserQuota" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"filesQuota" "UserFilesQuota" NOT NULL,
|
||||
"maxBytes" TEXT,
|
||||
"maxFiles" INTEGER,
|
||||
"maxUrls" INTEGER,
|
||||
"userId" TEXT,
|
||||
|
||||
CONSTRAINT "UserQuota_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserPasskey" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"lastUsed" TIMESTAMP(3),
|
||||
"name" TEXT NOT NULL,
|
||||
"reg" JSONB NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "UserPasskey_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OAuthProvider" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"provider" "OAuthProviderType" NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"accessToken" TEXT NOT NULL,
|
||||
"refreshToken" TEXT,
|
||||
"oauthId" TEXT,
|
||||
|
||||
CONSTRAINT "OAuthProvider_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "File" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deletesAt" TIMESTAMP(3),
|
||||
"name" TEXT NOT NULL,
|
||||
"originalName" TEXT,
|
||||
"size" BIGINT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxViews" INTEGER,
|
||||
"favorite" BOOLEAN NOT NULL DEFAULT false,
|
||||
"password" TEXT,
|
||||
"userId" TEXT,
|
||||
"folderId" TEXT,
|
||||
|
||||
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Thumbnail" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"fileId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Thumbnail_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Folder" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"public" BOOLEAN NOT NULL DEFAULT false,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Folder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IncompleteFile" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"status" "IncompleteFileStatus" NOT NULL,
|
||||
"chunksTotal" INTEGER NOT NULL,
|
||||
"chunksComplete" INTEGER NOT NULL,
|
||||
"metadata" JSONB NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "IncompleteFile_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Tag" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"color" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
|
||||
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Url" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"vanity" TEXT,
|
||||
"destination" TEXT NOT NULL,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxViews" INTEGER,
|
||||
"password" TEXT,
|
||||
"userId" TEXT,
|
||||
|
||||
CONSTRAINT "Url_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Metric" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"data" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "Metric_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Invite" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"code" TEXT NOT NULL,
|
||||
"uses" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxUses" INTEGER,
|
||||
"inviterId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Invite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_FileToTag" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_FileToTag_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_token_key" ON "User"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserQuota_userId_key" ON "UserQuota"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OAuthProvider_provider_oauthId_key" ON "OAuthProvider"("provider", "oauthId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Thumbnail_fileId_key" ON "Thumbnail"("fileId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Url_code_vanity_key" ON "Url"("code", "vanity");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Invite_code_key" ON "Invite"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_FileToTag_B_index" ON "_FileToTag"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Export" ADD CONSTRAINT "Export_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserQuota" ADD CONSTRAINT "UserQuota_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserPasskey" ADD CONSTRAINT "UserPasskey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OAuthProvider" ADD CONSTRAINT "OAuthProvider_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Thumbnail" ADD CONSTRAINT "Thumbnail_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IncompleteFile" ADD CONSTRAINT "IncompleteFile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_FileToTag" ADD CONSTRAINT "_FileToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "File"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_FileToTag" ADD CONSTRAINT "_FileToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ALTER COLUMN "filesDefaultExpiration" SET DATA TYPE TEXT;
|
||||
@@ -1,17 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ALTER COLUMN "chunksMax" SET DEFAULT '95mb',
|
||||
ALTER COLUMN "chunksMax" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "chunksSize" SET DEFAULT '25mb',
|
||||
ALTER COLUMN "chunksSize" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksDeleteInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksDeleteInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksClearInvitesInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksClearInvitesInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksMaxViewsInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksMaxViewsInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksThumbnailsInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksThumbnailsInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "tasksMetricsInterval" SET DEFAULT '30m',
|
||||
ALTER COLUMN "tasksMetricsInterval" SET DATA TYPE TEXT,
|
||||
ALTER COLUMN "filesMaxFileSize" SET DEFAULT '100mb',
|
||||
ALTER COLUMN "filesMaxFileSize" SET DATA TYPE TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "websiteLoginBackgroundBlur" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "filesRandomWordsNumAdjectives" INTEGER NOT NULL DEFAULT 2,
|
||||
ADD COLUMN "filesRandomWordsSeparator" TEXT NOT NULL DEFAULT '-';
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Folder" ADD COLUMN "allowUploads" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "featuresVersionAPI" TEXT NOT NULL DEFAULT 'https://zipline-version.diced.sh',
|
||||
ADD COLUMN "featuresVersionChecking" BOOLEAN NOT NULL DEFAULT true;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "oauthDiscordWhitelistIds" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `oauthDiscordWhitelistIds` on the `Zipline` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" DROP COLUMN "oauthDiscordWhitelistIds",
|
||||
ADD COLUMN "oauthDiscordAllowedIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN "oauthDiscordDeniedIds" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Zipline" ADD COLUMN "domains" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "filesDefaultCompressionFormat" TEXT DEFAULT 'jpg';
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "featuresThumbnailsFormat" TEXT NOT NULL DEFAULT 'jpg';
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "coreTrustProxy" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "filesMaxExpiration" TEXT;
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `mfaPasskeys` on the `Zipline` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" DROP COLUMN "mfaPasskeys",
|
||||
ADD COLUMN "mfaPasskeysEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "mfaPasskeysOrigin" TEXT,
|
||||
ADD COLUMN "mfaPasskeysRpID" TEXT;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "tasksCleanThumbnailsInterval" TEXT NOT NULL DEFAULT '1d';
|
||||
@@ -1,6 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Folder" ADD COLUMN "parentId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."Folder" ADD CONSTRAINT "Folder_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "public"."Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `sessions` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."User" DROP COLUMN "sessions";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "public"."UserSession" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"ua" TEXT NOT NULL,
|
||||
"client" TEXT NOT NULL,
|
||||
"device" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -1,403 +0,0 @@
|
||||
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([])
|
||||
}
|
||||
|
||||
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[]
|
||||
}
|
||||
|
||||
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 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 UserQuota {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
filesQuota UserFilesQuota
|
||||
maxBytes String?
|
||||
maxFiles Int?
|
||||
|
||||
maxUrls Int?
|
||||
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String? @unique
|
||||
}
|
||||
|
||||
enum UserFilesQuota {
|
||||
BY_BYTES
|
||||
BY_FILES
|
||||
}
|
||||
|
||||
model UserPasskey {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
lastUsed DateTime?
|
||||
|
||||
name String
|
||||
reg Json
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
userId String
|
||||
}
|
||||
|
||||
enum Role {
|
||||
USER
|
||||
ADMIN
|
||||
SUPERADMIN
|
||||
}
|
||||
|
||||
model OAuthProvider {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
userId String
|
||||
provider OAuthProviderType
|
||||
|
||||
username String
|
||||
accessToken String
|
||||
refreshToken String?
|
||||
oauthId String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([provider, oauthId])
|
||||
}
|
||||
|
||||
enum OAuthProviderType {
|
||||
DISCORD
|
||||
GOOGLE
|
||||
GITHUB
|
||||
OIDC
|
||||
}
|
||||
|
||||
model File {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletesAt DateTime?
|
||||
|
||||
name String // name & file saved on datasource
|
||||
originalName String? // original name of file when uploaded
|
||||
size BigInt
|
||||
type String
|
||||
views Int @default(0)
|
||||
maxViews Int?
|
||||
favorite Boolean @default(false)
|
||||
password String?
|
||||
|
||||
tags Tag[]
|
||||
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
userId String?
|
||||
|
||||
Folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
folderId String?
|
||||
|
||||
thumbnail Thumbnail?
|
||||
}
|
||||
|
||||
model Thumbnail {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
path String
|
||||
|
||||
file File @relation(fields: [fileId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
fileId String
|
||||
|
||||
@@unique([fileId])
|
||||
}
|
||||
|
||||
model Folder {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String
|
||||
public Boolean @default(false)
|
||||
allowUploads Boolean @default(false)
|
||||
|
||||
files File[]
|
||||
|
||||
parentId String?
|
||||
parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
children Folder[] @relation("FolderToFolder")
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
userId String
|
||||
}
|
||||
|
||||
model IncompleteFile {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
status IncompleteFileStatus
|
||||
chunksTotal Int
|
||||
chunksComplete Int
|
||||
|
||||
metadata Json
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
userId String
|
||||
}
|
||||
|
||||
enum IncompleteFileStatus {
|
||||
PENDING
|
||||
PROCESSING
|
||||
COMPLETE
|
||||
FAILED
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String @unique
|
||||
color String
|
||||
|
||||
files File[]
|
||||
User User? @relation(fields: [userId], references: [id])
|
||||
userId String?
|
||||
}
|
||||
|
||||
model 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])
|
||||
}
|
||||
|
||||
model Metric {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
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
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,3 @@
|
||||
module.exports = async (markdown) => {
|
||||
return markdown;
|
||||
};
|
||||
@@ -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',
|
||||
),
|
||||
);
|
||||
@@ -1,55 +0,0 @@
|
||||
type StepCommand = string | (() => void | Promise<void>);
|
||||
|
||||
export function step(name: string, command: StepCommand, condition: () => boolean = () => true) {
|
||||
return {
|
||||
name,
|
||||
command,
|
||||
condition,
|
||||
};
|
||||
}
|
||||
|
||||
export type Step = ReturnType<typeof step>;
|
||||
|
||||
function log(message: string) {
|
||||
console.log(`\n${message}\n`);
|
||||
}
|
||||
|
||||
export async function run(name: string, ...steps: Step[]) {
|
||||
const { execSync } = await import('child_process');
|
||||
|
||||
const runOne = process.argv[2];
|
||||
if (runOne) {
|
||||
const match = steps.find((s) => `${name}/${s.name}` === runOne);
|
||||
if (!match) {
|
||||
console.error(`x No step found with name "${runOne}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
steps = [match];
|
||||
}
|
||||
|
||||
const start = process.hrtime();
|
||||
for (const step of steps) {
|
||||
if (!step.condition()) {
|
||||
log(`- Skipping step "${name}/${step.name}"...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
log(`> Running step "${name}/${step.name}"...`);
|
||||
if (typeof step.command === 'string') {
|
||||
execSync(step.command, { stdio: 'inherit' });
|
||||
} else {
|
||||
await step.command();
|
||||
}
|
||||
} catch {
|
||||
console.error(`x Step "${name}/${step.name}" failed.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const diff = process.hrtime(start);
|
||||
const time = diff[0] * 1e9 + diff[1];
|
||||
const timeStr = time > 1e9 ? `${(time / 1e9).toFixed(2)}s` : `${(time / 1e6).toFixed(2)}ms`;
|
||||
log(`✓ Steps in "${name}" completed in ${timeStr}.`);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { step } from '.';
|
||||
|
||||
export const lintStep = step('lint', 'eslint .');
|
||||
@@ -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),
|
||||
);
|
||||
@@ -1,9 +0,0 @@
|
||||
import { run, step } from '.';
|
||||
import { lintStep } from './lint';
|
||||
|
||||
run(
|
||||
'validate',
|
||||
|
||||
lintStep,
|
||||
step('format', 'prettier --write --ignore-path .gitignore .'),
|
||||
);
|
||||
@@ -0,0 +1,145 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const inquirer = require('inquirer');
|
||||
const { stringify } = require('toml-patch');
|
||||
const { writeFileSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
|
||||
const createDockerCompose = (port) => {
|
||||
return `version: "3"
|
||||
services:
|
||||
zipline:
|
||||
ports:
|
||||
- "${port}:${port}"
|
||||
volumes:
|
||||
- "${join(process.cwd(), 'uploads')}:/opt/zipline/uploads"
|
||||
build: .
|
||||
tty: true`;
|
||||
};
|
||||
|
||||
const base = {
|
||||
database: {},
|
||||
meta: {
|
||||
title: 'Zipline',
|
||||
description: 'My Zipline Server',
|
||||
thumbnail:
|
||||
'https://github.githubassets.com/images/modules/open_graph/github-mark.png',
|
||||
color: '#128377'
|
||||
},
|
||||
core: { secret: 'my-secret', port: 3000, host: '127.0.0.1', theme: 'dark', secure: false },
|
||||
uploader: {
|
||||
directory: './uploads',
|
||||
route: '/u',
|
||||
length: 6,
|
||||
original: false,
|
||||
blacklisted: []
|
||||
},
|
||||
urls: { route: '/s', length: 4, vanity: false }
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const database = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'type',
|
||||
message: 'What database type?',
|
||||
choices: [
|
||||
{ name: 'postgres', extra: 'This is what we recomend using.' },
|
||||
{ name: 'cockroachdb' },
|
||||
{ name: 'mysql' },
|
||||
{ name: 'mariadb' },
|
||||
{ name: 'mssql' },
|
||||
{ name: 'sqlite3' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'host',
|
||||
message: 'Database Host (leave blank if sqlite3)'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'port',
|
||||
message: 'Database Port (leave blank if sqlite3)'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'database',
|
||||
message: 'Database Name (db path if sqlite3)'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Database User (leave blank if sqlite3)'
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: 'Database Password (leave blank if sqlite3)'
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('\nCore\n');
|
||||
|
||||
const core = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'secret',
|
||||
message: 'Secret (this must be secure)'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'port',
|
||||
message: 'Serve on Port'
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('\nUploader\n');
|
||||
|
||||
const uploader = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'directory',
|
||||
message: 'Uploads Directory'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'original',
|
||||
message: 'Keep Original File names?'
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('\nURLs\n');
|
||||
|
||||
const urls = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'vanity',
|
||||
message: 'Allow vanity URLs'
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('\nDocker\n');
|
||||
|
||||
const docker = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'useDocker',
|
||||
message: 'Use Docker?'
|
||||
}
|
||||
]);
|
||||
|
||||
const config = {
|
||||
database: { ...database },
|
||||
meta: { ...base.meta },
|
||||
core: { ...base.core, ...core },
|
||||
uploader: { ...base.uploader, ...uploader },
|
||||
urls: { ...base.urls, ...urls }
|
||||
};
|
||||
|
||||
writeFileSync('Ziplined.toml', stringify(config));
|
||||
|
||||
if (docker.useDocker) {
|
||||
console.log('Generating docker-compose.yml...');
|
||||
writeFileSync('docker-compose.yml', createDockerCompose(config.core.port));
|
||||
}
|
||||
})();
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ContextModalProps, ModalsProvider } from '@mantine/modals';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { SWRConfig } from 'swr';
|
||||
import ThemeProvider from '@/components/ThemeProvider';
|
||||
import { type ZiplineTheme } from '@/lib/theme';
|
||||
import { type Config } from '@/lib/config/validate';
|
||||
import { Button, Text } from '@mantine/core';
|
||||
|
||||
const AlertModal = ({ context, id, innerProps }: ContextModalProps<{ modalBody: string }>) => (
|
||||
<>
|
||||
<Text size='sm'>{innerProps.modalBody}</Text>
|
||||
|
||||
<Button fullWidth mt='md' onClick={() => context.closeModal(id)}>
|
||||
OK
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
const contextModals = {
|
||||
alert: AlertModal,
|
||||
};
|
||||
|
||||
declare module '@mantine/modals' {
|
||||
export interface MantineModalsOverride {
|
||||
modals: typeof contextModals;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Root({
|
||||
themes,
|
||||
defaultTheme,
|
||||
}: {
|
||||
themes?: ZiplineTheme[];
|
||||
defaultTheme?: Config['website']['theme'];
|
||||
}) {
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: async (url: RequestInfo | URL) => {
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
|
||||
throw new Error(json.message);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ThemeProvider ssrThemes={themes} ssrDefaultTheme={defaultTheme}>
|
||||
<ModalsProvider
|
||||
modalProps={{
|
||||
overlayProps: {
|
||||
blur: 6,
|
||||
},
|
||||
centered: true,
|
||||
}}
|
||||
modals={contextModals}
|
||||
>
|
||||
<Notifications position='top-center' zIndex={10000000} />
|
||||
<Outlet />
|
||||
</ModalsProvider>
|
||||
</ThemeProvider>
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import GenericError from './GenericError';
|
||||
|
||||
export default function DashboardErrorBoundary(props: Record<string, any>) {
|
||||
return (
|
||||
<GenericError
|
||||
title='Dashboard Client Error'
|
||||
message='Something went wrong while loading the dashboard. Please try again later, or report this issue if it persists.'
|
||||
details={{ ...props, type: 'dashboard' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Container, Paper, ScrollArea, Stack, Text, Title } from '@mantine/core';
|
||||
import { useRouteError } from 'react-router-dom';
|
||||
import FourOhFour from '../pages/404';
|
||||
|
||||
export default function GenericError({
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
}: {
|
||||
title?: string;
|
||||
message?: string;
|
||||
details?: Record<string, any>;
|
||||
}) {
|
||||
const routerError: any = useRouteError();
|
||||
if (routerError?.status === 404) return <FourOhFour />;
|
||||
|
||||
const routeError = JSON.parse(JSON.stringify(routerError, Object.getOwnPropertyNames(routerError)));
|
||||
|
||||
console.error(routerError);
|
||||
|
||||
return (
|
||||
<Container my='lg'>
|
||||
<Stack gap='xs'>
|
||||
<Title order={5}>{title || 'An error occurred'}</Title>
|
||||
<Text c='dimmed'>
|
||||
{message || 'Something went wrong. Please try again later, or report this issue if it persists.'}
|
||||
</Text>
|
||||
{details && (
|
||||
<Paper withBorder px={3} py={3}>
|
||||
<ScrollArea>
|
||||
<pre style={{ margin: 0 }}>{JSON.stringify({ routeError, details }, null, 2)}</pre>
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import GenericError from './GenericError';
|
||||
|
||||
export default function RootErrorBoundary(props: Record<string, any>) {
|
||||
return (
|
||||
<GenericError
|
||||
title='Dashboard Client Error'
|
||||
message='Something went wrong while loading the dashboard. Please try again later, or report this issue if it persists.'
|
||||
details={{ ...props, type: 'root' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<title>Zipline</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './routes';
|
||||
|
||||
import '@mantine/charts/styles.css';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/dropzone/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import 'mantine-datatable/styles.css';
|
||||
import './styles/global.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
import { Button, Center, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconArrowLeft } from '@tabler/icons-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function FourOhFour() {
|
||||
useTitle('404');
|
||||
|
||||
return (
|
||||
<Center h='100vh'>
|
||||
<Stack>
|
||||
<Title order={1}>404</Title>
|
||||
<Text c='dimmed' mt='-md'>
|
||||
Page not found
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to='/auth/login'
|
||||
color='blue'
|
||||
fullWidth
|
||||
leftSection={<IconArrowLeft size='1rem' />}
|
||||
>
|
||||
Go home
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
import ExternalAuthButton from '@/components/pages/login/ExternalAuthButton';
|
||||
import LocalLogin from '@/components/pages/login/LocalLogin';
|
||||
import PasskeyAuthButton from '@/components/pages/login/PasskeyAuthButton';
|
||||
import SecureWarningModal from '@/components/pages/login/SecureWarningModal';
|
||||
import TotpModal from '@/components/pages/login/TotpModal';
|
||||
import { getWebClient } from '@/lib/api/detect';
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import useLogin from '@/lib/hooks/useLogin';
|
||||
import useObjectState from '@/lib/hooks/useObjectState';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
import {
|
||||
Anchor,
|
||||
Box,
|
||||
Center,
|
||||
Divider,
|
||||
Group,
|
||||
Image,
|
||||
LoadingOverlay,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';
|
||||
import {
|
||||
IconBrandDiscordFilled,
|
||||
IconBrandGithubFilled,
|
||||
IconBrandGoogleFilled,
|
||||
IconCheck,
|
||||
IconCircleKeyFilled,
|
||||
} from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
import GenericError from '../../error/GenericError';
|
||||
|
||||
export default function Login() {
|
||||
useTitle('Login');
|
||||
|
||||
const query = new URLSearchParams(location.search);
|
||||
const navigate = useNavigate();
|
||||
const { user, mutate } = useLogin();
|
||||
|
||||
const isHttps = window.location.protocol === 'https:';
|
||||
const webClient = JSON.stringify(getWebClient());
|
||||
|
||||
const { data: config, error: configError, isLoading: configLoading } = useSWR('/api/server/public');
|
||||
|
||||
const showLocalLogin =
|
||||
query.get('local') === 'true' ||
|
||||
!(
|
||||
config?.oauth?.bypassLocalLogin &&
|
||||
Object.values(config?.oauthEnabled ?? {}).filter((x) => x === true).length > 0
|
||||
);
|
||||
|
||||
const willRedirect =
|
||||
config?.oauth?.bypassLocalLogin &&
|
||||
Object.values(config?.oauthEnabled ?? {}).filter((x) => x === true).length === 1 &&
|
||||
query.get('local') !== 'true';
|
||||
|
||||
useEffect(() => {
|
||||
if (willRedirect && config) {
|
||||
const provider = Object.keys(config.oauthEnabled).find(
|
||||
(x) => config.oauthEnabled[x as keyof typeof config.oauthEnabled] === true,
|
||||
);
|
||||
|
||||
if (provider) window.location.href = `/api/auth/oauth/${provider.toLowerCase()}`;
|
||||
}
|
||||
}, [willRedirect, config]);
|
||||
|
||||
const [totp, setTotp] = useObjectState({
|
||||
open: false,
|
||||
disabled: false,
|
||||
error: '',
|
||||
pin: '',
|
||||
});
|
||||
|
||||
const [secureModal, setSecureModal] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: { username: '', password: '' },
|
||||
validate: {
|
||||
username: (v) => (v.length >= 1 ? null : 'Username is required'),
|
||||
password: (v) => (v.length >= 1 ? null : 'Password is required'),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) navigate('/dashboard');
|
||||
if (config?.firstSetup) navigate('/auth/setup');
|
||||
}, [user, config, navigate]);
|
||||
|
||||
const handleLoginSubmit = async (values: any, code?: string) => {
|
||||
setTotp({ disabled: true, error: '' });
|
||||
|
||||
const { data, error } = await fetchApi(
|
||||
'/api/auth/login',
|
||||
'POST',
|
||||
{ ...values, code },
|
||||
{ 'x-zipline-client': webClient },
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (ApiError.check(error, 1044)) {
|
||||
form.setFieldError('username', 'Invalid username');
|
||||
form.setFieldError('password', 'Invalid password');
|
||||
} else {
|
||||
setTotp('error', error.error || 'Login failed');
|
||||
}
|
||||
setTotp('disabled', false);
|
||||
} else if (data?.totp) {
|
||||
setTotp({ open: true, disabled: false });
|
||||
} else {
|
||||
showNotification({
|
||||
message: 'Logging in...',
|
||||
icon: <IconCheck size='1rem' />,
|
||||
autoClose: 700,
|
||||
});
|
||||
mutate(data);
|
||||
}
|
||||
};
|
||||
|
||||
if (configLoading || !config) return <LoadingOverlay visible />;
|
||||
if (configError) return <GenericError title='Error' message='Config load failed' details={configError} />;
|
||||
|
||||
const hasBg = !!config.website.loginBackground;
|
||||
|
||||
return (
|
||||
<>
|
||||
{willRedirect && !showLocalLogin && <LoadingOverlay visible />}
|
||||
|
||||
<TotpModal
|
||||
state={totp}
|
||||
onPinChange={(val) => setTotp('pin', val)}
|
||||
onVerify={() => handleLoginSubmit(form.values, totp.pin)}
|
||||
onCancel={() => {
|
||||
setTotp('open', false);
|
||||
form.reset();
|
||||
}}
|
||||
/>
|
||||
|
||||
<SecureWarningModal
|
||||
opened={secureModal}
|
||||
onClose={() => setSecureModal(false)}
|
||||
returnHttps={config.returnHttps}
|
||||
/>
|
||||
|
||||
{isHttps && !config.returnHttps && (
|
||||
<Box pos='absolute' top={10} left='50%' style={{ transform: 'translateX(-50%)' }}>
|
||||
<Text size='sm' c='red' ta='center'>
|
||||
You are accessing this instance through a <b>secure</b> context but the server is not configured
|
||||
to use HTTPS. Click <Anchor onClick={() => setSecureModal(true)}> here</Anchor> to learn more.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isHttps && config.returnHttps && (
|
||||
<Box pos='absolute' top={10} left='50%' style={{ transform: 'translateX(-50%)' }}>
|
||||
<Text size='sm' c='red' ta='center'>
|
||||
You are accessing this instance through an <b>insecure</b> context but the server is configured to
|
||||
use HTTPS. This may cause issues when logging in. Click{' '}
|
||||
<Anchor onClick={() => setSecureModal(true)}> here</Anchor> to learn more.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Center h='100vh'>
|
||||
{hasBg && (
|
||||
<Image
|
||||
src={config.website.loginBackground}
|
||||
pos='absolute'
|
||||
inset={0}
|
||||
w='100%'
|
||||
h='100%'
|
||||
fit='cover'
|
||||
style={{ filter: config.website.loginBackgroundBlur ? 'blur(10px)' : undefined }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Paper
|
||||
w='350px'
|
||||
p='xl'
|
||||
shadow='xl'
|
||||
withBorder
|
||||
pos='relative'
|
||||
style={{
|
||||
backgroundColor: hasBg ? 'transparent' : undefined,
|
||||
backdropFilter: hasBg ? 'blur(35px)' : undefined,
|
||||
}}
|
||||
>
|
||||
<Title order={1} ta='center' mb='md'>
|
||||
<b>{config.website.title ?? 'Zipline'}</b>
|
||||
</Title>
|
||||
|
||||
<Stack>
|
||||
{showLocalLogin && (
|
||||
<LocalLogin
|
||||
form={form}
|
||||
onSubmit={handleLoginSubmit}
|
||||
loading={totp.disabled}
|
||||
hasBackground={hasBg}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider label='or' />
|
||||
|
||||
{config.mfa.passkeys && browserSupportsWebAuthn() && <PasskeyAuthButton onAuthSuccess={mutate} />}
|
||||
|
||||
<Group grow>
|
||||
{config.oauthEnabled.discord && (
|
||||
<ExternalAuthButton
|
||||
provider='Discord'
|
||||
leftSection={<IconBrandDiscordFilled stroke={4} size='1.1rem' />}
|
||||
/>
|
||||
)}
|
||||
{config.oauthEnabled.github && (
|
||||
<ExternalAuthButton provider='GitHub' leftSection={<IconBrandGithubFilled size='1.1rem' />} />
|
||||
)}
|
||||
{config.oauthEnabled.google && (
|
||||
<ExternalAuthButton
|
||||
provider='Google'
|
||||
leftSection={<IconBrandGoogleFilled stroke={4} size='1.1rem' />}
|
||||
/>
|
||||
)}
|
||||
{config.oauthEnabled.oidc && (
|
||||
<ExternalAuthButton provider='OIDC' leftSection={<IconCircleKeyFilled size='1.1rem' />} />
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Image,
|
||||
LoadingOverlay,
|
||||
Paper,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications, showNotification } from '@mantine/notifications';
|
||||
import { IconLogin, IconPlus, IconUserPlus, IconX } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import GenericError from '../../error/GenericError';
|
||||
import { getWebClient } from '@/lib/api/detect';
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Register');
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const {
|
||||
data: config,
|
||||
error: configError,
|
||||
isLoading: configLoading,
|
||||
} = useSWR<Response['/api/server/public']>('/api/server/public', {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenHidden: false,
|
||||
revalidateIfStale: false,
|
||||
});
|
||||
|
||||
const code = new URLSearchParams(location.search).get('code') ?? undefined;
|
||||
const {
|
||||
data: invite,
|
||||
error: inviteError,
|
||||
isLoading: inviteLoading,
|
||||
} = useSWR<Response['/api/auth/invites/web']>(
|
||||
location.search.includes('code') ? `/api/auth/invites/web${location.search}` : null,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenHidden: false,
|
||||
revalidateIfStale: false,
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
tos: false,
|
||||
},
|
||||
validate: {
|
||||
username: (value) => (value.length >= 1 ? null : 'Username is required'),
|
||||
password: (value) => (value.length >= 1 ? null : 'Password is required'),
|
||||
},
|
||||
enhanceGetInputProps: ({ field }) => ({
|
||||
name: field,
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('/api/user');
|
||||
if (res.ok) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) return;
|
||||
|
||||
if (!config?.features.userRegistration && !code) {
|
||||
navigate('/auth/login');
|
||||
}
|
||||
}, [code, config]);
|
||||
|
||||
const onSubmit = async (values: typeof form.values) => {
|
||||
const { username, password, tos } = values;
|
||||
|
||||
if (tos === false && config!.website.tos) {
|
||||
form.setFieldError('tos', 'You must agree to the Terms of Service to continue');
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await fetchApi(
|
||||
'/api/auth/register',
|
||||
'POST',
|
||||
{
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
},
|
||||
{
|
||||
'x-zipline-client': JSON.stringify(getWebClient()),
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (ApiError.check(error, 1039)) {
|
||||
form.setFieldError('username', 'Username is taken');
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Failed to register',
|
||||
message: error.error,
|
||||
color: 'red',
|
||||
icon: <IconX size='1rem' />,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Complete!',
|
||||
message: `Your "${data?.user?.username}" account has been created.`,
|
||||
color: 'green',
|
||||
icon: <IconPlus size='1rem' />,
|
||||
});
|
||||
|
||||
mutate('/api/user');
|
||||
navigate('/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || configLoading) return <LoadingOverlay visible />;
|
||||
|
||||
if (!config || configError) {
|
||||
return (
|
||||
<GenericError
|
||||
title='Error loading configuration'
|
||||
message='Could not load server configuration...'
|
||||
details={configError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (code && inviteError) {
|
||||
if (inviteError) {
|
||||
showNotification({
|
||||
id: 'invalid-invite',
|
||||
message: 'Invalid or expired invite. Please try again later.',
|
||||
color: 'red',
|
||||
});
|
||||
|
||||
navigate('/auth/login');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inviteLoading) return <LoadingOverlay visible />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Center h='100vh'>
|
||||
{config.website.loginBackground && (
|
||||
<Image
|
||||
src={config.website.loginBackground}
|
||||
alt='Background'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
...(config.website.loginBackgroundBlur && { filter: 'blur(10px)' }),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Paper
|
||||
w='350px'
|
||||
p='xl'
|
||||
shadow='xl'
|
||||
withBorder
|
||||
style={{
|
||||
backgroundColor: config.website.loginBackground ? 'rgba(0, 0, 0, 0)' : undefined,
|
||||
backdropFilter: config.website.loginBackgroundBlur ? 'blur(35px)' : undefined,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%', overflowWrap: 'break-word' }}>
|
||||
<Title
|
||||
order={1}
|
||||
ta='center'
|
||||
style={{
|
||||
whiteSpace: 'normal',
|
||||
fontSize: `clamp(20px, ${Math.max(50 - (config.website.title?.length ?? 0) / 2, 20)}px, 50px)`,
|
||||
}}
|
||||
>
|
||||
<b>{config.website.title ?? 'Zipline'}</b>
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
{invite && (
|
||||
<Text ta='center' size='sm' c='dimmed'>
|
||||
You’ve been invited to join <b>{config?.website?.title ?? 'Zipline'}</b>
|
||||
{invite.inviter && (
|
||||
<>
|
||||
{' '}
|
||||
by <b>{invite.inviter.username}</b>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack my='sm'>
|
||||
<TextInput
|
||||
size='md'
|
||||
placeholder='Enter your username...'
|
||||
autoComplete='username'
|
||||
styles={{
|
||||
input: {
|
||||
backgroundColor: config.website.loginBackground ? 'transparent' : undefined,
|
||||
},
|
||||
}}
|
||||
{...form.getInputProps('username', { withError: true })}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
size='md'
|
||||
placeholder='Enter your password...'
|
||||
autoComplete='new-password'
|
||||
styles={{
|
||||
input: {
|
||||
backgroundColor: config.website.loginBackground ? 'transparent' : undefined,
|
||||
},
|
||||
}}
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
|
||||
{config.website.tos && (
|
||||
<Checkbox
|
||||
label={
|
||||
<Text size='xs'>
|
||||
I agree to the{' '}
|
||||
<Link to='/auth/tos' target='_blank'>
|
||||
Terms of Service
|
||||
</Link>
|
||||
</Text>
|
||||
}
|
||||
required
|
||||
{...form.getInputProps('tos', { type: 'checkbox' })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size='md'
|
||||
fullWidth
|
||||
type='submit'
|
||||
variant={config.website.loginBackground ? 'outline' : 'filled'}
|
||||
leftSection={<IconUserPlus size='1rem' />}
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
<Stack my='xs'>
|
||||
<Divider label='or' />
|
||||
<Button
|
||||
component={Link}
|
||||
to='/auth/login'
|
||||
size='md'
|
||||
fullWidth
|
||||
variant='outline'
|
||||
leftSection={<IconLogin size='1rem' />}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
Component.displayName = 'Register';
|
||||
@@ -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'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
|
||||
"Server Settings" 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 "Finish" 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';
|
||||
@@ -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';
|
||||
@@ -1,10 +0,0 @@
|
||||
import DashboardInvites from '@/components/pages/invites';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Invites');
|
||||
|
||||
return <DashboardInvites />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/Admin/Invites';
|
||||
@@ -1,10 +0,0 @@
|
||||
import DashboardServerSettings from '@/components/pages/serverSettings';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Server Settings');
|
||||
|
||||
return <DashboardServerSettings />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/Admin/Settings';
|
||||
@@ -1,23 +0,0 @@
|
||||
import ViewUserFiles from '@/components/pages/users/ViewUserFiles';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
import { Params, redirect, useLoaderData } from 'react-router-dom';
|
||||
|
||||
export async function loader({ params }: { params: Params<string> }) {
|
||||
const res = await fetch('/api/users/' + params.id);
|
||||
if (!res.ok) {
|
||||
console.log("can't get user", res.status);
|
||||
return redirect('/dashboard/admin/users');
|
||||
}
|
||||
|
||||
const user = await res.json();
|
||||
return { user };
|
||||
}
|
||||
|
||||
export function Component() {
|
||||
const { user } = useLoaderData<typeof loader>();
|
||||
useTitle(`${user ? user.username : 'User'}'s files`);
|
||||
|
||||
return <ViewUserFiles />;
|
||||
}
|
||||
|
||||
Component.displayName = 'DashboardAdminViewUserFiles';
|
||||
@@ -1,10 +0,0 @@
|
||||
import DashboardUsers from '@/components/pages/users';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Users');
|
||||
|
||||
return <DashboardUsers />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/Admin/Users';
|
||||
@@ -1,10 +0,0 @@
|
||||
import DashboardFiles from '@/components/pages/files';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Files');
|
||||
|
||||
return <DashboardFiles />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/Files';
|
||||
@@ -1,10 +0,0 @@
|
||||
import DashboardFolders from '@/components/pages/folders';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Folders');
|
||||
|
||||
return <DashboardFolders />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/Folders';
|
||||
@@ -1,10 +0,0 @@
|
||||
import DashboardHome from '@/components/pages/dashboard';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle();
|
||||
|
||||
return <DashboardHome />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/';
|
||||
@@ -1,28 +0,0 @@
|
||||
import DashboardMetrics from '@/components/pages/metrics';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
import { isAdministrator } from '@/lib/role';
|
||||
import { redirect } from 'react-router-dom';
|
||||
|
||||
export async function loader() {
|
||||
const configRes = await fetch('/api/server/public');
|
||||
if (!configRes.ok) throw new Error('Failed to get public configuration');
|
||||
|
||||
const config = await configRes.json();
|
||||
if (config.features.metrics?.adminOnly) {
|
||||
const res = await fetch('/api/user');
|
||||
if (!res.ok) return redirect('/auth/login');
|
||||
|
||||
const { user } = await res.json();
|
||||
if (!isAdministrator(user.role)) return redirect('/dashboard');
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function Component() {
|
||||
useTitle('Metrics');
|
||||
|
||||
return <DashboardMetrics />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/Metrics';
|
||||
@@ -1,10 +0,0 @@
|
||||
import DashboardSettings from '@/components/pages/settings';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Settings');
|
||||
|
||||
return <DashboardSettings />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/Settings';
|
||||
@@ -1,10 +0,0 @@
|
||||
import UploadFile from '@/components/pages/upload/File';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Upload File');
|
||||
|
||||
return <UploadFile />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/Upload/File';
|
||||
@@ -1,10 +0,0 @@
|
||||
import UploadText from '@/components/pages/upload/Text';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle('Upload Text');
|
||||
|
||||
return <UploadText />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/Upload/Text';
|
||||
@@ -1,10 +0,0 @@
|
||||
import DashboardURLs from '@/components/pages/urls';
|
||||
import { useTitle } from '@/lib/hooks/useTitle';
|
||||
|
||||
export function Component() {
|
||||
useTitle('URLs');
|
||||
|
||||
return <DashboardURLs />;
|
||||
}
|
||||
|
||||
Component.displayName = 'Dashboard/URLs';
|
||||
@@ -1,164 +0,0 @@
|
||||
import { type Response } from '@/lib/api/response';
|
||||
import { Folder } from '@/lib/db/models/folder';
|
||||
import { FolderBreadcrumb } from '@/lib/folderHierarchy';
|
||||
import {
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Breadcrumbs,
|
||||
Card,
|
||||
Container,
|
||||
Group,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconFolder, IconUpload } from '@tabler/icons-react';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Link, Params, useLoaderData, useNavigate } from 'react-router-dom';
|
||||
|
||||
const DashboardFile = lazy(() => import('@/components/file/DashboardFile'));
|
||||
|
||||
export async function loader({ params }: { params: Params<string> }) {
|
||||
const res = await fetch(`/api/server/folder/${params.id}`);
|
||||
if (!res.ok) {
|
||||
throw new Response('Folder not found', { status: 404 });
|
||||
}
|
||||
return {
|
||||
folder: (await res.json()) as Response['/api/server/folder/[id]'],
|
||||
};
|
||||
}
|
||||
|
||||
function PublicFolderCard({ folder }: { folder: Partial<Folder> }) {
|
||||
return (
|
||||
<Link to={`/folder/${folder.id}`} style={{ textDecoration: 'none' }}>
|
||||
<Card withBorder shadow='sm' radius='sm' style={{ cursor: 'pointer' }}>
|
||||
<Card.Section withBorder inheritPadding py='xs'>
|
||||
<Group gap='xs'>
|
||||
<IconFolder size='1.2rem' />
|
||||
<Text fw={500}>{folder.name}</Text>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
<Card.Section inheritPadding py='xs'>
|
||||
<Stack gap={2}>
|
||||
<Text size='xs' c='dimmed'>
|
||||
{folder._count?.files ?? 0} files
|
||||
</Text>
|
||||
{(folder._count?.children ?? 0) > 0 && (
|
||||
<Text size='xs' c='dimmed'>
|
||||
{folder._count?.children} subfolders
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function Component() {
|
||||
const { folder } = useLoaderData<typeof loader>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const buildBreadcrumbs = () => {
|
||||
const items: FolderBreadcrumb[] = [];
|
||||
|
||||
let current = folder.parent as Partial<Folder> | undefined;
|
||||
while (current && current.public) {
|
||||
items.unshift({ id: current.id!, name: current.name!, public: true });
|
||||
current = current.parent as Partial<Folder> | undefined;
|
||||
}
|
||||
|
||||
items.push({ id: folder.id!, name: folder.name!, public: true });
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const breadcrumbs = buildBreadcrumbs();
|
||||
const children = (folder.children ?? []) as Partial<Folder>[];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container my='lg'>
|
||||
{breadcrumbs.length > 1 && (
|
||||
<Breadcrumbs mb='md'>
|
||||
{breadcrumbs.map((item, index) => (
|
||||
<Anchor
|
||||
key={item.id}
|
||||
onClick={() => navigate(`/folder/${item.id}`)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
fw={index === breadcrumbs.length - 1 ? 600 : 400}
|
||||
>
|
||||
{item.name}
|
||||
</Anchor>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
)}
|
||||
|
||||
<Group>
|
||||
<Title order={1}>{folder.name}</Title>
|
||||
|
||||
{folder.allowUploads && (
|
||||
<Link to={`/folder/${folder.id}/upload`}>
|
||||
<ActionIcon variant='outline'>
|
||||
<IconUpload size='1rem' />
|
||||
</ActionIcon>
|
||||
</Link>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{children.length > 0 && (
|
||||
<>
|
||||
<Title order={3} mt='md' mb='sm'>
|
||||
Subfolders
|
||||
</Title>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
lg: 4,
|
||||
md: 3,
|
||||
sm: 2,
|
||||
}}
|
||||
spacing='md'
|
||||
>
|
||||
{children.map((child) => (
|
||||
<PublicFolderCard key={child.id} folder={child} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(folder.files?.length ?? 0) > 0 && (
|
||||
<>
|
||||
<Title order={3} mt='md' mb='sm'>
|
||||
Files
|
||||
</Title>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
lg: 3,
|
||||
md: 2,
|
||||
}}
|
||||
spacing='md'
|
||||
>
|
||||
{folder.files?.map((file: any) => (
|
||||
<Suspense fallback={<Skeleton height={350} animate />} key={file.id}>
|
||||
<DashboardFile file={file} reduce />
|
||||
</Suspense>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
|
||||
{children.length === 0 && (folder.files?.length ?? 0) === 0 && (
|
||||
<Text c='dimmed' mt='md'>
|
||||
This folder is empty.
|
||||
</Text>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Component.displayName = 'ViewFolderId';
|
||||