server: add and run prettier

This commit is contained in:
Max Leiter 2022-03-24 14:57:40 -07:00
parent 056a2bd3ce
commit 2823c217ea
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
19 changed files with 668 additions and 553 deletions

7
server/.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"semi": false,
"trailingComma": "none",
"singleQuote": false,
"printWidth": 80,
"useTabs": true
}

View file

@ -1,4 +1,4 @@
import * as dotenv from 'dotenv'; import * as dotenv from "dotenv"
dotenv.config(); dotenv.config()
import './src/server'; import "./src/server"

View file

@ -7,7 +7,8 @@
"start": "ts-node index.ts", "start": "ts-node index.ts",
"dev": "nodemon index.ts", "dev": "nodemon index.ts",
"build": "tsc -p .", "build": "tsc -p .",
"migrate": "sequelize db:migrate" "migrate": "sequelize db:migrate",
"lint": "prettier --config .prettierrc 'src/**/*.ts' 'index.ts' --write"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@ -40,6 +41,7 @@
"@types/marked": "^4.0.3", "@types/marked": "^4.0.3",
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
"@types/react-dom": "^17.0.14", "@types/react-dom": "^17.0.14",
"prettier": "^2.6.0",
"ts-node": "^10.6.0", "ts-node": "^10.6.0",
"tsconfig-paths": "^3.14.1", "tsconfig-paths": "^3.14.1",
"tslint": "^6.1.3", "tslint": "^6.1.3",

View file

@ -1,29 +1,30 @@
import * as express from 'express'; import * as express from "express"
import * as bodyParser from 'body-parser'; import * as bodyParser from "body-parser"
import * as errorhandler from 'strong-error-handler'; import * as errorhandler from "strong-error-handler"
import * as cors from 'cors'; import * as cors from "cors"
import { posts, users, auth, files } from '@routes/index'; import { posts, users, auth, files } from "@routes/index"
import { errors } from 'celebrate' import { errors } from "celebrate"
export const app = express(); export const app = express()
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json({ limit: '5mb' })); app.use(bodyParser.json({ limit: "5mb" }))
const corsOptions = { const corsOptions = {
origin: `http://localhost:3001`, origin: `http://localhost:3001`
}; }
app.use(cors(corsOptions)); app.use(cors(corsOptions))
app.use("/auth", auth) app.use("/auth", auth)
app.use("/posts", posts) app.use("/posts", posts)
app.use("/users", users) app.use("/users", users)
app.use("/files", files) app.use("/files", files)
app.use(errors()); app.use(errors())
app.use(errorhandler({
debug: process.env.ENV !== 'production',
log: true,
}));
app.use(
errorhandler({
debug: process.env.ENV !== "production",
log: true
})
)

View file

@ -1,4 +1,4 @@
export default { export default {
port: process.env.PORT || 3000, port: process.env.PORT || 3000,
jwt_secret: process.env.JWT_SECRET || 'myjwtsecret', jwt_secret: process.env.JWT_SECRET || "myjwtsecret"
} }

View file

@ -1,30 +1,34 @@
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from "express"
import * as jwt from 'jsonwebtoken'; import * as jwt from "jsonwebtoken"
import config from '../config'; import config from "../config"
import { User as UserModel } from '../models/User'; import { User as UserModel } from "../models/User"
export interface User { export interface User {
id: string; id: string
} }
export interface UserJwtRequest extends Request { export interface UserJwtRequest extends Request {
user?: User; user?: User
} }
export default function authenticateToken(req: UserJwtRequest, res: Response, next: NextFunction) { export default function authenticateToken(
const authHeader = req.headers['authorization'] req: UserJwtRequest,
const token = authHeader && authHeader.split(' ')[1] res: Response,
next: NextFunction
) {
const authHeader = req.headers["authorization"]
const token = authHeader && authHeader.split(" ")[1]
if (token == null) return res.sendStatus(401) if (token == null) return res.sendStatus(401)
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => { jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
if (err) return res.sendStatus(403) if (err) return res.sendStatus(403)
const userObj = await UserModel.findByPk(user.id); const userObj = await UserModel.findByPk(user.id)
if (!userObj) { if (!userObj) {
return res.sendStatus(403); return res.sendStatus(403)
} }
req.user = user req.user = user
next() next()
}) })
} }

