diff --git a/README.md b/README.md index 4f65c21..87b4e7b 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,8 @@ Fast & lightweight file uploading. - Configurable - Fast - Built with Next.js & React -- Support for **multible database types** - Token protected uploading -- Easy setup instructions on [docs](https://zipline.diced.me) +- Easy setup instructions on [docs](https://zipline.diced.me) (One command install `docker-compose up`) # Installing diff --git a/package.json b/package.json index 7aa54c8..be2072c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zip3", - "version": "3.1.0", + "version": "3.2.0", "scripts": { "prepare": "husky install", "dev": "NODE_ENV=development node server", diff --git a/prisma/migrations/20210826034827_custom_themes/migration.sql b/prisma/migrations/20210826034827_custom_themes/migration.sql new file mode 100644 index 0000000..6d1b708 --- /dev/null +++ b/prisma/migrations/20210826034827_custom_themes/migration.sql @@ -0,0 +1,25 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "systemTheme" TEXT NOT NULL DEFAULT E'dark_blue'; + +-- CreateTable +CREATE TABLE "Theme" ( + "id" SERIAL NOT NULL, + "type" TEXT NOT NULL, + "primary" TEXT NOT NULL, + "secondary" TEXT NOT NULL, + "error" TEXT NOT NULL, + "warning" TEXT NOT NULL, + "info" TEXT NOT NULL, + "border" TEXT NOT NULL, + "mainBackground" TEXT NOT NULL, + "paperBackground" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + + PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Theme_userId_unique" ON "Theme"("userId"); + +-- AddForeignKey +ALTER TABLE "Theme" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 147caec..9d1ae16 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,12 +13,29 @@ model User { password String token String administrator Boolean @default(false) + systemTheme String @default("dark_blue") + customTheme Theme? embedTitle String? embedColor String @default("#2f3136") images Image[] urls Url[] } +model Theme { + id Int @id @default(autoincrement()) + type String + primary String + secondary String + error String + warning String + info String + border String + mainBackground String + paperBackground String + user User @relation(fields: [userId], references: [id]) + userId Int +} + model Image { id Int @id @default(autoincrement()) file String @@ -31,25 +48,25 @@ model Image { } model InvisibleImage { - id Int - image Image @relation(fields: [id], references: [id]) + id Int + image Image @relation(fields: [id], references: [id]) - invis String @unique + invis String @unique } model Url { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) to String - created_at DateTime @default(now()) - views Int @default(0) + created_at DateTime @default(now()) + views Int @default(0) invisible InvisibleUrl? - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id]) userId Int } model InvisibleUrl { - id Int - url Url @relation(fields: [id], references: [id]) + id Int + url Url @relation(fields: [id], references: [id]) - invis String @unique + invis String @unique } diff --git a/scripts/deploy-db.js b/scripts/deploy-db.js index 5336b1d..da31b55 100644 --- a/scripts/deploy-db.js +++ b/scripts/deploy-db.js @@ -3,9 +3,8 @@ const prismaRun = require('./prisma-run'); module.exports = async (config) => { try { - await prismaRun(config.database.url, ['migrate', 'deploy', '--schema=prisma/schema.prisma']); - await prismaRun(config.database.url, ['generate', '--schema=prisma/schema.prisma']); - await prismaRun(config.database.url, ['db', 'seed', '--preview-feature', '--schema=prisma/schema.prisma']); + await prismaRun(config.database.url, ['migrate', 'deploy']); + await prismaRun(config.database.url, ['generate']); } catch (e) { console.log(e); Logger.get('db').error('there was an error.. exiting..'); diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 1d7457e..a62c72b 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -23,6 +23,7 @@ import { DialogContent, DialogContentText, DialogTitle, + Select, } from '@material-ui/core'; import { Menu as MenuIcon, @@ -33,10 +34,15 @@ import { ContentCopy as CopyIcon, Autorenew as ResetIcon, Logout as LogoutIcon, - PeopleAlt as UsersIcon + PeopleAlt as UsersIcon, + Brush as BrushIcon } from '@material-ui/icons'; import copy from 'copy-to-clipboard'; import Backdrop from './Backdrop'; +import { friendlyThemeName, themes } from './Theming'; +import { useRouter } from 'next/router'; +import { useStoreDispatch } from 'lib/redux/store'; +import { updateUser } from 'lib/redux/reducers/user'; const items = [ { @@ -122,11 +128,14 @@ function ResetTokenDialog({ open, setOpen, setToken }) { } export default function Layout({ children, user, loading, noPaper }) { + const [systemTheme, setSystemTheme] = useState(user.systemTheme || 'dark_blue'); const [mobileOpen, setMobileOpen] = useState(false); const [anchorEl, setAnchorEl] = useState(null); const [copyOpen, setCopyOpen] = useState(false); const [resetOpen, setResetOpen] = useState(false); const [token, setToken] = useState(user?.token); + const router = useRouter(); + const dispatch = useStoreDispatch(); const open = Boolean(anchorEl); const handleClick = e => setAnchorEl(e.currentTarget); @@ -142,6 +151,17 @@ export default function Layout({ children, user, loading, noPaper }) { setAnchorEl(null); }; + const handleUpdateTheme = async (event: React.ChangeEvent<{ value: string }>) => { + const newUser = await useFetch('/api/user', 'PATCH', { + systemTheme: event.target.value || 'dark_blue' + }); + + setSystemTheme(newUser.systemTheme); + dispatch(updateUser(newUser)); + + router.replace(router.pathname); + }; + const drawer = (
@@ -221,6 +241,22 @@ export default function Layout({ children, user, loading, noPaper }) { + + + + )} diff --git a/src/components/Theming.tsx b/src/components/Theming.tsx new file mode 100644 index 0000000..ac68710 --- /dev/null +++ b/src/components/Theming.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { ThemeProvider } from '@emotion/react'; +import { CssBaseline } from '@material-ui/core'; +import dark_blue from 'lib/themes/dark_blue'; +import dark from 'lib/themes/dark'; +import ayu_dark from 'lib/themes/ayu_dark'; +import { useStoreSelector } from 'lib/redux/store'; +import createTheme from 'lib/themes'; + +export const themes = { + 'dark_blue': dark_blue, + 'dark': dark, + 'ayu_dark': ayu_dark +}; + +export const friendlyThemeName = { + 'dark_blue': 'Dark Blue', + 'dark': 'Very Dark', + 'ayu_dark': 'Ayu Dark' +}; + +export default function ZiplineTheming({ Component, pageProps }) { + let t; + + const user = useStoreSelector(state => state.user); + if (!user) t = themes.dark_blue; + else { + if (user.customTheme) { + t = createTheme({ + type: 'dark', + primary: user.customTheme.primary, + secondary: user.customTheme.secondary, + error: user.customTheme.error, + warning: user.customTheme.warning, + info: user.customTheme.info, + border: user.customTheme.border, + background: { + main: user.customTheme.mainBackground, + paper: user.customTheme.paperBackground + } + }); + } else { + t = themes[user.systemTheme] ?? themes.dark_blue; + } + } + + return ( + + + + + ); +} \ No newline at end of file diff --git a/src/components/pages/Dashboard.tsx b/src/components/pages/Dashboard.tsx index a30c172..a8b1b2b 100644 --- a/src/components/pages/Dashboard.tsx +++ b/src/components/pages/Dashboard.tsx @@ -24,6 +24,7 @@ type Aligns = 'inherit' | 'right' | 'left' | 'center' | 'justify'; export function bytesToRead(bytes: number) { if (isNaN(bytes)) return '0.0 B'; + if (bytes === Infinity) return '0.0 B'; const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']; let num = 0; @@ -96,7 +97,7 @@ export default function Dashboard() { const imgs = await useFetch('/api/user/images'); const stts = await useFetch('/api/stats'); setImages(imgs); - setStats(stts); + setStats(stts);console.log(stts); setApiLoading(false); }; diff --git a/src/components/pages/Manage.tsx b/src/components/pages/Manage.tsx index 64cf033..e20b263 100644 --- a/src/components/pages/Manage.tsx +++ b/src/components/pages/Manage.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { TextField, Button, Box, Typography } from '@material-ui/core'; +import { TextField, Button, Box, Typography, Select, MenuItem } from '@material-ui/core'; import { useFormik } from 'formik'; import * as yup from 'yup'; @@ -8,6 +8,7 @@ import Backdrop from 'components/Backdrop'; import Alert from 'components/Alert'; import { useStoreDispatch, useStoreSelector } from 'lib/redux/store'; import { updateUser } from 'lib/redux/reducers/user'; +import { useRouter } from 'next/router'; const validationSchema = yup.object({ username: yup @@ -15,6 +16,45 @@ const validationSchema = yup.object({ .required('Username is required') }); +const themeValidationSchema = yup.object({ + type: yup + .string() + .required('Type (dark, light) is required is required'), + primary: yup + .string() + .required('Primary color is required') + .matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }), + secondary: yup + .string() + .required('Secondary color is required') + .matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }), + error: yup + .string() + .required('Error color is required') + .matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }), + warning: yup + .string() + .required('Warning color is required') + .matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }), + info: yup + .string() + .required('Info color is required') + .matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }), + border: yup + .string() + .required('Border color is required') + .matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }), + mainBackground: yup + .string() + .required('Main Background is required') + .matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }), + paperBackground: yup + .string() + .required('Paper Background is required') + .matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }), + +}); + function TextInput({ id, label, formik, ...other }) { return ( state.user); const dispatch = useStoreDispatch(); + const router = useRouter(); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); @@ -84,6 +125,41 @@ export default function Manage() { } }); + const customThemeFormik = useFormik({ + initialValues: { + type: user.customTheme?.type || 'dark', + primary: user.customTheme?.primary || '', + secondary: user.customTheme?.secondary || '', + error: user.customTheme?.error || '', + warning: user.customTheme?.warning || '', + info: user.customTheme?.info || '', + border: user.customTheme?.border || '', + mainBackground: user.customTheme?.mainBackground || '', + paperBackground: user.customTheme?.paperBackground || '', + }, + validationSchema: themeValidationSchema, + onSubmit: async values => { + setLoading(true); + + const newUser = await useFetch('/api/user', 'PATCH', { customTheme: values }); + console.log(newUser); + + if (newUser.error) { + setLoading(false); + setMessage('An error occured'); + setSeverity('error'); + setOpen(true); + } else { + dispatch(updateUser(newUser)); + router.replace(router.pathname); + setLoading(false); + setMessage('Saved theme'); + setSeverity('success'); + setOpen(true); + } + } + }); + return ( <> @@ -104,7 +180,42 @@ export default function Manage() { + >Save User + + + Manage Theme +
+ + + + + + + + + + + diff --git a/src/lib/middleware/withZipline.ts b/src/lib/middleware/withZipline.ts index 62a7a23..25e8f12 100644 --- a/src/lib/middleware/withZipline.ts +++ b/src/lib/middleware/withZipline.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import type { CookieSerializeOptions } from 'cookie'; -import type { User } from '@prisma/client'; +import type { Image, Theme, User } from '@prisma/client'; import { serialize } from 'cookie'; import { sign64, unsign64 } from '../util'; @@ -17,7 +17,18 @@ export interface NextApiFile { } export type NextApiReq = NextApiRequest & { - user: () => Promise; + user: () => Promise<{ + username: string; + token: string; + embedTitle: string; + embedColor: string; + systemTheme: string; + customTheme?: Theme; + administrator: boolean; + id: number; + images: Image[]; + password: string; + } | null | void>; getCookie: (name: string) => string | null; cleanCookie: (name: string) => void; file?: NextApiFile; @@ -83,6 +94,18 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse) const user = await prisma.user.findFirst({ where: { id: Number(userId) + }, + select: { + administrator: true, + embedColor: true, + embedTitle: true, + id: true, + images: true, + password: true, + systemTheme: true, + customTheme: true, + token: true, + username: true } }); diff --git a/src/lib/redux/reducers/user.ts b/src/lib/redux/reducers/user.ts index b86472c..7e2e125 100644 --- a/src/lib/redux/reducers/user.ts +++ b/src/lib/redux/reducers/user.ts @@ -1,3 +1,4 @@ +import { Theme } from '@prisma/client'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; export interface User { @@ -5,6 +6,8 @@ export interface User { token: string; embedTitle: string; embedColor: string; + systemTheme: string; + customTheme?: Theme; } const initialState: User = null; diff --git a/src/lib/themes/ayu_dark.ts b/src/lib/themes/ayu_dark.ts new file mode 100644 index 0000000..3da6216 --- /dev/null +++ b/src/lib/themes/ayu_dark.ts @@ -0,0 +1,17 @@ +// https://github.com/AlphaNecron/ +// https://github.com/ayu-theme/ayu-colors +import createTheme from '.'; + +export default createTheme({ + type: 'dark', + primary: '#E6B450', + secondary: '#FFEE99', + error: '#FF3333', + warning: '#F29668', + info: '#95E6CB', + border: '#0A0E14', + background: { + main: '#0D1016', + paper: '#0A0E14' + } +}); \ No newline at end of file diff --git a/src/lib/themes/dark.ts b/src/lib/themes/dark.ts new file mode 100644 index 0000000..e7a2323 --- /dev/null +++ b/src/lib/themes/dark.ts @@ -0,0 +1,15 @@ +import createTheme from '.'; + +export default createTheme({ + type: 'dark', + primary: '#2c39a6', + secondary: '#7344e2', + error: '#ff4141', + warning: '#ff9800', + info: '#2f6fb9', + border: '#2b2b2b', + background: { + main: '#000000', + paper: '#060606' + } +}); \ No newline at end of file diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 2a994fd..3b5b46b 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -2,9 +2,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; import Head from 'next/head'; -import { ThemeProvider } from '@material-ui/core/styles'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import theme from 'lib/themes/dark_blue'; +import Theming from 'components/Theming'; import { useStore } from 'lib/redux/store'; export default function MyApp({ Component, pageProps }) { @@ -22,10 +20,10 @@ export default function MyApp({ Component, pageProps }) { - - - - + ); } diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts index 5c46199..a017e25 100644 --- a/src/pages/api/auth/login.ts +++ b/src/pages/api/auth/login.ts @@ -2,11 +2,18 @@ import prisma from 'lib/prisma'; import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; import { checkPassword } from 'lib/util'; import Logger from 'lib/logger'; +import prismaRun from '../../../../scripts/prisma-run'; +import config from 'lib/config'; async function handler(req: NextApiReq, res: NextApiRes) { if (req.method !== 'POST') return res.status(405).end(); const { username, password } = req.body as { username: string, password: string }; + const users = await prisma.user.findMany(); + if (users.length === 0) { + await prismaRun(config.database.url, ['db', 'seed', '--preview-feature']); + } + const user = await prisma.user.findFirst({ where: { username diff --git a/src/pages/api/user/images.ts b/src/pages/api/user/images.ts index e1ed881..665ba46 100644 --- a/src/pages/api/user/images.ts +++ b/src/pages/api/user/images.ts @@ -27,7 +27,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { } else { const images = await prisma.image.findMany({ where: { - user + userId: user.id }, select: { created_at: true, diff --git a/src/pages/api/user/index.ts b/src/pages/api/user/index.ts index ea87a71..57a361c 100644 --- a/src/pages/api/user/index.ts +++ b/src/pages/api/user/index.ts @@ -31,14 +31,49 @@ async function handler(req: NextApiReq, res: NextApiRes) { data: { embedColor: req.body.embedColor } }); + if (req.body.systemTheme) await prisma.user.update({ + where: { id: user.id }, + data: { systemTheme: req.body.systemTheme } + }); + + if (req.body.customTheme) { + if (user.customTheme) await prisma.user.update({ + where: { id: user.id }, + data: { + customTheme: { + update: { + ...req.body.customTheme + } + } + } + }); else await prisma.theme.create({ + data: { + userId: user.id, + ...req.body.customTheme + } + }); + } + const newUser = await prisma.user.findFirst({ - where: { id: user.id } + where: { + id: Number(user.id) + }, + select: { + administrator: true, + embedColor: true, + embedTitle: true, + id: true, + images: false, + password: false, + systemTheme: true, + customTheme: true, + token: true, + username: true + } }); Logger.get('user').info(`User ${user.username} (${newUser.username}) (${newUser.id}) was updated`); - delete newUser.password; - return res.json(newUser); } else { delete user.password;