rm server/

This commit is contained in:
Max Leiter 2022-11-14 18:46:24 -08:00
parent 37d4dfebcf
commit aef1788747
12 changed files with 2 additions and 1235 deletions

View file

@ -1,8 +1,8 @@
# <img src="client/public/assets/logo.png" height="32px" alt="" /> 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).
<hr />

View file

@ -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()
})
})
})

View file

@ -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)
})
})
})

View file

@ -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 <h2><a href=\"#my-markdown\" id=\"my-markdown\" style=\"color:inherit\">My Markdown</a></h2>,
// but we dont wan't to be too strict in case markup changes
expect(html).toMatch(/<h2><a.*<\/a><\/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(/<pre><code class="prism-code language-js">/)
})
})

View file

@ -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);
}
});

View file

@ -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)
}
}
)

View file

@ -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)
}
}
)

View file

@ -1,9 +0,0 @@
import { Router } from "express"
export const health = Router()
health.get("/", async (req, res) => {
return res.json({
status: "UP"
})
})

View file

@ -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"

View file

@ -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() })
}
}
)

View file

@ -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)
}
}
)

View file

@ -1,4 +0,0 @@
import * as dotenv from 'dotenv';
import * as path from 'path';
dotenv.config({ path: path.resolve(process.cwd(), '.env.test') });