View file

@ -1,14 +1,18 @@
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from "express"
const key = process.env.SECRET_KEY; const key = process.env.SECRET_KEY
if (!key) { if (!key) {
throw new Error('SECRET_KEY is not set.'); throw new Error("SECRET_KEY is not set.")
} }
export default function authenticateToken(req: Request, res: Response, next: NextFunction) { export default function authenticateToken(
const requestKey = req.headers['x-secret-key'] req: Request,
if (requestKey !== key) { res: Response,
return res.sendStatus(401) next: NextFunction
} ) {
next() const requestKey = req.headers["x-secret-key"]
if (requestKey !== key) {
return res.sendStatus(401)
}
next()
} }

View file

@ -1,53 +1,65 @@
import { BelongsTo, Column, CreatedAt, DataType, ForeignKey, IsUUID, Model, PrimaryKey, Scopes, Table, Unique } from 'sequelize-typescript'; import {
import { Post } from './Post'; BelongsTo,
import { User } from './User'; Column,
CreatedAt,
DataType,
ForeignKey,
IsUUID,
Model,
PrimaryKey,
Scopes,
Table,
Unique
} from "sequelize-typescript"
import { Post } from "./Post"
import { User } from "./User"
@Scopes(() => ({ @Scopes(() => ({
full: { full: {
include: [{ include: [
model: User, {
through: { attributes: [] }, model: User,
}, through: { attributes: [] }
{ },
model: Post, {
through: { attributes: [] }, model: Post,
}] through: { attributes: [] }
} }
]
}
})) }))
@Table @Table
export class File extends Model { export class File extends Model {
@IsUUID(4) @IsUUID(4)
@PrimaryKey @PrimaryKey
@Unique @Unique
@Column({ @Column({
type: DataType.UUID, type: DataType.UUID,
defaultValue: DataType.UUIDV4, defaultValue: DataType.UUIDV4
}) })
id!: string id!: string
@Column @Column
title!: string; title!: string
@Column @Column
content!: string; content!: string
@Column @Column
sha!: string; sha!: string
@Column @Column
html!: string; html!: string
@ForeignKey(() => User) @ForeignKey(() => User)
@BelongsTo(() => User, 'userId') @BelongsTo(() => User, "userId")
user!: User; user!: User
@ForeignKey(() => Post) @ForeignKey(() => Post)
@BelongsTo(() => Post, 'postId') @BelongsTo(() => Post, "postId")
post!: Post; post!: Post
@CreatedAt @CreatedAt
@Column @Column
createdAt!: Date; createdAt!: Date
} }

View file

