mirror of
https://github.com/diced/zipline.git
synced 2025-04-11 23:31:17 -05:00
Merge branch 'trunk' into trunk
This commit is contained in:
commit
00efc1a7e5
34 changed files with 2789 additions and 2961 deletions
4
.github/workflows/docker-release.yml
vendored
4
.github/workflows/docker-release.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
|||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
@ -52,7 +52,6 @@ jobs:
|
|||
${{ 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
|
||||
|
@ -103,7 +102,6 @@ jobs:
|
|||
--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 }}
|
||||
|
|
|
@ -24,7 +24,6 @@ services:
|
|||
- .env
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:postgres@postgres/postgres2
|
||||
- CORE_SECRET=thissecretisverynotsecretbutshouldonlybeusedindevelopmentsoitsfineiguess
|
||||
- CORE_HOSTNAME=0.0.0.0
|
||||
depends_on:
|
||||
- postgres
|
||||
|
|
|
@ -17,7 +17,8 @@ services:
|
|||
retries: 5
|
||||
|
||||
zipline:
|
||||
image: ghcr.io/diced/zipline:v4
|
||||
image: ghcr.io/diced/zipline:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '3000:3000'
|
||||
env_file:
|
||||
|
|
108
package.json
108
package.json
|
@ -20,90 +20,90 @@
|
|||
"db:migrate": "prisma migrate dev --create-only"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.717.0",
|
||||
"@fastify/cookie": "^9.4.0",
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/multipart": "^8.3.0",
|
||||
"@fastify/rate-limit": "^9.1.0",
|
||||
"@fastify/sensible": "^5.6.0",
|
||||
"@fastify/static": "^7.0.4",
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/cors": "^10.0.2",
|
||||
"@fastify/multipart": "^9.0.3",
|
||||
"@fastify/rate-limit": "^10.2.2",
|
||||
"@fastify/sensible": "^6.0.3",
|
||||
"@fastify/static": "^8.1.1",
|
||||
"@github/webauthn-json": "^2.1.1",
|
||||
"@mantine/charts": "^7.15.1",
|
||||
"@mantine/code-highlight": "^7.15.1",
|
||||
"@mantine/core": "^7.15.1",
|
||||
"@mantine/dates": "^7.15.1",
|
||||
"@mantine/dropzone": "^7.15.1",
|
||||
"@mantine/form": "^7.15.1",
|
||||
"@mantine/hooks": "^7.15.1",
|
||||
"@mantine/modals": "^7.15.1",
|
||||
"@mantine/notifications": "^7.15.1",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"@prisma/internals": "^6.1.0",
|
||||
"@prisma/migrate": "^6.1.0",
|
||||
"@tabler/icons-react": "^3.26.0",
|
||||
"@xoi/gps-metadata-remover": "^1.1.2",
|
||||
"@mantine/charts": "^7.17.0",
|
||||
"@mantine/code-highlight": "^7.17.0",
|
||||
"@mantine/core": "^7.17.0",
|
||||
"@mantine/dates": "^7.17.0",
|
||||
"@mantine/dropzone": "^7.17.0",
|
||||
"@mantine/form": "^7.17.0",
|
||||
"@mantine/hooks": "^7.17.0",
|
||||
"@mantine/modals": "^7.17.0",
|
||||
"@mantine/notifications": "^7.17.0",
|
||||
"@prisma/client": "^6.4.1",
|
||||
"@prisma/internals": "^6.4.1",
|
||||
"@prisma/migrate": "^6.4.1",
|
||||
"@tabler/icons-react": "^3.30.0",
|
||||
"@xoi/gps-metadata-remover": "^2.0.0",
|
||||
"argon2": "^0.41.1",
|
||||
"bytes": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"colorette": "^2.0.20",
|
||||
"commander": "^12.1.0",
|
||||
"commander": "^13.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastify": "^4.29.0",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fastify": "^5.2.1",
|
||||
"fastify-plugin": "^5.0.1",
|
||||
"fflate": "^0.8.2",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"highlight.js": "^11.11.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"iron-session": "^8.0.4",
|
||||
"isomorphic-dompurify": "^2.19.0",
|
||||
"katex": "^0.16.18",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
"katex": "^0.16.21",
|
||||
"mantine-datatable": "^7.15.1",
|
||||
"ms": "^2.1.3",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"next": "^15.1.2",
|
||||
"next": "^15.1.7",
|
||||
"otplib": "^12.0.1",
|
||||
"prisma": "^6.1.0",
|
||||
"prisma": "^6.4.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0-rc.1",
|
||||
"react-dom": "^19.0.0-rc.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"react-markdown": "^10.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sharp": "^0.33.5",
|
||||
"swr": "^2.2.5",
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.2"
|
||||
"swr": "^2.3.2",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.4",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@eslint/compat": "^1.2.7",
|
||||
"@eslint/eslintrc": "^3.3.0",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.27",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/ms": "^0.7.34",
|
||||
"@types/ms": "^2.1.0",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^20.17.10",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.1",
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-next": "^15.1.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.24.1",
|
||||
"@typescript-eslint/parser": "^8.24.1",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-next": "^15.1.7",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier": "^3.5.2",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tsup": "^8.3.5",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
"tsup": "^8.3.6",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
|
5070
pnpm-lock.yaml
generated
5070
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["omitApi"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
|
|
@ -88,13 +88,7 @@ export default function DashboardSettings() {
|
|||
</SimpleGrid>
|
||||
|
||||
<Stack mt='md' gap='md'>
|
||||
{error ? (
|
||||
<div>Error loading server settings</div>
|
||||
) : (
|
||||
<>
|
||||
<ServerSettingsDiscord swr={{ data, isLoading }} />
|
||||
</>
|
||||
)}
|
||||
{error ? null : <ServerSettingsDiscord swr={{ data, isLoading }} />}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -40,7 +40,6 @@ export function settingsOnSubmit(router: NextRouter, form: ReturnType<typeof use
|
|||
});
|
||||
|
||||
await fetch('/reload');
|
||||
await fetch('/api/reload');
|
||||
mutate('/api/server/settings', data);
|
||||
router.replace(router.asPath, undefined, { scroll: false });
|
||||
}
|
||||
|
|
|
@ -144,7 +144,8 @@ export default function SettingsFileView() {
|
|||
|
||||
<Switch
|
||||
label='Enable Embed'
|
||||
description='Enable the following embed properties. These properties take advantage of OpenGraph tags.'
|
||||
description='Enable the following embed properties. These properties take advantage of OpenGraph tags. View routes will need to be enabled for this to work.'
|
||||
disabled={!form.values.enabled}
|
||||
my='xs'
|
||||
{...form.getInputProps('embed', { type: 'checkbox' })}
|
||||
/>
|
||||
|
@ -152,22 +153,22 @@ export default function SettingsFileView() {
|
|||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='sm'>
|
||||
<TextInput
|
||||
label='Embed Title'
|
||||
disabled={!form.values.embed}
|
||||
disabled={!form.values.embed || !form.values.enabled}
|
||||
{...form.getInputProps('embedTitle')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Embed Description'
|
||||
disabled={!form.values.embed}
|
||||
disabled={!form.values.embed || !form.values.enabled}
|
||||
{...form.getInputProps('embedDescription')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Embed Site Name'
|
||||
disabled={!form.values.embed}
|
||||
disabled={!form.values.embed || !form.values.enabled}
|
||||
{...form.getInputProps('embedSiteName')}
|
||||
/>
|
||||
<ColorInput
|
||||
label='Embed Color'
|
||||
disabled={!form.values.embed}
|
||||
disabled={!form.values.embed || !form.values.enabled}
|
||||
{...form.getInputProps('embedColor')}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
|
|
@ -20,6 +20,7 @@ import useSWR from 'swr';
|
|||
import { flameshot } from './generators/flameshot';
|
||||
import { sharex } from './generators/sharex';
|
||||
import { shell } from './generators/shell';
|
||||
import { ishare } from './generators/ishare';
|
||||
|
||||
export type GeneratorOptions = {
|
||||
deletesAt: string | null;
|
||||
|
@ -84,6 +85,7 @@ const generators = {
|
|||
Flameshot: flameshot,
|
||||
ShareX: sharex,
|
||||
'Shell Script': shell,
|
||||
ishare,
|
||||
};
|
||||
|
||||
export default function GeneratorButton({
|
||||
|
@ -110,7 +112,7 @@ export default function GeneratorButton({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={() => setOpen(false)} title={`Generate ${name}`}>
|
||||
<Modal opened={opened} onClose={() => setOpen(false)} title={`Generate ${name} Uploader`}>
|
||||
{desc && (
|
||||
<Text size='sm' c='dimmed'>
|
||||
{desc}
|
||||
|
@ -121,7 +123,7 @@ export default function GeneratorButton({
|
|||
<Select
|
||||
data={[
|
||||
{ label: 'Upload File', value: 'file' },
|
||||
{ label: 'Shorten URL', value: 'url' },
|
||||
{ label: 'Shorten URL', value: 'url', disabled: name === 'ishare' },
|
||||
]}
|
||||
description='Select which type of destination you want to generate'
|
||||
label='Destination Type'
|
||||
|
|
63
src/components/pages/settings/parts/SettingsGenerators/generators/ishare.tsx
Executable file
63
src/components/pages/settings/parts/SettingsGenerators/generators/ishare.tsx
Executable file
|
@ -0,0 +1,63 @@
|
|||
import { UploadHeaders } from '@/lib/uploader/parseHeaders';
|
||||
import { GeneratorOptions, download } from '../GeneratorButton';
|
||||
|
||||
export function ishare(token: string, type: 'file' | 'url', options: GeneratorOptions) {
|
||||
if (type === 'url') {
|
||||
// unsupported in ishare
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
requesturl: `${window.location.origin}/api/upload`,
|
||||
name: `Zipline - ${window.location.origin} - File`,
|
||||
headers: {},
|
||||
fileformname: 'file',
|
||||
responseurl: '{{files[0].url}}',
|
||||
};
|
||||
|
||||
const toAddHeaders: UploadHeaders = {
|
||||
authorization: token,
|
||||
};
|
||||
|
||||
if (options.deletesAt !== null && type === 'file') {
|
||||
toAddHeaders['x-zipline-deletes-at'] = options.deletesAt;
|
||||
} else {
|
||||
delete toAddHeaders['x-zipline-deletes-at'];
|
||||
}
|
||||
|
||||
if (options.format !== 'default' && type === 'file') {
|
||||
toAddHeaders['x-zipline-format'] = options.format;
|
||||
} else {
|
||||
delete toAddHeaders['x-zipline-format'];
|
||||
}
|
||||
|
||||
if (options.imageCompressionPercent !== null && type === 'file') {
|
||||
toAddHeaders['x-zipline-image-compression-percent'] = options.imageCompressionPercent.toString();
|
||||
} else {
|
||||
delete toAddHeaders['x-zipline-image-compression-percent'];
|
||||
}
|
||||
|
||||
if (options.maxViews !== null) {
|
||||
toAddHeaders['x-zipline-max-views'] = options.maxViews.toString();
|
||||
} else {
|
||||
delete toAddHeaders['x-zipline-max-views'];
|
||||
}
|
||||
|
||||
if (options.addOriginalName === true && type === 'file') {
|
||||
toAddHeaders['x-zipline-original-name'] = 'true';
|
||||
} else {
|
||||
delete toAddHeaders['x-zipline-original-name'];
|
||||
}
|
||||
|
||||
if (options.overrides_returnDomain !== null) {
|
||||
toAddHeaders['x-zipline-domain'] = options.overrides_returnDomain;
|
||||
} else {
|
||||
delete toAddHeaders['x-zipline-domain'];
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(toAddHeaders)) {
|
||||
(config as any).headers[key] = value;
|
||||
}
|
||||
|
||||
return download(`zipline-${type}.iscu`, JSON.stringify(config, null, 2));
|
||||
}
|
|
@ -3,7 +3,7 @@ import { GeneratorOptions, download } from '../GeneratorButton';
|
|||
|
||||
export function sharex(token: string, type: 'file' | 'url', options: GeneratorOptions) {
|
||||
const config = {
|
||||
Version: '14.1.0',
|
||||
Version: '17.0.0',
|
||||
Name: `Zipline - ${window.location.origin} - ${type === 'file' ? 'File' : 'URL'}`,
|
||||
DestinationType: 'ImageUploader, TextUploader, FileUploader',
|
||||
RequestMethod: 'POST',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Anchor, Code, Group, Paper, Text, Title } from '@mantine/core';
|
||||
import { Anchor, Code, Group, Paper, Text, Title, Image as MantineImage } from '@mantine/core';
|
||||
import { IconPrompt } from '@tabler/icons-react';
|
||||
import Image from 'next/image';
|
||||
import GeneratorButton from './GeneratorButton';
|
||||
|
@ -52,6 +52,23 @@ export default function SettingsGenerators() {
|
|||
</>
|
||||
}
|
||||
/>
|
||||
<GeneratorButton
|
||||
name='ishare'
|
||||
icon={
|
||||
<MantineImage
|
||||
width={24}
|
||||
height={24}
|
||||
alt='ishare logo'
|
||||
src='https://isharemac.app/ishare/Util/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png'
|
||||
/>
|
||||
}
|
||||
desc={
|
||||
<>
|
||||
This generator requires <Anchor href='https://isharemac.app/'>ishare</Anchor> to be installed on
|
||||
macOS. This uploader is intended for use on macOS only.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<GeneratorButton
|
||||
name='Shell Script'
|
||||
icon={<IconPrompt size={24} />}
|
||||
|
|
|
@ -95,7 +95,6 @@ export default function ImportButton() {
|
|||
});
|
||||
|
||||
await fetch('/reload');
|
||||
await fetch('/api/reload');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
IconTrashFilled,
|
||||
IconWriting,
|
||||
} from '@tabler/icons-react';
|
||||
import ms from 'ms';
|
||||
import ms, { StringValue } from 'ms';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
@ -133,8 +133,8 @@ export default function UploadOptionsButton({ numFiles }: { numFiles: number })
|
|||
The file will automatically delete itself after this time.{' '}
|
||||
{config.files.defaultExpiration ? (
|
||||
<>
|
||||
The default expiration time is <b>{ms(config.files.defaultExpiration)}</b> (you can
|
||||
override this with the below option).
|
||||
The default expiration time is <b>{ms(config.files.defaultExpiration as StringValue)}</b>{' '}
|
||||
(you can override this with the below option).
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -8,9 +8,9 @@ export default function Markdown({ md }: { md: string }) {
|
|||
<Paper withBorder p='md'>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code({ node: _, inline, className, children, ...props }) {
|
||||
code({ node: _, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
return match ? (
|
||||
<HighlightCode language={match[1]} code={String(children).replace(/\n$/, '')} />
|
||||
) : (
|
||||
<Code className={className} {...props}>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { ApiAuthOauthResponse } from '@/pages/api/auth/oauth';
|
||||
|
||||
import { ApiAuthInvitesResponse } from '@/server/routes/api/auth/invites';
|
||||
import { ApiAuthInvitesIdResponse } from '@/server/routes/api/auth/invites/[id]';
|
||||
import { ApiLoginResponse } from '@/server/routes/api/auth/login';
|
||||
import { ApiLogoutResponse } from '@/server/routes/api/auth/logout';
|
||||
import { ApiAuthOauthResponse } from '@/server/routes/api/auth/oauth';
|
||||
import { ApiAuthRegisterResponse } from '@/server/routes/api/auth/register';
|
||||
import { ApiAuthWebauthnResponse } from '@/server/routes/api/auth/webauthn';
|
||||
import { ApiHealthcheckResponse } from '@/server/routes/api/healthcheck';
|
||||
|
|
|
@ -7,7 +7,9 @@ export async function readToDataURL(file: File): Promise<string> {
|
|||
});
|
||||
}
|
||||
|
||||
export async function fetchToDataURL(url: string) {
|
||||
export async function fetchToDataURL(url?: string) {
|
||||
if (!url) return null;
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return null;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import msFn from 'ms';
|
||||
import msFn, { StringValue } from 'ms';
|
||||
import { log } from '../logger';
|
||||
import { bytes } from '../bytes';
|
||||
import { prisma } from '../db';
|
||||
|
@ -446,7 +446,7 @@ function parse(value: string, type: EnvType) {
|
|||
case 'byte':
|
||||
return bytes(value);
|
||||
case 'ms':
|
||||
return msFn(value);
|
||||
return msFn(value as StringValue);
|
||||
case 'json[]':
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
|
|
|
@ -147,7 +147,7 @@ export function parseHeaders(headers: UploadHeaders, fileConfig: Config['files']
|
|||
response.deletesAt = expiresAt;
|
||||
} else {
|
||||
if (fileConfig.defaultExpiration) {
|
||||
const expiresAt = new Date(Date.now() + ms(fileConfig.defaultExpiration));
|
||||
const expiresAt = new Date(Date.now() + ms(fileConfig.defaultExpiration as StringValue));
|
||||
response.deletesAt = expiresAt;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ async function generate(config: Config, datasource: Datasource, ids: string[]) {
|
|||
stream.pipe(writeStream);
|
||||
stream.on('error', reject);
|
||||
writeStream.on('error', reject);
|
||||
writeStream.on('finish', resolve);
|
||||
writeStream.on('finish', resolve as any);
|
||||
});
|
||||
|
||||
const thumbnailTmpFile = join(config.core.tempDirectory, `zthumbnail_${file.id}.jpg`);
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { ziplineAuth } from '@/lib/middleware/ziplineAuth';
|
||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
import { OAuthProvider, OAuthProviderType } from '@prisma/client';
|
||||
|
||||
export type ApiAuthOauthResponse = OAuthProvider[];
|
||||
|
||||
type Body = {
|
||||
provider?: OAuthProviderType;
|
||||
};
|
||||
|
||||
const logger = log('api').c('auth').c('oauth');
|
||||
|
||||
async function handler(req: NextApiReq<Body>, res: NextApiRes<ApiAuthOauthResponse>) {
|
||||
if (req.method === 'DELETE') {
|
||||
const { password } = (await prisma.user.findFirst({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
select: {
|
||||
password: true,
|
||||
},
|
||||
}))!;
|
||||
|
||||
if (!req.user.oauthProviders.length) return res.badRequest('No providers to delete');
|
||||
if (req.user.oauthProviders.length === 1 && !password)
|
||||
return res.badRequest("You can't your last oauth provider without a password");
|
||||
|
||||
const { provider } = req.body;
|
||||
if (!provider) return res.badRequest('Provider is required');
|
||||
|
||||
const providers = await prisma.user.update({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
data: {
|
||||
oauthProviders: {
|
||||
deleteMany: [{ provider }],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
oauthProviders: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} unlinked an oauth provider`, {
|
||||
provider,
|
||||
});
|
||||
|
||||
return res.ok(providers.oauthProviders);
|
||||
} else {
|
||||
return res.ok(req.user.oauthProviders);
|
||||
}
|
||||
}
|
||||
|
||||
export default combine([method(['GET', 'DELETE']), ziplineAuth()], handler);
|
|
@ -1,25 +0,0 @@
|
|||
import { reloadSettings } from '@/lib/config';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { isAdministrator } from '@/lib/role';
|
||||
import { getSession } from '@/server/session';
|
||||
import { NextApiRequest, NextApiResponse } from 'next/types';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession(req, res);
|
||||
if (!session.id || !session.sessionId) return res.redirect(302, '/auth/login');
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
sessions: {
|
||||
has: session.sessionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return res.redirect(302, '/dashboard');
|
||||
if (!isAdministrator(user.role)) return res.redirect(302, '/dashboard');
|
||||
|
||||
await reloadSettings();
|
||||
|
||||
return res.json({ success: true });
|
||||
}
|
|
@ -247,9 +247,16 @@ export default function Login({ config }: InferGetServerSidePropsType<typeof get
|
|||
backdropFilter: config.website.loginBackgroundBlur ? 'blur(35px)' : undefined,
|
||||
}}
|
||||
>
|
||||
<Title order={1} size={50} ta='center'>
|
||||
<b>{config.website.title ?? 'Zipline'}</b>
|
||||
</Title>
|
||||
<div style={{ width: '100%', overflowWrap: 'break-word' }}>
|
||||
<Title
|
||||
order={1}
|
||||
size={(config.website.title ?? 'Zipline').length > 50 ? 20 : 50}
|
||||
ta='center'
|
||||
style={{ whiteSpace: 'normal' }}
|
||||
>
|
||||
<b>{config.website.title ?? 'Zipline'}</b>
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
{showLocalLogin && (
|
||||
<>
|
||||
|
|
|
@ -20,7 +20,7 @@ import { fastifySensible } from '@fastify/sensible';
|
|||
import { fastifyStatic } from '@fastify/static';
|
||||
import fastify from 'fastify';
|
||||
import { mkdir, readFile } from 'fs/promises';
|
||||
import ms from 'ms';
|
||||
import ms, { StringValue } from 'ms';
|
||||
import { parse } from 'url';
|
||||
import { version } from '../../package.json';
|
||||
import { checkRateLimit } from './plugins/checkRateLimit';
|
||||
|
@ -28,6 +28,7 @@ import next, { ALL_METHODS } from './plugins/next';
|
|||
import loadRoutes from './routes';
|
||||
import { filesRoute } from './routes/files.dy';
|
||||
import { urlsRoute } from './routes/urls.dy';
|
||||
import oauthPlugin from './plugins/oauth';
|
||||
|
||||
const MODE = process.env.NODE_ENV || 'production';
|
||||
const logger = log('server');
|
||||
|
@ -95,6 +96,8 @@ async function main() {
|
|||
root: config.core.tempDirectory,
|
||||
});
|
||||
|
||||
await server.register(oauthPlugin);
|
||||
|
||||
if (config.ratelimit.enabled) {
|
||||
try {
|
||||
checkRateLimit(config);
|
||||
|
@ -169,22 +172,22 @@ async function main() {
|
|||
server.next('/reload', ALL_METHODS);
|
||||
}
|
||||
|
||||
// TODO: no longer need this when all the api routes are handled by fastify :)
|
||||
const routeKeys = Object.keys(routes); // holds "currently migrated routes" so we can parse json through fastify
|
||||
server.addContentTypeParser('application/json', (req, body, done) => {
|
||||
if (routeKeys.includes(req.routeOptions.config.url)) {
|
||||
let bodyString = '';
|
||||
body.on('data', (chunk) => {
|
||||
bodyString += chunk;
|
||||
});
|
||||
// // TODO: no longer need this when all the api routes are handled by fastify :)
|
||||
// const routeKeys = Object.keys(routes); // holds "currently migrated routes" so we can parse json through fastify
|
||||
// server.addContentTypeParser('application/json', (req, body, done) => {
|
||||
// if (routeKeys.includes(req.routeOptions.config.url)) {
|
||||
// let bodyString = '';
|
||||
// body.on('data', (chunk) => {
|
||||
// bodyString += chunk;
|
||||
// });
|
||||
|
||||
body.on('end', () => {
|
||||
if (bodyString === '' || bodyString === null) return done(null, {});
|
||||
// body.on('end', () => {
|
||||
// if (bodyString === '' || bodyString === null) return done(null, {});
|
||||
|
||||
server.getDefaultJsonParser('error', 'ignore')(req, bodyString, done);
|
||||
});
|
||||
} else done(null, body);
|
||||
});
|
||||
// server.getDefaultJsonParser('error', 'ignore')(req, bodyString, done);
|
||||
// });
|
||||
// } else done(null, body);
|
||||
// });
|
||||
|
||||
server.setErrorHandler((error, _, res) => {
|
||||
if (error.statusCode) {
|
||||
|
@ -209,10 +212,11 @@ async function main() {
|
|||
logger.info('server started', { hostname: config.core.hostname, port: config.core.port });
|
||||
|
||||
// Tasks
|
||||
tasks.interval('deletefiles', ms(config.tasks.deleteInterval), deleteFiles(prisma));
|
||||
tasks.interval('maxviews', ms(config.tasks.maxViewsInterval), maxViews(prisma));
|
||||
tasks.interval('deletefiles', ms(config.tasks.deleteInterval as StringValue), deleteFiles(prisma));
|
||||
tasks.interval('maxviews', ms(config.tasks.maxViewsInterval as StringValue), maxViews(prisma));
|
||||
|
||||
if (config.features.metrics) tasks.interval('metrics', ms(config.tasks.metricsInterval), metrics(prisma));
|
||||
if (config.features.metrics)
|
||||
tasks.interval('metrics', ms(config.tasks.metricsInterval as StringValue), metrics(prisma));
|
||||
|
||||
if (config.features.thumbnails.enabled) {
|
||||
for (let i = 0; i !== config.features.thumbnails.num_threads; ++i) {
|
||||
|
@ -222,8 +226,12 @@ async function main() {
|
|||
});
|
||||
}
|
||||
|
||||
tasks.interval('thumbnails', ms(config.tasks.thumbnailsInterval), thumbnails(prisma));
|
||||
tasks.interval('clearinvites', ms(config.tasks.clearInvitesInterval), clearInvites(prisma));
|
||||
tasks.interval('thumbnails', ms(config.tasks.thumbnailsInterval as StringValue), thumbnails(prisma));
|
||||
tasks.interval(
|
||||
'clearinvites',
|
||||
ms(config.tasks.clearInvitesInterval as StringValue),
|
||||
clearInvites(prisma),
|
||||
);
|
||||
}
|
||||
|
||||
tasks.start();
|
||||
|
|
|
@ -3,18 +3,7 @@ import fastifyPlugin from 'fastify-plugin';
|
|||
import next from 'next';
|
||||
import { NextServerOptions, RequestHandler } from 'next/dist/server/next';
|
||||
|
||||
export const ALL_METHODS: HTTPMethods[] = [
|
||||
'DELETE',
|
||||
'GET',
|
||||
'HEAD',
|
||||
'PATCH',
|
||||
'POST',
|
||||
'PUT',
|
||||
// 'OPTIONS',
|
||||
'COPY',
|
||||
'MOVE',
|
||||
'TRACE',
|
||||
];
|
||||
export const ALL_METHODS: HTTPMethods[] = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'];
|
||||
|
||||
async function nextPlugin(fastify: FastifyInstance, options: NextServerOptions) {
|
||||
const nextServer = next(options);
|
||||
|
@ -48,7 +37,7 @@ async function nextPlugin(fastify: FastifyInstance, options: NextServerOptions)
|
|||
|
||||
export default fastifyPlugin(nextPlugin, {
|
||||
name: 'next',
|
||||
fastify: '4.x',
|
||||
fastify: '5.x',
|
||||
});
|
||||
|
||||
declare module 'fastify' {
|
||||
|
|
88
src/lib/oauth/withOAuth.ts → src/server/plugins/oauth.ts
Executable file → Normal file
88
src/lib/oauth/withOAuth.ts → src/server/plugins/oauth.ts
Executable file → Normal file
|
@ -1,12 +1,12 @@
|
|||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||
import { OAuthProviderType } from '@prisma/client';
|
||||
import { prisma } from '../db';
|
||||
import { findProvider } from './providerUtil';
|
||||
import { createToken, decrypt } from '../crypto';
|
||||
import { config } from '../config';
|
||||
import { User } from '../db/models/user';
|
||||
import Logger, { log } from '../logger';
|
||||
import { getSession, saveSession } from '@/server/session';
|
||||
import { config } from '@/lib/config';
|
||||
import { createToken, decrypt } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import Logger, { log } from '@/lib/logger';
|
||||
import { findProvider } from '@/lib/oauth/providerUtil';
|
||||
import { OAuthProviderType, User } from '@prisma/client';
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { getSession, saveSession } from '../session';
|
||||
|
||||
export interface OAuthQuery {
|
||||
state?: string;
|
||||
|
@ -26,31 +26,31 @@ export interface OAuthResponse {
|
|||
redirect?: string;
|
||||
}
|
||||
|
||||
export const withOAuth =
|
||||
(
|
||||
async function oauthPlugin(fastify: FastifyInstance) {
|
||||
fastify.decorateRequest('oauthHandle', oauthHandle);
|
||||
|
||||
async function oauthHandle(
|
||||
this: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
provider: OAuthProviderType,
|
||||
oauthProfile: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
|
||||
) =>
|
||||
async (req: NextApiReq, res: NextApiRes) => {
|
||||
handler: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
|
||||
) {
|
||||
const logger = log('api').c('auth').c('oauth').c(provider.toLowerCase());
|
||||
|
||||
req.query.host = req.headers.host ?? 'localhost:3000';
|
||||
(this.query as any).host = this.headers.host ?? 'localhost:3000';
|
||||
|
||||
const response = await oauthProfile(req.query as OAuthQuery, logger);
|
||||
|
||||
const session = await getSession(req, res);
|
||||
const response = await handler(this.query as OAuthQuery, logger);
|
||||
const session = await getSession(this, reply);
|
||||
|
||||
if (response.error) {
|
||||
logger.warn('invalid oauth request', {
|
||||
error: response.error,
|
||||
});
|
||||
return res.serverError(response.error, {
|
||||
oauth: response.error_code,
|
||||
});
|
||||
return reply.internalServerError(response.error_code + ' ' + response.error);
|
||||
}
|
||||
|
||||
if (response.redirect) {
|
||||
return res.redirect(response.redirect);
|
||||
return reply.redirect(response.redirect);
|
||||
}
|
||||
|
||||
logger.debug('oauth response', {
|
||||
|
@ -76,7 +76,7 @@ export const withOAuth =
|
|||
},
|
||||
});
|
||||
|
||||
const { state } = req.query as OAuthQuery;
|
||||
const { state } = this.query as OAuthQuery;
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
|
@ -99,10 +99,10 @@ export const withOAuth =
|
|||
}
|
||||
|
||||
if (urlState === 'link') {
|
||||
if (!user) return res.unauthorized('invalid session');
|
||||
if (!user) return reply.unauthorized('invalid session');
|
||||
|
||||
if (findProvider(provider, user.oauthProviders))
|
||||
return res.badRequest('This account is already linked to this provider');
|
||||
return reply.badRequest('This account is already linked to this provider');
|
||||
|
||||
logger.debug('attempting to link oauth account', {
|
||||
provider,
|
||||
|
@ -134,7 +134,7 @@ export const withOAuth =
|
|||
user: user.id,
|
||||
});
|
||||
|
||||
return res.redirect('/dashboard/settings');
|
||||
return reply.redirect('/dashboard/settings');
|
||||
} catch (e) {
|
||||
logger.error('failed to link oauth account', {
|
||||
provider,
|
||||
|
@ -142,7 +142,7 @@ export const withOAuth =
|
|||
error: e,
|
||||
});
|
||||
|
||||
return res.badRequest('Cant link account, already linked with this provider');
|
||||
return reply.badRequest('Cant link account, already linked with this provider');
|
||||
}
|
||||
} else if (user && userOauth) {
|
||||
await prisma.oAuthProvider.update({
|
||||
|
@ -164,7 +164,7 @@ export const withOAuth =
|
|||
user: user.id,
|
||||
});
|
||||
|
||||
return res.redirect('/dashboard');
|
||||
return reply.redirect('/dashboard');
|
||||
} else if (existingOauth) {
|
||||
const login = await prisma.oAuthProvider.update({
|
||||
where: {
|
||||
|
@ -188,15 +188,15 @@ export const withOAuth =
|
|||
user: login.user!.id,
|
||||
});
|
||||
|
||||
return res.redirect('/dashboard');
|
||||
return reply.redirect('/dashboard');
|
||||
} else if (config.oauth.loginOnly) {
|
||||
logger.warn('user tried to create account with oauth, but login only is enabled', {
|
||||
oauth: response.username || 'unknown',
|
||||
ua: req.headers['user-agent'],
|
||||
ua: this.headers['user-agent'],
|
||||
});
|
||||
return res.badRequest("Can't create users through oauth.");
|
||||
return reply.badRequest("Can't create users through oauth.");
|
||||
} else if (existingUser) {
|
||||
return res.badRequest('This username is already taken');
|
||||
return reply.badRequest('This username is already taken');
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -224,20 +224,36 @@ export const withOAuth =
|
|||
user: nuser.id,
|
||||
});
|
||||
|
||||
return res.redirect('/dashboard');
|
||||
return reply.redirect('/dashboard');
|
||||
} catch (e) {
|
||||
if ((e as { code: string }).code === 'P2002') {
|
||||
// already linked can't create, last failsafe lol
|
||||
logger.warn('user tried to create account with oauth, but already linked', {
|
||||
oauth: response.username || 'unknown',
|
||||
ua: req.headers['user-agent'],
|
||||
ua: this.headers['user-agent'],
|
||||
});
|
||||
logger.debug('oauth create error', {
|
||||
error: e,
|
||||
response,
|
||||
});
|
||||
|
||||
return res.badRequest('Cant create user, already linked with this provider');
|
||||
return reply.badRequest('Cant create user, already linked with this provider');
|
||||
} else throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default fastifyPlugin(oauthPlugin, {
|
||||
name: 'oauth',
|
||||
fastify: '5.x',
|
||||
});
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
oauthHandle: (
|
||||
reply: FastifyReply,
|
||||
provider: OAuthProviderType,
|
||||
handler: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>,
|
||||
) => void;
|
||||
}
|
||||
}
|
27
src/pages/api/auth/oauth/discord.ts → src/server/routes/api/auth/oauth/discord.ts
Executable file → Normal file
27
src/pages/api/auth/oauth/discord.ts → src/server/routes/api/auth/oauth/discord.ts
Executable file → Normal file
|
@ -1,14 +1,13 @@
|
|||
import { fetchToDataURL } from '@/lib/base64';
|
||||
import { config } from '@/lib/config';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
|
||||
import { encrypt } from '@/lib/crypto';
|
||||
import Logger from '@/lib/logger';
|
||||
import enabled from '@/lib/oauth/enabled';
|
||||
import { discordAuth } from '@/lib/oauth/providerUtil';
|
||||
import { fetchToDataURL } from '@/lib/base64';
|
||||
import Logger from '@/lib/logger';
|
||||
import { encrypt } from '@/lib/crypto';
|
||||
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
async function handler({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
async function discordOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauthRegistration)
|
||||
return {
|
||||
error: 'OAuth registration is disabled.',
|
||||
|
@ -72,6 +71,8 @@ async function handler({ code, host, state }: OAuthQuery, logger: Logger): Promi
|
|||
const userJson = await discordAuth.user(json.access_token);
|
||||
if (!userJson) return { error: 'Failed to fetch user' };
|
||||
|
||||
logger.debug('user', { '@me': userJson });
|
||||
|
||||
const avatar = userJson.avatar
|
||||
? `https://cdn.discordapp.com/avatars/${userJson.id}/${userJson.avatar}.png`
|
||||
: `https://cdn.discordapp.com/embed/avatars/${userJson.discriminator % 5}.png`;
|
||||
|
@ -85,4 +86,14 @@ async function handler({ code, host, state }: OAuthQuery, logger: Logger): Promi
|
|||
};
|
||||
}
|
||||
|
||||
export default combine([method(['GET'])], withOAuth('DISCORD', handler));
|
||||
export const PATH = '/api/auth/oauth/discord';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, async (req, res) => {
|
||||
return req.oauthHandle(res, 'DISCORD', discordOauth);
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
27
src/pages/api/auth/oauth/github.ts → src/server/routes/api/auth/oauth/github.ts
Executable file → Normal file
27
src/pages/api/auth/oauth/github.ts → src/server/routes/api/auth/oauth/github.ts
Executable file → Normal file
|
@ -2,13 +2,12 @@ import { fetchToDataURL } from '@/lib/base64';
|
|||
import { config } from '@/lib/config';
|
||||
import { encrypt } from '@/lib/crypto';
|
||||
import Logger from '@/lib/logger';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import enabled from '@/lib/oauth/enabled';
|
||||
import { githubAuth } from '@/lib/oauth/providerUtil';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
|
||||
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
async function githubOauth({ code, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauthRegistration)
|
||||
return {
|
||||
error: 'OAuth registration is disabled.',
|
||||
|
@ -42,7 +41,7 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
|
|||
});
|
||||
|
||||
logger.debug('github oauth request', {
|
||||
body,
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
const res = await fetch('https://github.com/login/oauth/access_token', {
|
||||
|
@ -54,7 +53,9 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
|
|||
},
|
||||
});
|
||||
|
||||
if (!res.ok)
|
||||
const isJson = res.headers.get('content-type')?.startsWith('application/json');
|
||||
|
||||
if (!isJson && !res.ok)
|
||||
return {
|
||||
error: 'Failed to fetch access token',
|
||||
};
|
||||
|
@ -75,6 +76,8 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
|
|||
const userJson = await githubAuth.user(json.access_token);
|
||||
if (!userJson) return { error: 'Failed to fetch user' };
|
||||
|
||||
logger.debug('user', { user: userJson });
|
||||
|
||||
return {
|
||||
access_token: json.access_token,
|
||||
refresh_token: json.refresh_token,
|
||||
|
@ -84,4 +87,14 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
|
|||
};
|
||||
}
|
||||
|
||||
export default combine([method(['GET'])], withOAuth('GITHUB', handler));
|
||||
export const PATH = '/api/auth/oauth/github';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, async (req, res) => {
|
||||
return req.oauthHandle(res, 'GITHUB', githubOauth);
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
23
src/pages/api/auth/oauth/google.ts → src/server/routes/api/auth/oauth/google.ts
Executable file → Normal file
23
src/pages/api/auth/oauth/google.ts → src/server/routes/api/auth/oauth/google.ts
Executable file → Normal file
|
@ -2,13 +2,12 @@ import { fetchToDataURL } from '@/lib/base64';
|
|||
import { config } from '@/lib/config';
|
||||
import { encrypt } from '@/lib/crypto';
|
||||
import Logger from '@/lib/logger';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import enabled from '@/lib/oauth/enabled';
|
||||
import { googleAuth } from '@/lib/oauth/providerUtil';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
|
||||
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
async function handler({ code, host, state }: OAuthQuery, _logger: Logger): Promise<OAuthResponse> {
|
||||
async function googleOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauthRegistration)
|
||||
return {
|
||||
error: 'OAuth registration is disabled.',
|
||||
|
@ -47,6 +46,8 @@ async function handler({ code, host, state }: OAuthQuery, _logger: Logger): Prom
|
|||
access_type: 'offline',
|
||||
});
|
||||
|
||||
logger.debug('google oauth request', { body: body.toString() });
|
||||
|
||||
const res = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
body,
|
||||
|
@ -66,6 +67,8 @@ async function handler({ code, host, state }: OAuthQuery, _logger: Logger): Prom
|
|||
const userJson = await googleAuth.user(json.access_token);
|
||||
if (!userJson) return { error: 'Failed to fetch user' };
|
||||
|
||||
logger.debug('user', { userinfo: userJson });
|
||||
|
||||
return {
|
||||
access_token: json.access_token,
|
||||
refresh_token: json.refresh_token,
|
||||
|
@ -75,4 +78,14 @@ async function handler({ code, host, state }: OAuthQuery, _logger: Logger): Prom
|
|||
};
|
||||
}
|
||||
|
||||
export default combine([method(['GET'])], withOAuth('GOOGLE', handler));
|
||||
export const PATH = '/api/auth/oauth/google';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, async (req, res) => {
|
||||
return req.oauthHandle(res, 'GOOGLE', googleOauth);
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
63
src/server/routes/api/auth/oauth/index.ts
Normal file
63
src/server/routes/api/auth/oauth/index.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { log } from '@/lib/logger';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { OAuthProvider, OAuthProviderType } from '@prisma/client';
|
||||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import { prisma } from '@/lib/db';
|
||||
|
||||
export type ApiAuthOauthResponse = OAuthProvider[];
|
||||
|
||||
type Body = {
|
||||
provider?: OAuthProviderType;
|
||||
};
|
||||
|
||||
const logger = log('api').c('auth').c('oauth');
|
||||
|
||||
export const PATH = '/api/auth/oauth/oidc';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
return res.send(req.user.oauthProviders);
|
||||
});
|
||||
|
||||
server.delete<{ Body: Body }>(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||
const { password } = (await prisma.user.findFirst({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
select: {
|
||||
password: true,
|
||||
},
|
||||
}))!;
|
||||
|
||||
if (!req.user.oauthProviders.length) return res.badRequest('No providers to delete');
|
||||
if (req.user.oauthProviders.length === 1 && !password)
|
||||
return res.badRequest("You can't delete your last oauth provider without a password");
|
||||
|
||||
const { provider } = req.body;
|
||||
if (!provider) return res.badRequest('Provider is required');
|
||||
|
||||
const providers = await prisma.user.update({
|
||||
where: {
|
||||
id: req.user.id,
|
||||
},
|
||||
data: {
|
||||
oauthProviders: {
|
||||
deleteMany: [{ provider }],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
oauthProviders: true,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`${req.user.username} unlinked an oauth provider`, {
|
||||
provider,
|
||||
});
|
||||
|
||||
return res.send(providers.oauthProviders);
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
|
@ -1,14 +1,13 @@
|
|||
import { fetchToDataURL } from '@/lib/base64';
|
||||
import { config } from '@/lib/config';
|
||||
import { encrypt } from '@/lib/crypto';
|
||||
import Logger from '@/lib/logger';
|
||||
import { combine } from '@/lib/middleware/combine';
|
||||
import { method } from '@/lib/middleware/method';
|
||||
import enabled from '@/lib/oauth/enabled';
|
||||
import { oidcAuth } from '@/lib/oauth/providerUtil';
|
||||
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
|
||||
import { OAuthQuery, OAuthResponse } from '@/server/plugins/oauth';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
|
||||
// thanks to @danejur for this https://github.com/diced/zipline/pull/372
|
||||
async function handler({ code, host, state }: OAuthQuery, _logger: Logger): Promise<OAuthResponse> {
|
||||
async function oidcOauth({ code, host, state }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
|
||||
if (!config.features.oauthRegistration)
|
||||
return {
|
||||
error: 'OAuth registration is disabled.',
|
||||
|
@ -48,6 +47,10 @@ async function handler({ code, host, state }: OAuthQuery, _logger: Logger): Prom
|
|||
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}/api/auth/oauth/oidc`,
|
||||
});
|
||||
|
||||
logger.debug('oidc oauth request', {
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
const res = await fetch(config.oauth.oidc.tokenUrl!, {
|
||||
method: 'POST',
|
||||
body,
|
||||
|
@ -67,12 +70,27 @@ async function handler({ code, host, state }: OAuthQuery, _logger: Logger): Prom
|
|||
const userJson = await oidcAuth.user(json.access_token, config.oauth.oidc.userinfoUrl!);
|
||||
if (!userJson) return { error: 'Failed to fetch user' };
|
||||
|
||||
logger.debug('user', { userinfo: userJson });
|
||||
|
||||
return {
|
||||
access_token: json.access_token,
|
||||
refresh_token: json.refresh_token || null,
|
||||
username: userJson.preferred_username,
|
||||
// many different properties, so we are just gonna go down the list
|
||||
username:
|
||||
userJson.preferred_username ?? userJson.name ?? userJson.given_name ?? userJson.email ?? userJson.sub,
|
||||
user_id: userJson.sub,
|
||||
avatar: await fetchToDataURL(userJson.picture ?? null),
|
||||
};
|
||||
}
|
||||
|
||||
export default combine([method(['GET'])], withOAuth('OIDC', handler));
|
||||
export const PATH = '/api/auth/oauth/oidc';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, async (req, res) => {
|
||||
return req.oauthHandle(res, 'OIDC', oidcOauth);
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
|
@ -8,7 +8,7 @@ import { administratorMiddleware } from '@/server/middleware/administrator';
|
|||
import { userMiddleware } from '@/server/middleware/user';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { statSync } from 'fs';
|
||||
import ms from 'ms';
|
||||
import ms, { StringValue } from 'ms';
|
||||
import { cpus } from 'os';
|
||||
import { resolve } from 'path';
|
||||
import { z } from 'zod';
|
||||
|
@ -20,7 +20,7 @@ type Body = Partial<Settings>;
|
|||
|
||||
const reservedRoutes = ['/dashboard', '/api', '/raw', '/robots.txt', '/manifest.json', '/favicon.ico'];
|
||||
|
||||
const zMs = z.string().refine((value) => ms(value) > 0, 'Value must be greater than 0');
|
||||
const zMs = z.string().refine((value) => ms(value as StringValue) > 0, 'Value must be greater than 0');
|
||||
const zBytes = z.string().refine((value) => bytes(value) > 0, 'Value must be greater than 0');
|
||||
|
||||
const discordEmbed = z
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { config } from '@/lib/config';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { randomCharacters } from '@/lib/random';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
|
@ -54,7 +53,7 @@ export async function getSession(
|
|||
|
||||
export async function saveSession(
|
||||
session: Awaited<ReturnType<typeof getSession>>,
|
||||
user: User,
|
||||
user: { id: string } & Record<string, any>,
|
||||
overwriteSessions = true,
|
||||
) {
|
||||
session.id = user.id;
|
||||
|
|
Loading…
Add table
Reference in a new issue