diff --git a/README.md b/README.md index 132d9f2..371f2fa 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,16 @@ If you want to contribute, need support, or want to stay updated, you can join t In both `server` and `client`, run `yarn` (if you need yarn, you can download it [here](https://yarnpkg.com/).) You can run `yarn dev` in either / both folders to start the server and client with file watching / live reloading. +To migrate the sqlite database in development, you can use `yarn migrate` to see a list of options. + ### Production `yarn build` in both `client/` and `server/` will produce production code for the client and server respectively. The client and server each also have Dockerfiles which you can use with a docker-compose (an example compose will be provided in the near future). If you're deploying the front-end to something like Vercel, you'll need to set the root folder to `client/`. +In production the sqlite database will be automatically migrated to the latest version. + ### Environment Variables You can change these to your liking. diff --git a/client/components/admin/index.tsx b/client/components/admin/index.tsx index 2fa6853..55158e0 100644 --- a/client/components/admin/index.tsx +++ b/client/components/admin/index.tsx @@ -23,7 +23,7 @@ const Admin = () => { if (posts) { // sum the sizes of each file per post const sizes = posts.reduce((acc, post) => { - const size = post.files.reduce((acc, file) => acc + file.html.length, 0) + const size = post.files?.reduce((acc, file) => acc + file.html.length, 0) || 0 return { ...acc, [post.id]: byteToMB(size) } }, {}) setPostSizes(sizes) diff --git a/client/components/admin/post-modal-link.tsx b/client/components/admin/post-modal-link.tsx index e1d3a5b..6e78b14 100644 --- a/client/components/admin/post-modal-link.tsx +++ b/client/components/admin/post-modal-link.tsx @@ -30,7 +30,7 @@ const PostModal = ({ id }: { {post.title} Click an item to expand - {post.files.map((file) => ( + {post.files?.map((file) => (
diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx index cd92cad..051119e 100644 --- a/client/components/post-list/list-item.tsx +++ b/client/components/post-list/list-item.tsx @@ -39,7 +39,7 @@ const ListItem = ({ post, isOwner = true, deletePost }: { post: Post, isOwner?: - {post.files.length === 1 ? "1 file" : `${post.files.length} files`} + {post.files?.length === 1 ? "1 file" : `${post.files?.length || 0} files`} @@ -49,7 +49,7 @@ const ListItem = ({ post, isOwner = true, deletePost }: { post: Post, isOwner?: - {post.files.map((file: File) => { + {post.files?.map((file: File) => { return
{file.title || 'Untitled file'} diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx index bf05c5a..2709dbb 100644 --- a/client/components/post-page/index.tsx +++ b/client/components/post-page/index.tsx @@ -6,9 +6,8 @@ import styles from './post-page.module.css' import homeStyles from '@styles/Home.module.css' import type { File, Post } from "@lib/types" -import { Page, Button, Text, Badge, Tooltip, Spacer, ButtonDropdown, ButtonGroup, useMediaQuery } from "@geist-ui/core" -import { useCallback, useEffect, useMemo, useState } from "react" -import { timeAgo, timeUntil } from "@lib/time-ago" +import { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core" +import { useEffect, useState } from "react" import Archive from '@geist-ui/icons/archive' import FileDropdown from "@components/file-dropdown" import ScrollToTop from "@components/scroll-to-top" @@ -51,6 +50,7 @@ const PostPage = ({ post }: Props) => { const download = async () => { + if (!post.files) return const downloadZip = (await import("client-zip")).downloadZip const blob = await downloadZip(post.files.map((file: any) => { return { @@ -102,12 +102,12 @@ const PostPage = ({ post }: Props) => { - +
{/* {post.files.length > 1 && } */} - {post.files.map(({ id, content, title }: File) => ( + {post.files?.map(({ id, content, title }: File) => ( { - // Checks migrations and run them if they are not already applied. To keep - // track of the executed migrations, a table (and sequelize model) called SequelizeMeta - // will be automatically created (if it doesn't exist already) and parsed. - console.log("Checking migrations...") - const migrations = await umzug.up() - if (migrations.length > 0) { - console.log("Migrations applied:") - console.log(migrations) - } else { - console.log("No migrations applied.") - } -})() + +// If you're in a development environment, you can manually migrate with `yarn migrate:{up,down}` in the `server` folder +if (config.is_production) { + ;(async () => { + // Checks migrations and run them if they are not already applied. To keep + // track of the executed migrations, a table (and sequelize model) called SequelizeMeta + // will be automatically created (if it doesn't exist already) and parsed. + console.log("Checking migrations...") + const migrations = await umzug.up() + if (migrations.length > 0) { + console.log("Migrations applied:") + console.log(migrations) + } else { + console.log("No migrations applied.") + } + })() +} diff --git a/server/src/lib/config.ts b/server/src/lib/config.ts index 09fe7cc..2330cae 100644 --- a/server/src/lib/config.ts +++ b/server/src/lib/config.ts @@ -1,5 +1,55 @@ -export default { - port: process.env.PORT || 3000, - jwt_secret: process.env.JWT_SECRET || "myjwtsecret", - drift_home: process.env.DRIFT_HOME || "~/.drift" +type Config = { + port: number + jwt_secret: string + drift_home: string + is_production: boolean + memory_db: boolean + enable_admin: boolean + secret_key: string + registration_password: string } + +const config = (): Config => { + const stringToBoolean = (str: string | undefined): boolean => { + if (str === "true") { + return true + } else if (str === "false") { + return false + } else { + throw new Error(`Invalid boolean value: ${str}`) + } + } + + const throwIfUndefined = (str: string | undefined): string => { + if (str === undefined) { + throw new Error(`Missing environment variable: ${str}`) + } + return str + } + + const validNodeEnvs = (str: string | undefined) => { + const valid = ["development", "production"] + if (str && !valid.includes(str)) { + throw new Error(`Invalid environment variable: ${str}`) + } else if (!str) { + console.warn("No NODE_ENV specified, defaulting to development") + } else { + console.log(`Using NODE_ENV: ${str}`) + } + } + validNodeEnvs(process.env.NODE_ENV) + + const config: Config = { + port: process.env.PORT ? parseInt(process.env.PORT) : 3000, + jwt_secret: process.env.JWT_SECRET || "myjwtsecret", + drift_home: process.env.DRIFT_HOME || "~/.drift", + is_production: process.env.NODE_ENV === "production", + memory_db: stringToBoolean(process.env.MEMORY_DB), + enable_admin: stringToBoolean(process.env.ENABLE_ADMIN), + secret_key: throwIfUndefined(process.env.SECRET_KEY), + registration_password: process.env.REGISTRATION_PASSWORD || "" + } + return config +} + +export default config() diff --git a/server/src/lib/middleware/is-admin.ts b/server/src/lib/middleware/is-admin.ts index e2bcc09..81efc7c 100644 --- a/server/src/lib/middleware/is-admin.ts +++ b/server/src/lib/middleware/is-admin.ts @@ -19,7 +19,7 @@ export default function authenticateToken( const authHeader = req.headers["authorization"] const token = authHeader && authHeader.split(" ")[1] if (token == null) return res.sendStatus(401) - if (!process.env.ENABLE_ADMIN) return res.sendStatus(404) + if (!config.enable_admin) return res.sendStatus(404) jwt.verify(token, config.jwt_secret, async (err: any, user: any) => { if (err) return res.sendStatus(403) diff --git a/server/src/lib/middleware/secret-key.ts b/server/src/lib/middleware/secret-key.ts index aa912c9..1cc5f78 100644 --- a/server/src/lib/middleware/secret-key.ts +++ b/server/src/lib/middleware/secret-key.ts @@ -1,17 +1,13 @@ +import config from "@lib/config" import { NextFunction, Request, Response } from "express" -const key = process.env.SECRET_KEY -if (!key) { - throw new Error("SECRET_KEY is not set.") -} - export default function authenticateToken( req: Request, res: Response, next: NextFunction ) { const requestKey = req.headers["x-secret-key"] - if (requestKey !== key) { + if (requestKey !== config.secret_key) { return res.sendStatus(401) } next() diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index 8e92187..559e3e0 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -95,7 +95,15 @@ admin.get("/post/:id", async (req, res, next) => { admin.delete("/post/:id", async (req, res, next) => { try { - const post = await Post.findByPk(req.params.id) + const post = await Post.findByPk(req.params.id, { + include: [ + { + model: File, + as: "files" + } + ] + }) + if (!post) { return res.status(404).json({ message: "Post not found" diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 2c9f02f..1ac9666 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -8,10 +8,10 @@ import { celebrate, Joi } from "celebrate" const NO_EMPTY_SPACE_REGEX = /^\S*$/ +// we require a server password if the password is set and we're in production export const requiresServerPassword = - (process.env.MEMORY_DB || process.env.NODE_ENV === "production") && - !!process.env.REGISTRATION_PASSWORD -console.log(`Registration password required: ${requiresServerPassword}`) + config.registration_password.length > 0 && config.is_production +if (requiresServerPassword) console.log(`Registration password enabled.`) export const auth = Router() @@ -25,10 +25,7 @@ const validateAuthPayload = ( } if (requiresServerPassword) { - if ( - !serverPassword || - process.env.REGISTRATION_PASSWORD !== serverPassword - ) { + if (!serverPassword || config.registration_password !== serverPassword) { throw new Error( "Server password is incorrect. Please contact the server administrator." ) @@ -68,10 +65,7 @@ auth.post( const user = { username: username as string, password: await hash(req.body.password, salt), - role: - !!process.env.MEMORY_DB && process.env.ENABLE_ADMIN && count === 0 - ? "admin" - : "user" + role: config.enable_admin && count === 0 ? "admin" : "user" } const created_user = await User.create(user) diff --git a/server/src/routes/posts.ts b/server/src/routes/posts.ts index 1b6d761..a4959aa 100644 --- a/server/src/routes/posts.ts +++ b/server/src/routes/posts.ts @@ -38,7 +38,7 @@ posts.post( userId: Joi.string().required(), password: Joi.string().optional(), // expiresAt, allow to be null - expiresAt: Joi.date().optional().allow(null, '') + expiresAt: Joi.date().optional().allow(null, "") } }), async (req, res, next) => {