@ -1,58 +1,74 @@
import { BelongsToMany, Column, CreatedAt, DataType, HasMany, IsUUID, Model, PrimaryKey, Scopes, Table, Unique, UpdatedAt } from 'sequelize-typescript'; import {
import { PostAuthor } from './PostAuthor'; BelongsToMany,
import { User } from './User'; Column,
import { File } from './File'; CreatedAt,
DataType,
HasMany,
IsUUID,
Model,
PrimaryKey,
Scopes,
Table,
Unique,
UpdatedAt
} from "sequelize-typescript"
import { PostAuthor } from "./PostAuthor"
import { User } from "./User"
import { File } from "./File"
@Scopes(() => ({ @Scopes(() => ({
user: { user: {
include: [{ include: [
model: User, {
through: { attributes: [] }, model: User,
}], through: { attributes: [] }
}, }
full: { ]
include: [{ },
model: User, full: {
through: { attributes: [] }, include: [
}, {
{ model: User,
model: File, through: { attributes: [] }
through: { attributes: [] }, },
}] {
} model: File,
through: { attributes: [] }
}
]
}
})) }))
@Table @Table
export class Post extends Model { export class Post extends Model {
@IsUUID(4) @IsUUID(4)
@PrimaryKey @PrimaryKey
@Unique @Unique
@Column({ @Column({
type: DataType.UUID, type: DataType.UUID,
defaultValue: DataType.UUIDV4, defaultValue: DataType.UUIDV4
}) })
id!: string id!: string
@Column @Column
title!: string; title!: string
@BelongsToMany(() => User, () => PostAuthor) @BelongsToMany(() => User, () => PostAuthor)
users?: User[]; users?: User[]
@HasMany(() => File, { constraints: false }) @HasMany(() => File, { constraints: false })
files?: File[]; files?: File[]
@CreatedAt @CreatedAt
@Column @Column
createdAt!: Date; createdAt!: Date
@Column @Column
visibility!: string; visibility!: string
@Column @Column
password?: string; password?: string
@UpdatedAt @UpdatedAt
@Column @Column
updatedAt!: Date; updatedAt!: Date
} }

View file

@ -1,23 +1,32 @@
import { Model, Column, Table, ForeignKey, IsUUID, PrimaryKey, DataType, Unique } from "sequelize-typescript"; import {
import { Post } from "./Post"; Model,
import { User } from "./User"; Column,
Table,
ForeignKey,
IsUUID,
PrimaryKey,
DataType,
Unique
} from "sequelize-typescript"
import { Post } from "./Post"
import { User } from "./User"
@Table @Table
export class PostAuthor extends Model { export class PostAuthor extends Model {
@IsUUID(4) @IsUUID(4)
@PrimaryKey @PrimaryKey
@Unique @Unique
@Column({ @Column({
type: DataType.UUID, type: DataType.UUID,
defaultValue: DataType.UUIDV4, defaultValue: DataType.UUIDV4
}) })
id!: string id!: string
@ForeignKey(() => Post) @ForeignKey(() => Post)
@Column @Column
postId!: number; postId!: number
@ForeignKey(() => User) @ForeignKey(() => User)
@Column @Column
authorId!: number; authorId!: number
} }

View file

@ -1,48 +1,59 @@
import { Model, Column, Table, BelongsToMany, Scopes, CreatedAt, UpdatedAt, IsUUID, PrimaryKey, DataType, Unique } from "sequelize-typescript"; import {
import { Post } from "./Post"; Model,
import { PostAuthor } from "./PostAuthor"; Column,
Table,
BelongsToMany,
Scopes,
CreatedAt,
UpdatedAt,
IsUUID,
PrimaryKey,
DataType,
Unique
} from "sequelize-typescript"
import { Post } from "./Post"
import { PostAuthor } from "./PostAuthor"
@Scopes(() => ({ @Scopes(() => ({
posts: { posts: {
include: [ include: [
{ {
model: Post, model: Post,
through: { attributes: [] }, through: { attributes: [] }
}, }
], ]
}, },
withoutPassword: { withoutPassword: {
attributes: { attributes: {
exclude: ["password"] exclude: ["password"]
} }
} }
})) }))
@Table @Table
export class User extends Model { export class User extends Model {
@IsUUID(4) @IsUUID(4)
@PrimaryKey @PrimaryKey
@Unique @Unique
@Column({ @Column({
type: DataType.UUID, type: DataType.UUID,
defaultValue: DataType.UUIDV4, defaultValue: DataType.UUIDV4
}) })
id!: string id!: string
@Column @Column
username!: string; username!: string
@Column @Column
password!: string; password!: string
@BelongsToMany(() => Post, () => PostAuthor) @BelongsToMany(() => Post, () => PostAuthor)
posts?: Post[]; posts?: Post[]
@CreatedAt @CreatedAt
@Column @Column
createdAt!: Date; createdAt!: Date
@UpdatedAt @UpdatedAt
@Column @Column
updatedAt!: Date; updatedAt!: Date
} }

