client/server: use config
dictionary on server, improve types on client
This commit is contained in:
parent
8da6d62cea
commit
dafc0c37f8
16 changed files with 116 additions and 57 deletions
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -30,7 +30,7 @@ const PostModal = ({ id }: {
|
|||
<Modal width={'var(--main-content)'} {...bindings}>
|
||||
<Modal.Title>{post.title}</Modal.Title>
|
||||
<Modal.Subtitle>Click an item to expand</Modal.Subtitle>
|
||||
{post.files.map((file) => (
|
||||
{post.files?.map((file) => (
|
||||
<div key={file.id} className={styles.postModal}>
|
||||
<Modal.Content>
|
||||
<details>
|
||||
|
|
|
@ -39,7 +39,7 @@ const ListItem = ({ post, isOwner = true, deletePost }: { post: Post, isOwner?:
|
|||
<CreatedAgoBadge createdAt={post.createdAt} />
|
||||
</span>
|
||||
<span style={{ marginLeft: 'var(--gap)' }}>
|
||||
<Badge type="secondary">{post.files.length === 1 ? "1 file" : `${post.files.length} files`}</Badge>
|
||||
<Badge type="secondary">{post.files?.length === 1 ? "1 file" : `${post.files?.length || 0} files`}</Badge>
|
||||
</span>
|
||||
<span style={{ marginLeft: 'var(--gap)' }}>
|
||||
<ExpirationBadge postExpirationDate={post.expiresAt} />
|
||||
|
@ -49,7 +49,7 @@ const ListItem = ({ post, isOwner = true, deletePost }: { post: Post, isOwner?:
|
|||
</Card.Body>
|
||||
<Divider h="1px" my={0} />
|
||||
<Card.Content>
|
||||
{post.files.map((file: File) => {
|
||||
{post.files?.map((file: File) => {
|
||||
return <div key={file.id}>
|
||||
<Link color href={`${getPostPath(post.visibility, post.id)}#${file.title}`}>
|
||||
{file.title || 'Untitled file'}
|
||||
|
|
|
@ -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) => {
|
|||
<Button auto onClick={download} icon={<Archive />}>
|
||||
Download as ZIP archive
|
||||
</Button>
|
||||
<FileDropdown files={post.files} />
|
||||
<FileDropdown files={post.files || []} />
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
</div>
|
||||
{/* {post.files.length > 1 && <FileTree files={post.files} />} */}
|
||||
{post.files.map(({ id, content, title }: File) => (
|
||||
{post.files?.map(({ id, content, title }: File) => (
|
||||
<DocumentComponent
|
||||
key={id}
|
||||
title={title}
|
||||
|
|
2
client/lib/types.d.ts
vendored
2
client/lib/types.d.ts
vendored
|
@ -21,7 +21,7 @@ export type Post = {
|
|||
title: string
|
||||
description: string
|
||||
visibility: PostVisibility
|
||||
files: Files
|
||||
files?: Files
|
||||
createdAt: string
|
||||
users?: User[]
|
||||
expiresAt: Date | string | null
|
||||
|
|
1
server/migrate.js
Normal file
1
server/migrate.js
Normal file
|
@ -0,0 +1 @@
|
|||
require("./src/database").umzug.runAsCLI()
|
|
@ -8,8 +8,9 @@
|
|||
"dev": "cross-env NODE_ENV=development nodemon index.ts",
|
||||
"build": "mkdir -p ./dist && cp .env ./dist/.env && tsc -p ./tsconfig.json && tsc-alias -p ./tsconfig.json && yarn post-build",
|
||||
"post-build": "cp package.json ./dist/package.json && cp yarn.lock ./dist/yarn.lock && cd dist && env NODE_ENV=production yarn install",
|
||||
"migrate": "sequelize-cli-ts db:migrate",
|
||||
"migrate:undo": "sequelize-cli-ts db:migrate:undo",
|
||||
"migrate:up": "ts-node migrate up",
|
||||
"migrate:down": "ts-node migrate down",
|
||||
"migrate": "ts-node migrate",
|
||||
"lint": "prettier --config .prettierrc 'src/**/*.ts' 'index.ts' --write"
|
||||
},
|
||||
"author": "",
|
||||
|
|
|
@ -5,6 +5,7 @@ import { posts, users, auth, files, admin } from "@routes/index"
|
|||
import { errors } from "celebrate"
|
||||
import secretKey from "@lib/middleware/secret-key"
|
||||
import markdown from "@lib/render-markdown"
|
||||
import config from "@lib/config"
|
||||
|
||||
export const app = express()
|
||||
|
||||
|
@ -35,7 +36,7 @@ app.use(errors())
|
|||
|
||||
app.use(
|
||||
errorhandler({
|
||||
debug: process.env.NODE_ENV !== "production",
|
||||
debug: !config.is_production,
|
||||
log: true
|
||||
})
|
||||
)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import config from "@lib/config"
|
||||
import databasePath from "@lib/get-database-path"
|
||||
import { Sequelize } from "sequelize-typescript"
|
||||
import { SequelizeStorage, Umzug } from "umzug"
|
||||
|
@ -5,23 +6,22 @@ import { SequelizeStorage, Umzug } from "umzug"
|
|||
export const sequelize = new Sequelize({
|
||||
dialect: "sqlite",
|
||||
database: "drift",
|
||||
storage: process.env.MEMORY_DB === "true" ? ":memory:" : databasePath,
|
||||
storage: config.memory_db ? ":memory:" : databasePath,
|
||||
models: [__dirname + "/lib/models"],
|
||||
logging: true
|
||||
})
|
||||
|
||||
if (process.env.MEMORY_DB !== "true") {
|
||||
if (config.memory_db) {
|
||||
console.log(`Database path: ${databasePath}`)
|
||||
} else {
|
||||
console.log("Using in-memory database")
|
||||
}
|
||||
|
||||
const umzug = new Umzug({
|
||||
export const umzug = new Umzug({
|
||||
migrations: {
|
||||
glob:
|
||||
process.env.NODE_ENV === "production"
|
||||
? __dirname + "/migrations/*.js"
|
||||
: __dirname + "/migrations/*.ts"
|
||||
glob: config.is_production
|
||||
? __dirname + "/migrations/*.js"
|
||||
: __dirname + "/migrations/*.ts"
|
||||
},
|
||||
context: sequelize.getQueryInterface(),
|
||||
storage: new SequelizeStorage({ sequelize }),
|
||||
|
@ -29,16 +29,20 @@ const umzug = new Umzug({
|
|||
})
|
||||
|
||||
export type Migration = typeof umzug._types.migration
|
||||
;(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.")
|
||||
}
|
||||
})()
|
||||
|
||||
// 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.")
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in a new issue