commit 5926ecff4a782fdb9b2561e4e669144fc3183871 Author: AlphaNecron Date: Thu Sep 16 12:40:46 2021 +0700 feat: initial diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..86db7fe --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + 'extends': ['next', 'next/core-web-vitals'], + 'rules': { + 'indent': ['error', 2], + 'linebreak-style': ['error', 'unix'], + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'], + 'jsx-quotes': ['error', 'prefer-single'], + 'react/prop-types': 'off', + 'react-hooks/rules-of-hooks': 'off', + 'react-hooks/exhaustive-deps': 'off', + 'react/jsx-uses-react': 'warn', + 'react/jsx-uses-vars': 'warn', + 'react/no-danger-with-children': 'warn', + 'react/no-deprecated': 'warn', + 'react/no-direct-mutation-state': 'warn', + 'react/no-is-mounted': 'warn', + 'react/no-typos': 'error', + 'react/react-in-jsx-scope': 'error', + 'react/require-render-return': 'error', + 'react/style-prop-object': 'warn', + '@next/next/no-img-element': 'off' + } +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8de2b46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# lock files +yarn.lock +package.lock.json + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +# axtral +config.toml +/uploads diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..d71a03b --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn commitlint --edit $1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b12f3e3 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/axtral-env.d.ts b/axtral-env.d.ts new file mode 100644 index 0000000..59c1da8 --- /dev/null +++ b/axtral-env.d.ts @@ -0,0 +1,11 @@ +import type { PrismaClient } from '@prisma/client'; +import type { Config } from './src/lib/types'; + +declare global { + namespace NodeJS { + interface Global { + prisma: PrismaClient; + config: Config; + } + } +} \ No newline at end of file diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..9bc3dd4 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..14a1ef5 --- /dev/null +++ b/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + reactStrictMode: true +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6202217 --- /dev/null +++ b/package.json @@ -0,0 +1,149 @@ +{ + "name": "axtral", + "version": "0.1.0", + "private": true, + "engines": { + "node": ">=16" + }, + "scripts": { + "dev": "NODE_ENV=development node server", + "build": "npm-run-all build:schema build:next", + "build:next": "next build", + "build:schema": "prisma generate --schema=prisma/schema.prisma", + "start": "node server", + "prepare": "husky install", + "lint": "next lint --fix" + }, + "prisma": { + "seed": "ts-node --compiler-options {\"module\":\"commonjs\"} --transpile-only prisma/seed.ts" + }, + "dependencies": { + "@chakra-ui/react": "^1.6.7", + "@emotion/react": "^11", + "@emotion/styled": "^11", + "@iarna/toml": "^2.2.5", + "@prisma/client": "^3.0.2", + "@reduxjs/toolkit": "^1.6.1", + "argon2": "^0.28.2", + "cookie": "^0.4.1", + "copy-to-clipboard": "^3.3.1", + "formik": "^2.2.9", + "framer-motion": "^4", + "multer": "^1.4.3", + "next": "^11.1.2", + "prisma": "^3.0.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-dropzone": "^11.3.4", + "react-feather": "^2.0.9", + "react-redux": "^7.2.5", + "react-responsive": "^9.0.0-beta.4" + }, + "devDependencies": { + "@commitlint/cli": "^13.1.0", + "@commitlint/config-conventional": "^13.1.0", + "@types/node": "^16.7.13", + "@types/react": "^17.0.20", + "@typescript-eslint/parser": "^4.31.0", + "eslint": "7.32.0", + "eslint-config-next": "11.1.2", + "husky": "^7.0.2", + "ts-node": "^10.2.1", + "typescript": "^4.3.2", + "yarn-run-all": "^3.1.1" + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ], + "parserPreset": "conventional-changelog-conventionalcommits", + "rules": { + "body-leading-blank": [ + 1, + "always" + ], + "body-max-line-length": [ + 2, + "always", + 100 + ], + "footer-leading-blank": [ + 1, + "always" + ], + "footer-max-line-length": [ + 2, + "always", + 100 + ], + "header-max-length": [ + 2, + "always", + 100 + ], + "subject-case": [ + 2, + "never", + [ + "sentence-case", + "start-case", + "pascal-case", + "upper-case" + ] + ], + "subject-empty": [ + 2, + "never" + ], + "subject-full-stop": [ + 2, + "never", + "." + ], + "type-case": [ + 2, + "always", + "lower-case" + ], + "type-empty": [ + 2, + "never" + ], + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test" + ] + ], + "scope-enum": [ + 1, + "always", + [ + "prisma", + "scripts", + "server", + "pages", + "config", + "api", + "hooks", + "components", + "middleware", + "redux", + "lib", + "assets" + ] + ] + } + } +} \ No newline at end of file diff --git a/prisma/migrations/20210916045649_init/migration.sql b/prisma/migrations/20210916045649_init/migration.sql new file mode 100644 index 0000000..132a991 --- /dev/null +++ b/prisma/migrations/20210916045649_init/migration.sql @@ -0,0 +1,38 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL, + "token" TEXT NOT NULL, + "isAdmin" BOOLEAN NOT NULL DEFAULT false, + "embedTitle" TEXT, + "embedColor" TEXT NOT NULL DEFAULT E'#4fd1c5', + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "File" ( + "id" SERIAL NOT NULL, + "fileName" TEXT NOT NULL, + "mimetype" TEXT NOT NULL DEFAULT E'image/png', + "slug" TEXT NOT NULL, + "uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deletionToken" TEXT NOT NULL, + "views" INTEGER NOT NULL DEFAULT 0, + "userId" INTEGER NOT NULL, + + CONSTRAINT "File_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_token_key" ON "User"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "File_slug_key" ON "File"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "File_deletionToken_key" ON "File"("deletionToken"); + +-- AddForeignKey +ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..0009530 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,31 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id Int @id @default(autoincrement()) + username String + password String + token String @unique + isAdmin Boolean @default(false) + embedTitle String? + embedColor String @default("#B794F4") + files File[] +} + +model File { + id Int @id @default(autoincrement()) + fileName String + mimetype String @default("image/png") + slug String @unique + uploadedAt DateTime @default(now()) + deletionToken String @unique + views Int @default(0) + user User @relation(fields: [userId], references: [id]) + userId Int +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..a8e80a8 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,29 @@ +import { PrismaClient } from '@prisma/client'; +import { hashPassword, createToken } from '../src/lib/utils'; + +const prisma = new PrismaClient(); + +async function main() { + const user = await prisma.user.create({ + data: { + username: 'axtral', + password: await hashPassword('axtraluser'), + token: createToken(), + isAdmin: true + } + }); + console.log(` +Use these credentials when logging in Axtral for the first time: +Username: ${user.username} +Password: axtraluser +`); +} + +main() + .catch(e => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..fbf0e25 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/scripts/deployDb.js b/scripts/deployDb.js new file mode 100644 index 0000000..8864ced --- /dev/null +++ b/scripts/deployDb.js @@ -0,0 +1,13 @@ +const { error } = require('../src/lib/logger'); +const prismaRun = require('./prismaRun'); + +module.exports = async (config) => { + try { + await prismaRun(config.core.database_url, ['migrate', 'deploy']); + await prismaRun(config.core.database_url, ['generate'], true); + } catch (e) { + console.log(e); + error('DB', 'there was an error.. exiting..'); + process.exit(1); + } +}; \ No newline at end of file diff --git a/scripts/prismaRun.js b/scripts/prismaRun.js new file mode 100644 index 0000000..0ab8a71 --- /dev/null +++ b/scripts/prismaRun.js @@ -0,0 +1,26 @@ +const { spawn } = require('child_process'); +const { join } = require('path'); + +module.exports = (url, args, nostdout = false) => { + return new Promise((res, rej) => { + const proc = spawn(join(process.cwd(), 'node_modules', '.bin', 'prisma'), args, { + env: { + DATABASE_URL: url, + ...process.env + }, + }); + + let a = ''; + + proc.stdout.on('data', d => { + if (!nostdout) console.log(d.toString()); + a += d.toString(); + }); + proc.stderr.on('data', d => { + if (!nostdout) console.log(d.toString()); + rej(d.toString()); + }); + proc.stdout.on('end', () => res(a)); + proc.stdout.on('close', () => res(a)); + }); +}; \ No newline at end of file diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..a475888 --- /dev/null +++ b/server/index.js @@ -0,0 +1,98 @@ +const next = require('next'); +const { createServer } = require('http'); +const { stat, mkdir } = require('fs/promises'); +const { extname } = require('path'); +const { PrismaClient } = require('@prisma/client'); +const validateConfig = require('./validateConfig'); +const { info, error } = require('../src/lib/logger'); +const getFile = require('./static'); +const prismaRun = require('../scripts/prismaRun'); +const configReader = require('../src/lib/configReader'); +const mimes = require('../src/lib/mimetype'); +const deployDb = require('../scripts/deployDb'); + +info('SERVER', 'Starting Axtral server'); + +const dev = process.env.NODE_ENV === 'development'; + +(async () => { + try { + const config = configReader(); + await validateConfig(config); + const data = await prismaRun(config.core.database_url, ['migrate', 'status'], true); + if (data.includes('Following migration have not yet been applied:')) { + info('DB', 'Some migrations are not applied, applying them now...'); + await deployDb(config); + info('DB', 'Finished applying migrations'); + } + process.env.DATABASE_URL = config.core.database_url; + await stat('./.next'); + await mkdir(config.uploader.directory, { recursive: true }); + const app = next({ + dir: '.', + dev, + quiet: dev + }, config.core.port, config.core.host); + await app.prepare(); + const handle = app.getRequestHandler(); + const prisma = new PrismaClient(); + const srv = createServer(async (req, res) => { + if (req.url.startsWith('/raw')) { + const parts = req.url.split('/'); + if (!parts[2] || parts[2] === '') return; + const data = await getFile(config.uploader.directory, parts[2]); + if (!data) { + app.render404(req, res); + } else { + let file = await prisma.file.findFirst({ + where: { + fileName: parts[2], + } + }); + if (file) { + await prisma.file.update({ + where: { + id: file.id, + }, + data: { + views: { + increment: 1 + } + } + }); + res.setHeader('Content-Type', file.mimetype); + } else { + const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream'; + res.setHeader('Content-Type', mimetype); + } + res.end(data); + } + } else { + handle(req, res); + } + res.statusCode === 200 ? info('URL', `${res.statusCode} ${req.url}`) : error('URL', `${res.statusCode} ${req.url}`); + }); + srv.on('error', (e) => { + error('SERVER', e); + process.exit(1); + }); + srv.on('listening', async () => { + info('SERVER', `Listening on ${config.core.host}:${config.core.port}`); + if (process.platform === 'linux' && dev) execSync(`xdg-open ${config.core.secure ? 'https' : 'http'}://${config.core.host === '0.0.0.0' ? 'localhost' : config.core.host}:${config.core.port}`); + const users = await prisma.user.findMany(); + if (users.length === 0) { + await prismaRun(config.core.database_url, ['db', 'seed']); + } + }); + srv.listen(config.core.port, config.core.host ?? '0.0.0.0'); + } catch (e) { + if (e.message && e.message.startsWith('Could not find a production')) { + error('WEB', 'There is no production build - run yarn build'); + } else if (e.code && e.code === 'ENOENT') { + if (e.path === './.next') error('WEB', 'There is no production build - run yarn build'); + } else { + error('SERVER', e); + process.exit(1); + } + } +})(); diff --git a/server/static.js b/server/static.js new file mode 100644 index 0000000..32a16f5 --- /dev/null +++ b/server/static.js @@ -0,0 +1,11 @@ +const { readFile } = require('fs/promises'); +const { join } = require('path'); + +module.exports = async (dir, file) => { + try { + const data = await readFile(join(process.cwd(), dir, file)); + return data; + } catch (e) { + return null; + } +}; \ No newline at end of file diff --git a/server/validateConfig.js b/server/validateConfig.js new file mode 100644 index 0000000..3ae580e --- /dev/null +++ b/server/validateConfig.js @@ -0,0 +1,39 @@ +const { error } = require('../src/lib/logger'); + +function dot(str, obj) { + return str.split('.').reduce((a,b) => a[b], obj); +} + +const path = (path, type) => ({ path, type }); + +module.exports = async config => { + const paths = [ + path('core.secure', 'boolean'), + path('core.secret', 'string'), + path('core.host', 'string'), + path('core.port', 'number'), + path('core.database_url', 'string'), + path('uploader.length', 'number'), + path('uploader.directory', 'string'), + path('uploader.blacklisted', 'object'), + ]; + + let errors = 0; + for (let i = 0, L = paths.length; i !== L; ++i) { + const path = paths[i]; + const value = dot(path.path, config); + if (value === undefined) { + error('CONFIG', `There was no ${path.path} in config which was required`); + ++errors; + } + const type = typeof value; + if (value !== undefined && type !== path.type) { + error('CONFIG', `Expected ${path.type} on ${path.path}, but got ${type}`); + ++errors; + } + } + if (errors !== 0) { + error('CONFIG', `Exiting due to ${errors} errors`); + process.exit(1); + } +}; \ No newline at end of file diff --git a/src/components/FileViewer.tsx b/src/components/FileViewer.tsx new file mode 100644 index 0000000..32b927b --- /dev/null +++ b/src/components/FileViewer.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Heading } from '@chakra-ui/react'; + +export default function FileViewer({ src, type }) { + switch (type) { + case 'image': { + return {src}/; + } + case 'video': { + return