View file

@ -1,9 +1,12 @@
import { Sequelize } from 'sequelize-typescript'; import { Sequelize } from "sequelize-typescript"
export const sequelize = new Sequelize({ export const sequelize = new Sequelize({
dialect: 'sqlite', dialect: "sqlite",
database: 'drift', database: "drift",
storage: process.env.MEMORY_DB === "true" ? ":memory:" : __dirname + '/../../drift.sqlite', storage:
models: [__dirname + '/models'], process.env.MEMORY_DB === "true"
host: 'localhost', ? ":memory:"
}); : __dirname + "/../../drift.sqlite",
models: [__dirname + "/models"],
host: "localhost"
})

View file

@ -1,121 +1,137 @@
import { Router } from 'express' import { Router } from "express"
import { genSalt, hash, compare } from "bcrypt" import { genSalt, hash, compare } from "bcrypt"
import { User } from '@lib/models/User' import { User } from "@lib/models/User"
import { sign } from 'jsonwebtoken' import { sign } from "jsonwebtoken"
import config from '@lib/config' import config from "@lib/config"
import jwt from '@lib/middleware/jwt' import jwt from "@lib/middleware/jwt"
import { celebrate, Joi } from 'celebrate' import { celebrate, Joi } from "celebrate"
const NO_EMPTY_SPACE_REGEX = /^\S*$/ const NO_EMPTY_SPACE_REGEX = /^\S*$/
export const requiresServerPassword = (process.env.MEMORY_DB || process.env.ENV === 'production') && !!process.env.REGISTRATION_PASSWORD export const requiresServerPassword =
(process.env.MEMORY_DB || process.env.ENV === "production") &&
!!process.env.REGISTRATION_PASSWORD
console.log(`Registration password required: ${requiresServerPassword}`) console.log(`Registration password required: ${requiresServerPassword}`)
export const auth = Router() export const auth = Router()
const validateAuthPayload = (username: string, password: string, serverPassword?: string): void => { const validateAuthPayload = (
if (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) { username: string,
throw new Error("Authentication data does not fulfill requirements") password: string,
} serverPassword?: string
): void => {
if (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) {
throw new Error("Authentication data does not fulfill requirements")
}
if (requiresServerPassword) { if (requiresServerPassword) {
if (!serverPassword || process.env.REGISTRATION_PASSWORD !== serverPassword) { if (
throw new Error("Server password is incorrect. Please contact the server administrator.") !serverPassword ||
} process.env.REGISTRATION_PASSWORD !== serverPassword
} ) {
throw new Error(
"Server password is incorrect. Please contact the server administrator."
)
}
}
} }
auth.post('/signup', auth.post(
celebrate({ "/signup",
body: { celebrate({
username: Joi.string().required(), body: {
password: Joi.string().required(), username: Joi.string().required(),
serverPassword: Joi.string().required().allow('', null), password: Joi.string().required(),
} serverPassword: Joi.string().required().allow("", null)
}), }
async (req, res, next) => { }),
try { async (req, res, next) => {
validateAuthPayload(req.body.username, req.body.password, req.body.serverPassword) try {
const username = req.body.username.toLowerCase(); validateAuthPayload(
req.body.username,
req.body.password,
req.body.serverPassword
)
const username = req.body.username.toLowerCase()
const existingUser = await User.findOne({ const existingUser = await User.findOne({
where: { username: username }, where: { username: username }
}); })
if (existingUser) { if (existingUser) {
throw new Error("Username already exists"); throw new Error("Username already exists")
} }
const salt = await genSalt(10); const salt = await genSalt(10)
const user = { const user = {
username: username as string, username: username as string,
password: await hash(req.body.password, salt), password: await hash(req.body.password, salt)
}; }
const created_user = await User.create(user); const created_user = await User.create(user)
const token = generateAccessToken(created_user.id); const token = generateAccessToken(created_user.id)
res.status(201).json({ token: token, userId: created_user.id }); res.status(201).json({ token: token, userId: created_user.id })
} catch (e) { } catch (e) {
next(e); next(e)
} }
} }
); )
auth.post( auth.post(
"/signin", "/signin",
celebrate({ celebrate({
body: { body: {
username: Joi.string().required(), username: Joi.string().required(),
password: Joi.string().required(), password: Joi.string().required(),
serverPassword: Joi.string().required().allow('', null), serverPassword: Joi.string().required().allow("", null)
}, }
}), }),
async (req, res, next) => { async (req, res, next) => {
const error = "User does not exist or password is incorrect"; const error = "User does not exist or password is incorrect"
const errorToThrow = new Error(error); const errorToThrow = new Error(error)
try { try {
if (!req.body.username || !req.body.password) { if (!req.body.username || !req.body.password) {
throw errorToThrow; throw errorToThrow
} }
const username = req.body.username.toLowerCase(); const username = req.body.username.toLowerCase()
const user = await User.findOne({ where: { username: username } }); const user = await User.findOne({ where: { username: username } })
if (!user) { if (!user) {
throw errorToThrow; throw errorToThrow
} }
const password_valid = await compare(req.body.password, user.password); const password_valid = await compare(req.body.password, user.password)
if (password_valid) { if (password_valid) {
const token = generateAccessToken(user.id); const token = generateAccessToken(user.id)
res.status(200).json({ token: token, userId: user.id }); res.status(200).json({ token: token, userId: user.id })
} else { } else {
throw errorToThrow; throw errorToThrow
} }
} catch (e) { } catch (e) {
next(e); next(e)
} }
} }
); )
auth.get("/requires-passcode", async (req, res, next) => { auth.get("/requires-passcode", async (req, res, next) => {
if (requiresServerPassword) { if (requiresServerPassword) {
res.status(200).json({ requiresPasscode: true }); res.status(200).json({ requiresPasscode: true })
} else { } else {
res.status(200).json({ requiresPasscode: false }); res.status(200).json({ requiresPasscode: false })
} }
}); })
function generateAccessToken(id: string) { function generateAccessToken(id: string) {
return sign({ id: id }, config.jwt_secret, { expiresIn: "2d" }); return sign({ id: id }, config.jwt_secret, { expiresIn: "2d" })
} }
auth.get("/verify-token", jwt, async (req, res, next) => { auth.get("/verify-token", jwt, async (req, res, next) => {
try { try {
res.status(200).json({ res.status(200).json({
message: "You are authenticated", message: "You are authenticated"
}); })
} catch (e) { } catch (e) {
next(e); next(e)
} }
}); })

