diff --git a/README.md b/README.md index 77bceb5e..343927f4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Drift -Drift is a self-hostable Gist clone. It's also a major work-in-progress, but is completely functional. +Drift is a self-hostable Gist clone. It's in beta, but is completely functional. -You can try a demo at https://drift.maxleiter.com. The demo is built on master but has no database, so files and accounts can be wiped at any time. +You can try a demo at https://drift.lol. The demo is built on main but has no database, so files and accounts can be wiped at any time. If you want to contribute, need support, or want to stay updated, you can join the IRC channel at #drift on irc.libera.chat or [reach me on twitter](https://twitter.com/Max_Leiter). If you don't have an IRC client yet, you can use a webclient [here](https://demo.thelounge.chat/#/connect?join=%23drift&nick=drift-user&realname=Drift%20User).
diff --git a/server/src/__tests__/e2e.ts b/server/src/__tests__/e2e.ts deleted file mode 100644 index 664ef5ca..00000000 --- a/server/src/__tests__/e2e.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as request from "supertest" -import { app } from "../app" - -describe("GET /health", () => { - it("should return 200 and a status up", (done) => { - request(app) - .get(`/health`) - .expect("Content-Type", /json/) - .expect(200) - .end((err, res) => { - if (err) return done(err) - expect(res.body).toMatchObject({ status: "UP" }) - done() - }) - }) -}) diff --git a/server/src/lib/__tests__/config.ts b/server/src/lib/__tests__/config.ts deleted file mode 100644 index 8a9aacac..00000000 --- a/server/src/lib/__tests__/config.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { config } from "../config" - -describe("Config", () => { - it("should build a valid development config when no environment is set", () => { - const emptyEnv = {} - const result = config(emptyEnv) - - expect(result).toHaveProperty("is_production", false) - expect(result).toHaveProperty("port") - expect(result).toHaveProperty("jwt_secret") - expect(result).toHaveProperty("drift_home") - expect(result).toHaveProperty("memory_db") - expect(result).toHaveProperty("enable_admin") - expect(result).toHaveProperty("secret_key") - expect(result).toHaveProperty("registration_password") - expect(result).toHaveProperty("welcome_content") - expect(result).toHaveProperty("welcome_title") - }) - - it("should fail when building a prod environment without SECRET_KEY", () => { - expect(() => config({ NODE_ENV: "production" })).toThrow( - new Error("Missing environment variable: SECRET_KEY") - ) - }) - - it("should build a prod config with a SECRET_KEY", () => { - const result = config({ NODE_ENV: "production", SECRET_KEY: "secret" }) - - expect(result).toHaveProperty("is_production", true) - expect(result).toHaveProperty("secret_key", "secret") - }) - - describe("jwt_secret", () => { - it("should use default jwt_secret when environment is blank string", () => { - const result = config({ JWT_SECRET: "" }) - - expect(result).toHaveProperty("is_production", false) - expect(result).toHaveProperty("jwt_secret", "myjwtsecret") - }) - }) - - describe("booleans", () => { - it("should parse 'true' as true", () => { - const result = config({ MEMORY_DB: "true" }) - - expect(result).toHaveProperty("memory_db", true) - }) - it("should parse 'false' as false", () => { - const result = config({ MEMORY_DB: "false" }) - - expect(result).toHaveProperty("memory_db", false) - }) - it("should fail when it is not parseable", () => { - expect(() => config({ MEMORY_DB: "foo" })).toThrow( - new Error("Invalid boolean value: foo") - ) - }) - it("should default to false when the string is empty", () => { - const result = config({ MEMORY_DB: "" }) - - expect(result).toHaveProperty("memory_db", false) - }) - }) -}) diff --git a/server/src/lib/__tests__/get-html-from-drift-file.ts b/server/src/lib/__tests__/get-html-from-drift-file.ts deleted file mode 100644 index 461b6f9b..00000000 --- a/server/src/lib/__tests__/get-html-from-drift-file.ts +++ /dev/null @@ -1,17 +0,0 @@ -import getHtmlFromFile from "@lib/get-html-from-drift-file" - -describe("get-html-from-drift-file", () => { - it("should not wrap markdown in code blocks", () => { - const markdown = `## My Markdown` - const html = getHtmlFromFile({ content: markdown, title: "my-markdown.md" }) - // the string is

My Markdown

, - // but we dont wan't to be too strict in case markup changes - expect(html).toMatch(/

<\/h2>/) - }) - - it("should wrap code in code blocks", () => { - const code = `const foo = "bar"` - const html = getHtmlFromFile({ content: code, title: "my-code.js" }) - expect(html).toMatch(/
/)
-	})
-})
diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts
deleted file mode 100644
index 780eb627..00000000
--- a/server/src/routes/admin.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-import isAdmin, { UserJwtRequest } from "@lib/middleware/is-admin";
-import { Post } from "@lib/models/Post";
-import { User } from "@lib/models/User";
-import { File } from "@lib/models/File";
-import { Router } from "express";
-import { celebrate, Joi } from "celebrate";
-
-export const admin = Router();
-
-admin.use(isAdmin);
-
-admin.get("/is-admin", async (req, res) => {
-  return res.json({
-    isAdmin: true,
-  });
-});
-
-admin.get("/users", async (req, res, next) => {
-  try {
-    const users = await User.findAll({
-      attributes: {
-        exclude: ["password"],
-        include: ["id", "username", "createdAt", "updatedAt"],
-      },
-      include: [
-        {
-          model: Post,
-          as: "posts",
-          attributes: ["id"],
-        },
-      ],
-    });
-    res.json(users);
-  } catch (e) {
-    next(e);
-  }
-});
-
-admin.post(
-  "/users/toggle-role",
-  celebrate({
-    body: {
-      id: Joi.string().required(),
-      role: Joi.string().required().allow("user", "admin"),
-    },
-  }),
-  async (req: UserJwtRequest, res, next) => {
-    try {
-      const { id, role } = req.body;
-      if (req.user?.id === id) {
-        return res.status(400).json({
-          error: "You can't change your own role",
-        });
-      }
-
-      const user = await User.findByPk(id);
-      if (!user) {
-        return res.status(404).json({
-          error: "User not found",
-        });
-      }
-
-      await user.update({
-        role,
-      });
-
-      await user.save();
-
-      res.json({
-        success: true,
-      });
-    } catch (e) {
-      next(e);
-    }
-  }
-);
-
-admin.delete("/users/:id", async (req, res, next) => {
-  try {
-    const user = await User.findByPk(req.params.id);
-    if (!user) {
-      return res.status(404).json({
-        error: "User not found",
-      });
-    }
-    // TODO: verify CASCADE is removing files + posts
-    await user.destroy();
-
-    res.json({
-      success: true,
-    });
-  } catch (e) {
-    next(e);
-  }
-});
-
-admin.delete("/posts/:id", async (req, res, next) => {
-  try {
-    const post = await Post.findByPk(req.params.id);
-    if (!post) {
-      return res.status(404).json({
-        error: "Post not found",
-      });
-    }
-    await post.destroy();
-
-    res.json({
-      success: true,
-    });
-  } catch (e) {
-    next(e);
-  }
-});
-
-admin.get("/posts", async (req, res, next) => {
-  try {
-    const posts = await Post.findAll({
-      attributes: {
-        exclude: ["content"],
-        include: ["id", "title", "visibility", "createdAt"],
-      },
-      include: [
-        {
-          model: File,
-          as: "files",
-          attributes: ["id", "title", "createdAt", "html"],
-        },
-        {
-          model: User,
-          as: "users",
-          attributes: ["id", "username"],
-        },
-      ],
-    });
-    res.json(posts);
-  } catch (e) {
-    next(e);
-  }
-});
-
-admin.get("/post/:id", async (req, res, next) => {
-  try {
-    const post = await Post.findByPk(req.params.id, {
-      attributes: {
-        exclude: ["content"],
-        include: ["id", "title", "visibility", "createdAt"],
-      },
-      include: [
-        {
-          model: File,
-          as: "files",
-          attributes: ["id", "title", "sha", "createdAt", "updatedAt", "html"],
-        },
-        {
-          model: User,
-          as: "users",
-          attributes: ["id", "username"],
-        },
-      ],
-    });
-    if (!post) {
-      return res.status(404).json({
-        message: "Post not found",
-      });
-    }
-
-    res.json(post);
-  } catch (e) {
-    next(e);
-  }
-});
-
-admin.delete("/post/:id", async (req, res, next) => {
-  try {
-    const post = await Post.findByPk(req.params.id, {
-      include: [
-        {
-          model: File,
-          as: "files",
-        },
-      ],
-    });
-
-    if (!post) {
-      return res.status(404).json({
-        message: "Post not found",
-      });
-    }
-
-    if (post.files?.length)
-      await Promise.all(post.files.map((file) => file.destroy()));
-    await post.destroy({ force: true });
-    res.json({
-      message: "Post deleted",
-    });
-  } catch (e) {
-    next(e);
-  }
-});
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
deleted file mode 100644
index ca763598..00000000
--- a/server/src/routes/auth.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-import { Router } from "express"
-import { genSalt, hash, compare } from "bcryptjs"
-import { User } from "@lib/models/User"
-import { AuthToken } from "@lib/models/AuthToken"
-import { sign, verify } from "jsonwebtoken"
-import config from "@lib/config"
-import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
-import { celebrate, Joi } from "celebrate"
-import secretKey from "@lib/middleware/secret-key"
-
-const NO_EMPTY_SPACE_REGEX = /^\S*$/
-
-// we require a server password if the password is set and we're in production
-export const requiresServerPassword =
-	config.registration_password.length > 0 && config.is_production
-if (requiresServerPassword) console.log(`Registration password enabled.`)
-
-export const auth = Router()
-
-const validateAuthPayload = (
-	username: string,
-	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 (!serverPassword || config.registration_password !== serverPassword) {
-			throw new Error(
-				"Server password is incorrect. Please contact the server administrator."
-			)
-		}
-	}
-}
-
-auth.post(
-	"/signup",
-	celebrate({
-		body: {
-			username: Joi.string().required(),
-			password: Joi.string().required(),
-			serverPassword: Joi.string().required().allow("", null)
-		}
-	}),
-	async (req, res, next) => {
-		try {
-			validateAuthPayload(
-				req.body.username,
-				req.body.password,
-				req.body.serverPassword
-			)
-			const username = req.body.username.toLowerCase()
-
-			const existingUser = await User.findOne({
-				where: { username: username }
-			})
-
-			if (existingUser) {
-				throw new Error("Username already exists")
-			}
-
-			const salt = await genSalt(10)
-			const { count } = await User.findAndCountAll()
-
-			const user = {
-				username: username as string,
-				password: await hash(req.body.password, salt),
-				role: config.enable_admin && count === 0 ? "admin" : "user"
-			}
-
-			const created_user = await User.create(user)
-
-			const token = generateAccessToken(created_user)
-
-			res.status(201).json({ token: token, userId: created_user.id })
-		} catch (e) {
-			res.status(401).json({
-				error: {
-					message: e.message
-				}
-			})
-		}
-	}
-)
-
-auth.post(
-	"/signin",
-	celebrate({
-		body: {
-			username: Joi.string().required(),
-			password: Joi.string().required(),
-			serverPassword: Joi.string().required().allow("", null)
-		}
-	}),
-	async (req, res, next) => {
-		const error = "User does not exist or password is incorrect"
-		const errorToThrow = new Error(error)
-		try {
-			if (!req.body.username || !req.body.password) {
-				throw errorToThrow
-			}
-
-			const username = req.body.username.toLowerCase()
-			const user = await User.findOne({ where: { username: username } })
-			if (!user) {
-				throw errorToThrow
-			}
-
-			const password_valid = await compare(req.body.password, user.password)
-			if (password_valid) {
-				const token = generateAccessToken(user)
-				res.status(200).json({ token: token, userId: user.id })
-			} else {
-				throw errorToThrow
-			}
-		} catch (e) {
-			res.status(401).json({
-				error: {
-					message: error
-				}
-			})
-		}
-	}
-)
-
-auth.get("/requires-passcode", async (req, res, next) => {
-	if (requiresServerPassword) {
-		res.status(200).json({ requiresPasscode: true })
-	} else {
-		res.status(200).json({ requiresPasscode: false })
-	}
-})
-
-/**
- * Creates an access token, stores it in AuthToken table, and returns it
- */
-function generateAccessToken(user: User) {
-	const token = sign({ id: user.id }, config.jwt_secret, { expiresIn: "2d" })
-	const authToken = new AuthToken({
-		userId: user.id,
-		token: token
-	})
-	authToken.save()
-
-	return token
-}
-
-auth.get("/verify-token", jwt, async (req, res, next) => {
-	try {
-		res.status(200).json({
-			message: "You are authenticated"
-		})
-	} catch (e) {
-		next(e)
-	}
-})
-
-auth.post("/signout", secretKey, async (req, res, next) => {
-	try {
-		const authHeader = req.headers["authorization"]
-		const token = authHeader?.split(" ")[1]
-		let reason = ""
-		if (token == null) return res.sendStatus(401)
-
-		verify(token, config.jwt_secret, async (err: any, user: any) => {
-			if (err) {
-				reason = "Token expired"
-			} else if (user) {
-				reason = "User signed out"
-			} else {
-				reason = "Unknown"
-			}
-
-			// find and destroy the AuthToken + set the reason
-			const authToken = await AuthToken.findOne({ where: { token: token } })
-			if (authToken == null) {
-				res.sendStatus(401)
-			} else {
-				authToken.expiredReason = reason
-				authToken.save()
-				authToken.destroy()
-			}
-
-			req.headers["authorization"] = ""
-			res.status(201).json({
-				message: "You are now logged out",
-				token,
-				reason
-			})
-		})
-	} catch (e) {
-		next(e)
-	}
-})
-
-auth.put(
-	"/change-password",
-	jwt,
-	celebrate({
-		body: {
-			oldPassword: Joi.string().required().min(6).max(128),
-			newPassword: Joi.string().required().min(6).max(128)
-		}
-	}),
-	async (req: UserJwtRequest, res, next) => {
-		try {
-			const user = await User.findOne({ where: { id: req.user?.id } })
-			if (!user) {
-				return res.sendStatus(401)
-			}
-
-			const password_valid = await compare(req.body.oldPassword, user.password)
-			if (!password_valid) {
-				res.status(401).json({
-					error: "Old password is incorrect"
-				})
-			}
-
-			const salt = await genSalt(10)
-			user.password = await hash(req.body.newPassword, salt)
-			user.save()
-
-			res.status(200).json({
-				message: "Password changed"
-			})
-		} catch (e) {
-			next(e)
-		}
-	}
-)
diff --git a/server/src/routes/files.ts b/server/src/routes/files.ts
deleted file mode 100644
index a4c05c61..00000000
--- a/server/src/routes/files.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { celebrate, Joi } from "celebrate"
-import { Router } from "express"
-import { File } from "@lib/models/File"
-import secretKey from "@lib/middleware/secret-key"
-import jwt from "@lib/middleware/jwt"
-import getHtmlFromFile from "@lib/get-html-from-drift-file"
-
-export const files = Router()
-
-files.post(
-	"/html",
-	jwt,
-	// celebrate({
-	// 	body: Joi.object().keys({
-	// 		content: Joi.string().required().allow(""),
-	// 		title: Joi.string().required().allow(""),
-	// 	})
-	// }),
-	async (req, res, next) => {
-		const { content, title } = req.body
-		const renderedHtml = getHtmlFromFile({
-			content,
-			title
-		})
-
-		res.setHeader("Content-Type", "text/plain")
-		// res.setHeader("Cache-Control", "public, max-age=4800")
-		res.status(200).write(renderedHtml)
-		res.end()
-	}
-)
-
-files.get(
-	"/raw/:id",
-	celebrate({
-		params: {
-			id: Joi.string().required()
-		}
-	}),
-	secretKey,
-	async (req, res, next) => {
-		try {
-			const file = await File.findOne({
-				where: {
-					id: req.params.id
-				},
-				attributes: ["title", "content"]
-			})
-
-			if (!file) {
-				return res.status(404).json({ error: "File not found" })
-			}
-
-			// TODO: JWT-checkraw files
-			if (file?.post?.visibility === "private") {
-				// jwt(req as UserJwtRequest, res, () => {
-				//     res.json(file);
-				// })
-				res.json(file)
-			} else {
-				res.json(file)
-			}
-		} catch (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"]
-			})
-
-			if (!file) {
-				return res.status(404).json({ error: "File not found" })
-			}
-
-			res.setHeader("Content-Type", "text/plain")
-			res.setHeader("Cache-Control", "public, max-age=4800")
-			res.status(200).write(file.html)
-			res.end()
-		} catch (error) {
-			next(error)
-		}
-	}
-)
diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts
deleted file mode 100644
index 64821351..00000000
--- a/server/src/routes/health.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Router } from "express"
-
-export const health = Router()
-
-health.get("/", async (req, res) => {
-	return res.json({
-		status: "UP"
-	})
-})
diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts
deleted file mode 100644
index 72605d17..00000000
--- a/server/src/routes/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export { auth } from "./auth"
-export { posts } from "./posts"
-export { user } from "./user"
-export { files } from "./files"
-export { admin } from "./admin"
-export { health } from "./health"
diff --git a/server/src/routes/posts.ts b/server/src/routes/posts.ts
deleted file mode 100644
index 6f95ef69..00000000
--- a/server/src/routes/posts.ts
+++ /dev/null
@@ -1,512 +0,0 @@
-import { Router } from "express"
-import { celebrate, Joi } from "celebrate"
-import { File } from "@lib/models/File"
-import { Post } from "@lib/models/Post"
-import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
-import * as crypto from "crypto"
-import { User } from "@lib/models/User"
-import secretKey from "@lib/middleware/secret-key"
-import { Op } from "sequelize"
-import { PostAuthor } from "@lib/models/PostAuthor"
-import getHtmlFromFile from "@lib/get-html-from-drift-file"
-import { getGist, createPostFromGist } from "@lib/gist"
-
-export const posts = Router()
-
-const postVisibilitySchema = (value: string) => {
-	if (
-		value === "public" ||
-		value === "private" ||
-		value === "unlisted" ||
-		value === "protected"
-	) {
-		return value
-	} else {
-		throw new Error("Invalid post visibility")
-	}
-}
-
-posts.post(
-	"/create",
-	jwt,
-	celebrate({
-		body: {
-			title: Joi.string().required(),
-			description: Joi.string().optional().min(0).max(256),
-			files: Joi.any().required(),
-			visibility: Joi.string()
-				.custom(postVisibilitySchema, "valid visibility")
-				.required(),
-			userId: Joi.string().required(),
-			password: Joi.string().optional(),
-			//  expiresAt, allow to be null
-			expiresAt: Joi.date().optional().allow(null, ""),
-			parentId: Joi.string().optional().allow(null, "")
-		}
-	}),
-	async (req, res) => {
-		try {
-			// check if all files have titles
-			const files = req.body.files as File[]
-			const fileTitles = files.map((file) => file.title)
-			const missingTitles = fileTitles.filter((title) => title === "")
-			if (missingTitles.length > 0) {
-				throw new Error("All files must have a title")
-			}
-
-			if (files.length === 0) {
-				throw new Error("You must submit at least one file")
-			}
-
-			let hashedPassword: string = ""
-			if (req.body.visibility === "protected") {
-				hashedPassword = crypto
-					.createHash("sha256")
-					.update(req.body.password)
-					.digest("hex")
-			}
-
-			const newPost = new Post({
-				title: req.body.title,
-				description: req.body.description,
-				visibility: req.body.visibility,
-				password: hashedPassword,
-				expiresAt: req.body.expiresAt
-			})
-
-			await newPost.save()
-			await newPost.$add("users", req.body.userId)
-			const newFiles = await Promise.all(
-				files.map(async (file) => {
-					const html = getHtmlFromFile(file)
-					const newFile = new File({
-						title: file.title || "",
-						content: file.content,
-						sha: crypto
-							.createHash("sha256")
-							.update(file.content)
-							.digest("hex")
-							.toString(),
-						html: html || "",
-						userId: req.body.userId,
-						postId: newPost.id
-					})
-					await newFile.save()
-					return newFile
-				})
-			)
-
-			await Promise.all(
-				newFiles.map(async (file) => {
-					await newPost.$add("files", file.id)
-					await newPost.save()
-				})
-			)
-			if (req.body.parentId) {
-				// const parentPost = await Post.findOne({
-				// 	where: { id: req.body.parentId }
-				// })
-				// if (parentPost) {
-				// 	await parentPost.$add("children", newPost.id)
-				// 	await parentPost.save()
-				// }
-				const parentPost = await Post.findByPk(req.body.parentId)
-				if (parentPost) {
-					newPost.$set("parent", req.body.parentId)
-					await newPost.save()
-				} else {
-					throw new Error("Parent post not found")
-				}
-			}
-
-			res.json(newPost)
-		} catch (e) {
-			res.status(400).json(e)
-		}
-	}
-)
-
-posts.get("/", secretKey, async (req, res, next) => {
-	try {
-		const posts = await Post.findAll({
-			attributes: ["id", "title", "description", "visibility", "createdAt"]
-		})
-		res.json(posts)
-	} catch (e) {
-		next(e)
-	}
-})
-
-posts.get("/mine", jwt, async (req: UserJwtRequest, res, next) => {
-	if (!req.user) {
-		return res.status(401).json({ error: "Unauthorized" })
-	}
-
-	const page = parseInt(req.headers["x-page"]?.toString() || "1")
-
-	try {
-		const user = await User.findByPk(req.user.id, {
-			include: [
-				{
-					model: Post,
-					as: "posts",
-					include: [
-						{
-							model: File,
-							as: "files",
-							attributes: ["id", "title", "createdAt"]
-						},
-						{
-							model: Post,
-							as: "parent",
-							attributes: ["id", "title", "visibility"]
-						}
-					],
-					attributes: [
-						"id",
-						"title",
-						"description",
-						"visibility",
-						"createdAt",
-						"expiresAt"
-					]
-				}
-			]
-		})
-		if (!user) {
-			return res.status(404).json({ error: "User not found" })
-		}
-
-		const userPosts = user.posts
-		const sorted = userPosts?.sort((a, b) => {
-			return b.createdAt.getTime() - a.createdAt.getTime()
-		})
-
-		const paginated = sorted?.slice((page - 1) * 10, page * 10)
-
-		const hasMore =
-			paginated && sorted ? paginated.length < sorted.length : false
-
-		return res.json({
-			posts: paginated,
-			hasMore
-		})
-	} catch (error) {
-		next(error)
-	}
-})
-
-posts.get(
-	"/search",
-	jwt,
-	celebrate({
-		query: {
-			q: Joi.string().required()
-		}
-	}),
-	async (req: UserJwtRequest, res, next) => {
-		const { q } = req.query
-		if (typeof q !== "string") {
-			return res.status(400).json({ error: "Invalid query" })
-		}
-
-		try {
-			const posts = await Post.findAll({
-				where: {
-					[Op.or]: [
-						{ title: { [Op.like]: `%${q}%` } },
-						{ description: { [Op.like]: `%${q}%` } },
-						{ "$files.title$": { [Op.like]: `%${q}%` } },
-						{ "$files.content$": { [Op.like]: `%${q}%` } }
-					],
-					[Op.and]: [{ "$users.id$": req.user?.id || "" }]
-				},
-				include: [
-					{
-						model: File,
-						as: "files",
-						attributes: ["id", "title"]
-					},
-					{
-						model: User,
-						as: "users",
-						attributes: ["id", "username"]
-					},
-					{
-						model: Post,
-						as: "parent",
-						attributes: ["id", "title", "visibility"]
-					}
-				],
-				attributes: [
-					"id",
-					"title",
-					"description",
-					"visibility",
-					"createdAt",
-					"deletedAt"
-				],
-				order: [["createdAt", "DESC"]]
-			})
-
-			res.json(posts)
-		} catch (e) {
-			next(e)
-		}
-	}
-)
-
-const fullPostSequelizeOptions = {
-	include: [
-		{
-			model: File,
-			as: "files",
-			attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt"]
-		},
-		{
-			model: User,
-			as: "users",
-			attributes: ["id", "username"]
-		},
-		{
-			model: Post,
-			as: "parent",
-			attributes: ["id", "title", "visibility", "createdAt"]
-		}
-	],
-	attributes: [
-		"id",
-		"title",
-		"description",
-		"visibility",
-		"createdAt",
-		"updatedAt",
-		"deletedAt",
-		"expiresAt"
-	]
-}
-
-posts.get(
-	"/authenticate",
-	celebrate({
-		query: {
-			id: Joi.string().required(),
-			password: Joi.string().required()
-		}
-	}),
-	async (req, res, next) => {
-		const { id, password } = req.query
-
-		const post = await Post.findByPk(id?.toString(), {
-			...fullPostSequelizeOptions,
-			attributes: [...fullPostSequelizeOptions.attributes, "password"]
-		})
-
-		const hash = crypto
-			.createHash("sha256")
-			.update(password?.toString() || "")
-			.digest("hex")
-			.toString()
-
-		if (hash !== post?.password) {
-			return res.status(400).json({ error: "Incorrect password." })
-		}
-
-		res.json(post)
-	}
-)
-
-posts.get(
-	"/:id",
-	secretKey,
-	celebrate({
-		params: {
-			id: Joi.string().required()
-		}
-	}),
-	async (req: UserJwtRequest, res, next) => {
-		const isUserAuthor = (post: Post) => {
-			return (
-				req.user?.id &&
-				post.users?.map((user) => user.id).includes(req.user?.id)
-			)
-		}
-
-		try {
-			const post = await Post.findByPk(req.params.id, fullPostSequelizeOptions)
-
-			if (!post) {
-				return res.status(404).json({ error: "Post not found" })
-			}
-
-			// if public or unlisted, cache
-			if (post.visibility === "public" || post.visibility === "unlisted") {
-				res.set("Cache-Control", "public, max-age=4800")
-			}
-
-			if (post.visibility === "public" || post?.visibility === "unlisted") {
-				res.json(post)
-			} else if (post.visibility === "private") {
-				jwt(req as UserJwtRequest, res, () => {
-					if (isUserAuthor(post)) {
-						res.json(post)
-					} else {
-						res.status(403).send()
-					}
-				})
-			} else if (post.visibility === "protected") {
-				// The client ensures to not send the post to the client.
-				// See client/pages/post/[id].tsx::getServerSideProps
-				res.json(post)
-			}
-		} catch (e) {
-			res.status(400).json(e)
-		}
-	}
-)
-
-posts.delete("/:id", jwt, async (req: UserJwtRequest, res, next) => {
-	try {
-		const post = await Post.findByPk(req.params.id, {
-			include: [
-				{
-					model: User,
-					as: "users",
-					attributes: ["id"]
-				},
-				{
-					model: File,
-					as: "files",
-					attributes: ["id"]
-				}
-			]
-		})
-		if (!post) {
-			return res.status(404).json({ error: "Post not found" })
-		}
-
-		if (req.user?.id !== post.users![0].id) {
-			return res.status(403).json({ error: "Forbidden" })
-		}
-		if (post.files?.length)
-			await Promise.all(post.files.map((file) => file.destroy()))
-
-		const postAuthor = await PostAuthor.findOne({
-			where: {
-				postId: post.id
-			}
-		})
-		if (postAuthor) await postAuthor.destroy()
-		await post.destroy()
-		res.json({ message: "Post deleted" })
-	} catch (e) {
-		next(e)
-	}
-})
-
-posts.put(
-	"/:id",
-	jwt,
-	celebrate({
-		params: {
-			id: Joi.string().required()
-		},
-		body: {
-			visibility: Joi.string()
-				.custom(postVisibilitySchema, "valid visibility")
-				.required(),
-			password: Joi.string().optional()
-		}
-	}),
-	async (req: UserJwtRequest, res, next) => {
-		try {
-			const isUserAuthor = (post: Post) => {
-				return (
-					req.user?.id &&
-					post.users?.map((user) => user.id).includes(req.user?.id)
-				)
-			}
-
-			const { visibility, password } = req.body
-
-			let hashedPassword: string = ""
-			if (visibility === "protected") {
-				hashedPassword = crypto
-					.createHash("sha256")
-					.update(password)
-					.digest("hex")
-			}
-
-			const { id } = req.params
-			const post = await Post.findByPk(id, {
-				include: [
-					{
-						model: User,
-						as: "users",
-						attributes: ["id"]
-					}
-				]
-			})
-
-			if (!post) {
-				return res.status(404).json({ error: "Post not found" })
-			}
-
-			if (!isUserAuthor(post)) {
-				return res
-					.status(403)
-					.json({ error: "This post does not belong to you" })
-			}
-
-			await Post.update(
-				{ password: hashedPassword, visibility },
-				{ where: { id } }
-			)
-
-			res.json({ id, visibility })
-		} catch (e) {
-			res.status(400).json(e)
-		}
-	}
-)
-
-posts.post(
-	"/import/gist/id/:id",
-	jwt,
-	celebrate({
-		body: {
-			visibility: Joi.string()
-				.custom(postVisibilitySchema, "valid visibility")
-				.required(),
-			password: Joi.string().optional(),
-			expiresAt: Joi.date().optional().allow(null, "")
-		}
-	}),
-	async (req: UserJwtRequest, res, next) => {
-		try {
-			const { id } = req.params
-			const { visibility, password, expiresAt } = req.body
-			const gist = await getGist(id)
-
-			let hashedPassword: string = ""
-			if (visibility === "protected") {
-				hashedPassword = crypto
-					.createHash("sha256")
-					.update(password)
-					.digest("hex")
-			}
-			const newFile = await createPostFromGist(
-				{
-					userId: req.user!.id,
-					visibility,
-					password: hashedPassword,
-					expiresAt
-				},
-				gist
-			)
-			return res.json(newFile)
-		} catch (e) {
-			res.status(400).json({ error: e.toString() })
-		}
-	}
-)
diff --git a/server/src/routes/user.ts b/server/src/routes/user.ts
deleted file mode 100644
index 46368d40..00000000
--- a/server/src/routes/user.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { Router } from "express"
-import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
-import { User } from "@lib/models/User"
-import { celebrate, Joi } from "celebrate"
-
-export const user = Router()
-
-user.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
-	const error = () =>
-		res.status(401).json({
-			message: "Unauthorized"
-		})
-
-	try {
-		if (!req.user) {
-			return error()
-		}
-
-		const user = await User.findByPk(req.user?.id, {
-			attributes: {
-				exclude: ["password"]
-			}
-		})
-		if (!user) {
-			return error()
-		}
-		res.json(user)
-	} catch (error) {
-		next(error)
-	}
-})
-
-user.put(
-	"/profile",
-	jwt,
-	celebrate({
-		body: {
-			displayName: Joi.string().optional().allow(""),
-			bio: Joi.string().optional().allow(""),
-			email: Joi.string().optional().email().allow("")
-		}
-	}),
-	async (req: UserJwtRequest, res, next) => {
-		const error = () =>
-			res.status(401).json({
-				message: "Unauthorized"
-			})
-
-		try {
-			if (!req.user) {
-				return error()
-			}
-
-			const user = await User.findByPk(req.user?.id)
-			if (!user) {
-				return error()
-			}
-
-			const { displayName, bio, email } = req.body
-			const toUpdate = {} as any
-			if (displayName) {
-				toUpdate.displayName = displayName
-			}
-			if (bio) {
-				toUpdate.bio = bio
-			}
-			if (email) {
-				toUpdate.email = email
-			}
-
-			await user.update(toUpdate)
-			res.json(user)
-		} catch (error) {
-			next(error)
-		}
-	}
-)
diff --git a/server/test/setup-tests.ts b/server/test/setup-tests.ts
deleted file mode 100644
index fe68937d..00000000
--- a/server/test/setup-tests.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import * as dotenv from 'dotenv';
-import * as path from 'path';
-
-dotenv.config({ path: path.resolve(process.cwd(), '.env.test') });