rm server/
This commit is contained in:
parent
37d4dfebcf
commit
aef1788747
12 changed files with 2 additions and 1235 deletions
|
@ -1,8 +1,8 @@
|
||||||
# <img src="client/public/assets/logo.png" height="32px" alt="" /> Drift
|
# <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).
|
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 />
|
<hr />
|
||||||
|
|
|
@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -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">/)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { Router } from "express"
|
|
||||||
|
|
||||||
export const health = Router()
|
|
||||||
|
|
||||||
health.get("/", async (req, res) => {
|
|
||||||
return res.json({
|
|
||||||
status: "UP"
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -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"
|
|
|
@ -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() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -1,4 +0,0 @@
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
dotenv.config({ path: path.resolve(process.cwd(), '.env.test') });
|
|
Loading…
Reference in a new issue