View file

@ -1,72 +1,72 @@
import { celebrate, Joi } from "celebrate"; import { celebrate, Joi } from "celebrate"
import { Router } from "express"; import { Router } from "express"
import { File } from "@lib/models/File"; import { File } from "@lib/models/File"
import secretKey from "@lib/middleware/secret-key"; import secretKey from "@lib/middleware/secret-key"
export const files = Router(); export const files = Router()
files.get("/raw/:id", files.get(
celebrate({ "/raw/:id",
params: { celebrate({
id: Joi.string().required(), params: {
}, id: Joi.string().required()
}), }
secretKey, }),
async (req, res, next) => { secretKey,
try { async (req, res, next) => {
const file = await File.findOne({ try {
where: { const file = await File.findOne({
id: req.params.id where: {
}, id: req.params.id
attributes: ["title", "content"], },
}) attributes: ["title", "content"]
})
if (!file) { if (!file) {
return res.status(404).json({ error: "File not found" }) return res.status(404).json({ error: "File not found" })
} }
// TODO: JWT-checkraw files // TODO: JWT-checkraw files
if (file?.post?.visibility === "private") { if (file?.post?.visibility === "private") {
// jwt(req as UserJwtRequest, res, () => { // jwt(req as UserJwtRequest, res, () => {
// res.json(file); // res.json(file);
// }) // })
res.json(file); res.json(file)
} else { } else {
res.json(file); res.json(file)
} }
} } catch (e) {
catch (e) { next(e)
next(e); }
} }
}
) )
files.get(
"/html/:id",
celebrate({
params: {
id: Joi.string().required()
}
}),
async (req, res, next) => {
try {
const file = await File.findOne({
where: {
id: req.params.id
},
attributes: ["html"]
})
files.get("/html/:id", if (!file) {
celebrate({ return res.status(404).json({ error: "File not found" })
params: { }
id: Joi.string().required(),
},
}),
async (req, res, next) => {
try {
const file = await File.findOne({
where: {
id: req.params.id
},
attributes: ["html"],
})
if (!file) { res.setHeader("Content-Type", "text/plain")
return res.status(404).json({ error: "File not found" }) res.setHeader("Cache-Control", "public, max-age=4800")
} res.status(200).write(file.html)
res.end()
res.setHeader('Content-Type', 'text/plain') } catch (error) {
res.setHeader('Cache-Control', 'public, max-age=4800') next(error)
res.status(200).write(file.html) }
res.end() }
} catch (error) {
next(error)
}
}
) )

View file

@ -1,4 +1,4 @@
export { auth } from "./auth"; export { auth } from "./auth"
export { posts } from "./posts"; export { posts } from "./posts"
export { users } from "./users"; export { users } from "./users"
export { files } from "./files"; export { files } from "./files"

View file

@ -1,209 +1,236 @@
import { Router } from "express"; import { Router } from "express"
import { celebrate, Joi } from "celebrate"; import { celebrate, Joi } from "celebrate"
import { File } from '@lib/models/File' import { File } from "@lib/models/File"
import { Post } from '@lib/models/Post'; import { Post } from "@lib/models/Post"
import jwt, { UserJwtRequest } from '@lib/middleware/jwt'; import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
import * as crypto from "crypto"; import * as crypto from "crypto"
import { User } from '@lib/models/User'; import { User } from "@lib/models/User"
import secretKey from '@lib/middleware/secret-key'; import secretKey from "@lib/middleware/secret-key"
import markdown from '@lib/render-markdown'; import markdown from "@lib/render-markdown"
export const posts = Router(); export const posts = Router()
const postVisibilitySchema = (value: string) => { const postVisibilitySchema = (value: string) => {
if (value === 'public' || value === 'private') { if (value === "public" || value === "private") {
return value; return value
} else { } else {
throw new Error('Invalid post visibility'); throw new Error("Invalid post visibility")
} }
} }
posts.post( posts.post(
"/create", "/create",
jwt, jwt,
celebrate({ celebrate({
body: { body: {
title: Joi.string().required(), title: Joi.string().required().allow("", null),
files: Joi.any().required(), files: Joi.any().required(),
visibility: Joi.string().custom(postVisibilitySchema, 'valid visibility').required(), visibility: Joi.string()
userId: Joi.string().required(), .custom(postVisibilitySchema, "valid visibility")
password: Joi.string().optional(), .required(),
}, userId: Joi.string().required(),
}), password: Joi.string().optional()
async (req, res, next) => { }
try { }),
let hashedPassword: string = '' async (req, res, next) => {
if (req.body.visibility === 'protected') { try {
hashedPassword = crypto.createHash('sha256').update(req.body.password).digest('hex'); let hashedPassword: string = ""
} if (req.body.visibility === "protected") {
hashedPassword = crypto
.createHash("sha256")
.update(req.body.password)
.digest("hex")
}
const newPost = new Post({ const newPost = new Post({
title: req.body.title, title: req.body.title,
visibility: req.body.visibility, visibility: req.body.visibility,
password: hashedPassword, password: hashedPassword
}) })
await newPost.save() await newPost.save()
await newPost.$add('users', req.body.userId); await newPost.$add("users", req.body.userId)
const newFiles = await Promise.all(req.body.files.map(async (file) => { const newFiles = await Promise.all(
const html = getHtmlFromFile(file); req.body.files.map(async (file) => {
const newFile = new File({ const html = getHtmlFromFile(file)
title: file.title, const newFile = new File({
content: file.content, title: file.title || "",
sha: crypto.createHash('sha256').update(file.content).digest('hex').toString(), content: file.content,
html sha: crypto
}) .createHash("sha256")
.update(file.content)
.digest("hex")
.toString(),
html
})
await newFile.$set("user", req.body.userId); await newFile.$set("user", req.body.userId)
await newFile.$set("post", newPost.id); await newFile.$set("post", newPost.id)
await newFile.save(); await newFile.save()
return newFile; return newFile
})) })
)
await Promise.all(newFiles.map((file) => { await Promise.all(
newPost.$add("files", file.id); newFiles.map((file) => {
newPost.save(); newPost.$add("files", file.id)
})) newPost.save()
})
)
res.json(newPost); res.json(newPost)
} catch (e) { } catch (e) {
next(e); next(e)
} }
} }
); )
posts.get("/", secretKey, async (req, res, next) => { posts.get("/", secretKey, async (req, res, next) => {
try { try {
const posts = await Post.findAll({ const posts = await Post.findAll({
attributes: ["id", "title", "visibility", "createdAt"], attributes: ["id", "title", "visibility", "createdAt"]
}) })
res.json(posts); res.json(posts)
} catch (e) { } catch (e) {
next(e); next(e)
} }
}); })
posts.get("/mine", jwt, secretKey, async (req: UserJwtRequest, res, next) => { posts.get("/mine", jwt, secretKey, async (req: UserJwtRequest, res, next) => {
if (!req.user) { if (!req.user) {
return res.status(401).json({ error: "Unauthorized" }) return res.status(401).json({ error: "Unauthorized" })
} }
try { try {
const user = await User.findByPk(req.user.id, { const user = await User.findByPk(req.user.id, {
include: [ include: [
{ {
model: Post, model: Post,
as: "posts", as: "posts",
include: [ include: [
{ {
model: File, model: File,
as: "files" as: "files"
} }
] ]
}, }
], ]
}) })
if (!user) { if (!user) {
return res.status(404).json({ error: "User not found" }) return res.status(404).json({ error: "User not found" })
} }
return res.json(user.posts?.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())) return res.json(
} catch (error) { user.posts?.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
next(error) )
} } catch (error) {
next(error)
}
}) })
posts.get( posts.get(
"/:id", "/:id",
celebrate({ celebrate({
params: { params: {
id: Joi.string().required(), id: Joi.string().required()
}, }
}), }),
async (req: UserJwtRequest, res, next) => { async (req: UserJwtRequest, res, next) => {
try { try {
const post = await Post.findOne({ const post = await Post.findOne({
where: { where: {
id: req.params.id, id: req.params.id
}, },
include: [ include: [
{ {
model: File, model: File,
as: "files", as: "files",
attributes: [ attributes: [
"id", "id",
"title", "title",
"content", "content",
"sha", "sha",
"createdAt", "createdAt",
"updatedAt", "updatedAt"
], ]
}, },
{ {
model: User, model: User,
as: "users", as: "users",
attributes: ["id", "username"], attributes: ["id", "username"]
}, }
], ]
}); })
if (!post) { if (!post) {
return res.status(404).json({ error: "Post not found" }) return res.status(404).json({ error: "Post not found" })
} }
// if public or unlisted, cache // if public or unlisted, cache
if (post.visibility === 'public' || post.visibility === 'unlisted') { if (post.visibility === "public" || post.visibility === "unlisted") {
res.set('Cache-Control', 'public, max-age=4800') res.set("Cache-Control", "public, max-age=4800")
} }
if (post.visibility === 'public' || post?.visibility === 'unlisted') { if (post.visibility === "public" || post?.visibility === "unlisted") {
secretKey(req, res, () => { secretKey(req, res, () => {
res.json(post); res.json(post)
}) })
} else if (post.visibility === 'private') { } else if (post.visibility === "private") {
jwt(req as UserJwtRequest, res, () => { jwt(req as UserJwtRequest, res, () => {
res.json(post); res.json(post)
}) })
} else if (post.visibility === 'protected') { } else if (post.visibility === "protected") {
const { password } = req.query const { password } = req.query
if (!password || typeof password !== 'string') { if (!password || typeof password !== "string") {
return jwt(req as UserJwtRequest, res, () => { return jwt(req as UserJwtRequest, res, () => {
res.json(post); res.json(post)
}) })
} }
const hash = crypto.createHash('sha256').update(password).digest('hex').toString() const hash = crypto
if (hash !== post.password) { .createHash("sha256")
return res.status(400).json({ error: "Incorrect password." }) .update(password)
} .digest("hex")
.toString()
if (hash !== post.password) {
return res.status(400).json({ error: "Incorrect password." })
}
res.json(post); res.json(post)
} }
} } catch (e) {
catch (e) { next(e)
next(e); }
} }
} )
);
function getHtmlFromFile(file: any) { function getHtmlFromFile(file: any) {
const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']; const renderAsMarkdown = [
const fileType = () => { "markdown",
const pathParts = file.title.split("."); "md",
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""; "mdown",
return language; "mkdn",
}; "mkd",
const type = fileType(); "mdwn",
let contentToRender: string = (file.content || ''); "mdtxt",
"mdtext",
"text",
""
]
const fileType = () => {
const pathParts = file.title.split(".")
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
return language
}
const type = fileType()
let contentToRender: string = file.content || ""
if (!renderAsMarkdown.includes(type)) { if (!renderAsMarkdown.includes(type)) {
contentToRender = contentToRender = `~~~${type}
`~~~${type}
${file.content} ${file.content}
~~~`; ~~~`
} else { } else {
contentToRender = '\n' + file.content; contentToRender = "\n" + file.content
} }
const html = markdown(contentToRender); const html = markdown(contentToRender)
return html; return html
} }

View file

@ -1,8 +1,8 @@
import { Router } from "express"; import { Router } from "express"
// import jwt from "@lib/middleware/jwt"; // import jwt from "@lib/middleware/jwt";
// import { User } from "@lib/models/User"; // import { User } from "@lib/models/User";
export const users = Router(); export const users = Router()
// users.get("/", jwt, async (req, res, next) => { // users.get("/", jwt, async (req, res, next) => {
// try { // try {

View file

@ -1,13 +1,11 @@
import { createServer } from 'http'; import { createServer } from "http"
import { app } from './app'; import { app } from "./app"
import config from './lib/config'; import config from "./lib/config"
import { sequelize } from './lib/sequelize'; import { sequelize } from "./lib/sequelize"
(async () => { ;(async () => {
await sequelize.sync({}); await sequelize.sync({})
createServer(app) createServer(app).listen(config.port, () =>
.listen( console.info(`Server running on port ${config.port}`)
config.port, )
() => console.info(`Server running on port ${config.port}`) })()
);
})();

View file

@ -2027,6 +2027,11 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prettier@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.0.tgz#12f8f504c4d8ddb76475f441337542fa799207d4"
integrity sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A==
prism-react-renderer@^1.3.1: prism-react-renderer@^1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d" resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d"