diff --git a/README.md b/README.md
index 77bceb5e..c725e157 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,12 @@ Drift is a self-hostable Gist clone. It's also a major work-in-progress, but is
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.
-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).
+
**Contents:**
+
- [Setup](#setup)
- [Development](#development)
- [Production](#production)
@@ -27,7 +29,7 @@ To migrate the sqlite database in development, you can use `yarn migrate` to see
### Production
-`yarn build` in both `client/` and `server/` will produce production code for the client and server respectively.
+`yarn build` in both `client/` and `server/` will produce production code for the client and server respectively.
If you're deploying the front-end to something like Vercel, you'll need to set the root folder to `client/`.
@@ -50,8 +52,6 @@ You can change these to your liking.
- `MEMORY_DB`: if `true`, a sqlite database will not be created and changes will only exist in memory. Mainly for the demo.
- `REGISTRATION_PASSWORD`: if `true`, the user will be required to provide this password to sign-up, in addition to their username and account password. If it's not set, no additional password will be required.
- `SECRET_KEY`: the same secret key as the client
-- `WELCOME_CONTENT`: a markdown string that's rendered on the home page
-- `WELCOME_TITLE`: the file title for the post on the homepage.
- `ENABLE_ADMIN`: the first account created is an administrator account
- `DRIFT_HOME`: defaults to ~/.drift, the directory for storing the database and eventually images
diff --git a/client/components/admin/action-dropdown/index.tsx b/client/components/admin/action-dropdown/index.tsx
index 48c57335..41516896 100644
--- a/client/components/admin/action-dropdown/index.tsx
+++ b/client/components/admin/action-dropdown/index.tsx
@@ -2,42 +2,37 @@ import { Popover, Button } from "@geist-ui/core"
import { MoreVertical } from "@geist-ui/icons"
type Action = {
- title: string
- onClick: () => void
+ title: string
+ onClick: () => void
}
const ActionDropdown = ({
- title = "Actions",
- actions,
- showTitle = false,
+ title = "Actions",
+ actions,
+ showTitle = false
}: {
- title?: string,
- showTitle?: boolean,
- actions: Action[]
+ title?: string
+ showTitle?: boolean
+ actions: Action[]
}) => {
- return (
-
- {showTitle &&
- {title}
- }
- {actions.map(action => (
-
- {action.title}
-
- ))}
- >
- }
- hideArrow
- >
- } auto>
-
- )
+ return (
+
+ {showTitle && {title} }
+ {actions.map((action) => (
+
+ {action.title}
+
+ ))}
+ >
+ }
+ hideArrow
+ >
+ } auto>
+
+ )
}
-export default ActionDropdown
\ No newline at end of file
+export default ActionDropdown
diff --git a/client/components/admin/index.tsx b/client/components/admin/index.tsx
index 338babe9..bb09b59d 100644
--- a/client/components/admin/index.tsx
+++ b/client/components/admin/index.tsx
@@ -2,6 +2,7 @@ import { Text, Spacer } from "@geist-ui/core"
import Cookies from "js-cookie"
import styles from "./admin.module.css"
import PostTable from "./post-table"
+import ServerInfo from "./server-info"
import UserTable from "./user-table"
export const adminFetcher = async (
@@ -24,6 +25,8 @@ const Admin = () => {
return (
Administration
+
+
diff --git a/client/components/admin/post-table.tsx b/client/components/admin/post-table.tsx
index c66f99ea..a7f5c4a6 100644
--- a/client/components/admin/post-table.tsx
+++ b/client/components/admin/post-table.tsx
@@ -9,7 +9,7 @@ import ActionDropdown from "./action-dropdown"
const PostTable = () => {
const [posts, setPosts] = useState
()
- const { setToast } = useToasts()
+ // const { setToast } = useToasts()
useEffect(() => {
const fetchPosts = async () => {
@@ -35,8 +35,8 @@ const PostTable = () => {
visibility: post.visibility,
size: post.files
? byteToMB(
- post.files.reduce((acc, file) => acc + file.html.length, 0)
- )
+ post.files.reduce((acc, file) => acc + file.html.length, 0)
+ )
: 0,
actions: ""
}
@@ -109,14 +109,14 @@ const PostTable = () => {
dataIndex: "",
key: "actions",
width: 50,
- render(post: Post) {
+ render() {
return (
deletePost(post.id)
+ onClick: () => deletePost()
}
]}
/>
@@ -128,7 +128,11 @@ const PostTable = () => {
return (
{!posts && Loading... }
- {posts && {posts.length} posts }
+ {posts && (
+
+ {posts.length} posts
+
+ )}
{posts && }
)
diff --git a/client/components/admin/server-info.tsx b/client/components/admin/server-info.tsx
new file mode 100644
index 00000000..2b7bf96b
--- /dev/null
+++ b/client/components/admin/server-info.tsx
@@ -0,0 +1,75 @@
+import SettingsGroup from "@components/settings-group"
+import { Button, Input, Spacer, Textarea, useToasts } from "@geist-ui/core"
+import { useEffect, useState } from "react"
+import { adminFetcher } from "."
+
+const Homepage = () => {
+ const [description, setDescription] = useState("")
+ const [title, setTitle] = useState("")
+ const { setToast } = useToasts()
+ useEffect(() => {
+ const fetchServerInfo = async () => {
+ const res = await adminFetcher("/server-info")
+ const data = await res.json()
+ setDescription(data.welcomeMessage)
+ setTitle(data.welcomeTitle)
+ }
+ fetchServerInfo()
+ }, [])
+
+ const onSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ const res = await adminFetcher("/server-info", {
+ method: "PUT",
+ body: {
+ description,
+ title
+ }
+ })
+
+ if (res.status === 200) {
+ setToast({
+ type: "success",
+ text: "Server info updated"
+ })
+ setDescription(description)
+ setTitle(title)
+ } else {
+ setToast({
+ text: "Something went wrong",
+ type: "error"
+ })
+ }
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default Homepage
diff --git a/client/components/admin/user-table.tsx b/client/components/admin/user-table.tsx
index e8beb710..b931db70 100644
--- a/client/components/admin/user-table.tsx
+++ b/client/components/admin/user-table.tsx
@@ -131,7 +131,8 @@ const UserTable = () => {
actions={[
{
title: user.role === "admin" ? "Change role" : "Make admin",
- onClick: () => toggleRole(user.id, user.role === "admin" ? "user" : "admin")
+ onClick: () =>
+ toggleRole(user.id, user.role === "admin" ? "user" : "admin")
},
{
title: "Delete",
@@ -147,7 +148,11 @@ const UserTable = () => {
return (
{!users && Loading... }
- {users && {users.length} users }
+ {users && (
+
+ {users.length} users
+
+ )}
{users && }
)
diff --git a/client/components/header/index.tsx b/client/components/header/index.tsx
index 7b5efdef..962d9938 100644
--- a/client/components/header/index.tsx
+++ b/client/components/header/index.tsx
@@ -89,10 +89,10 @@ const Header = () => {
href: "/mine"
},
{
- name: 'settings',
+ name: "settings",
icon: ,
- value: 'settings',
- href: '/settings'
+ value: "settings",
+ href: "/settings"
},
{
name: "sign out",
diff --git a/client/components/new-post/index.tsx b/client/components/new-post/index.tsx
index 8814c12c..5148e6ba 100644
--- a/client/components/new-post/index.tsx
+++ b/client/components/new-post/index.tsx
@@ -1,9 +1,4 @@
-import {
- Button,
- useToasts,
- ButtonDropdown,
- Input,
-} from "@geist-ui/core"
+import { Button, useToasts, ButtonDropdown, Input } from "@geist-ui/core"
import { useRouter } from "next/router"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import generateUUID from "@lib/generate-uuid"
diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx
index ccd94275..0ddcf97e 100644
--- a/client/components/post-list/list-item.tsx
+++ b/client/components/post-list/list-item.tsx
@@ -97,10 +97,7 @@ const ListItem = ({
{post.files?.map((file: File) => {
return (
-
+
{file.title || "Untitled file"}
diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx
index 3ddc8155..fa9c0f55 100644
--- a/client/components/post-page/index.tsx
+++ b/client/components/post-page/index.tsx
@@ -85,9 +85,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
}
const viewParentClick = () => {
- router.push(
- `/post/${post.parent!.id}`
- )
+ router.push(`/post/${post.parent!.id}`)
}
if (isLoading) {
@@ -123,11 +121,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
Edit a Copy
{post.parent && (
- }
- onClick={viewParentClick}
- >
+ } onClick={viewParentClick}>
View Parent
)}
diff --git a/client/components/settings-group/index.tsx b/client/components/settings-group/index.tsx
index adf9affd..9325dce9 100644
--- a/client/components/settings-group/index.tsx
+++ b/client/components/settings-group/index.tsx
@@ -1,24 +1,21 @@
import { Fieldset, Text, Divider } from "@geist-ui/core"
-import styles from './settings-group.module.css'
+import styles from "./settings-group.module.css"
type Props = {
- title: string,
- children: React.ReactNode | React.ReactNode[],
+ title: string
+ children: React.ReactNode | React.ReactNode[]
}
-const SettingsGroup = ({
- title,
- children,
-}: Props) => {
- return
-
- {title}
-
-
-
- {children}
-
-
+const SettingsGroup = ({ title, children }: Props) => {
+ return (
+
+
+ {title}
+
+
+ {children}
+
+ )
}
export default SettingsGroup
diff --git a/client/components/settings/index.tsx b/client/components/settings/index.tsx
index e6ed9215..9eaf8b20 100644
--- a/client/components/settings/index.tsx
+++ b/client/components/settings/index.tsx
@@ -3,20 +3,24 @@ import Profile from "./sections/profile"
import SettingsGroup from "../settings-group"
const SettingsPage = () => {
- return ()
+ return (
+
+ )
}
-export default SettingsPage
\ No newline at end of file
+export default SettingsPage
diff --git a/client/components/settings/sections/password.tsx b/client/components/settings/sections/password.tsx
index 4f46a3c1..9ef92770 100644
--- a/client/components/settings/sections/password.tsx
+++ b/client/components/settings/sections/password.tsx
@@ -3,95 +3,132 @@ import Cookies from "js-cookie"
import { useState } from "react"
const Password = () => {
- const [password, setPassword] = useState('')
- const [newPassword, setNewPassword] = useState('')
- const [confirmPassword, setConfirmPassword] = useState('')
+ const [password, setPassword] = useState("")
+ const [newPassword, setNewPassword] = useState("")
+ const [confirmPassword, setConfirmPassword] = useState("")
- const { setToast } = useToasts()
+ const { setToast } = useToasts()
- const handlePasswordChange = (e: React.ChangeEvent) => {
- setPassword(e.target.value)
- }
+ const handlePasswordChange = (e: React.ChangeEvent) => {
+ setPassword(e.target.value)
+ }
- const handleNewPasswordChange = (e: React.ChangeEvent) => {
- setNewPassword(e.target.value)
- }
+ const handleNewPasswordChange = (e: React.ChangeEvent) => {
+ setNewPassword(e.target.value)
+ }
- const handleConfirmPasswordChange = (e: React.ChangeEvent) => {
- setConfirmPassword(e.target.value)
- }
+ const handleConfirmPasswordChange = (
+ e: React.ChangeEvent
+ ) => {
+ setConfirmPassword(e.target.value)
+ }
- const onSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- if (!password || !newPassword || !confirmPassword) {
- setToast({
- text: "Please fill out all fields",
- type: "error",
- })
- }
+ const onSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!password || !newPassword || !confirmPassword) {
+ setToast({
+ text: "Please fill out all fields",
+ type: "error"
+ })
+ }
- if (newPassword !== confirmPassword) {
- setToast({
- text: "New password and confirm password do not match",
- type: "error",
- })
- }
+ if (newPassword !== confirmPassword) {
+ setToast({
+ text: "New password and confirm password do not match",
+ type: "error"
+ })
+ }
- const res = await fetch("/server-api/auth/change-password", {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- "Authorization": `Bearer ${Cookies.get("drift-token")}`,
- },
- body: JSON.stringify({
- oldPassword: password,
- newPassword,
- }),
- })
+ const res = await fetch("/server-api/auth/change-password", {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${Cookies.get("drift-token")}`
+ },
+ body: JSON.stringify({
+ oldPassword: password,
+ newPassword
+ })
+ })
+ if (res.status === 200) {
+ setToast({
+ text: "Password updated successfully",
+ type: "success"
+ })
+ setPassword("")
+ setNewPassword("")
+ setConfirmPassword("")
+ } else {
+ const data = await res.json()
- if (res.status === 200) {
- setToast({
- text: "Password updated successfully",
- type: "success",
- })
- setPassword('')
- setNewPassword('')
- setConfirmPassword('')
- } else {
- const data = await res.json()
+ setToast({
+ text: data.error ?? "Failed to update password",
+ type: "error"
+ })
+ }
+ }
- setToast({
- text: data.error ?? "Failed to update password",
- type: "error",
- })
- }
- }
-
- return (
-
-
- Current password
-
-
-
- New password
-
-
-
- Confirm password
-
-
- Change Password
- )
+ return (
+
+
+ Current password
+
+
+
+ New password
+
+
+
+ Confirm password
+
+
+
+ Change Password
+
+
+ )
}
-export default Password
\ No newline at end of file
+export default Password
diff --git a/client/components/settings/sections/profile.tsx b/client/components/settings/sections/profile.tsx
index ed95851a..90366d2a 100644
--- a/client/components/settings/sections/profile.tsx
+++ b/client/components/settings/sections/profile.tsx
@@ -4,97 +4,121 @@ import Cookies from "js-cookie"
import { useEffect, useState } from "react"
const Profile = () => {
- const user = useUserData()
- const [name, setName] = useState()
- const [email, setEmail] = useState()
- const [bio, setBio] = useState()
+ const user = useUserData()
+ const [name, setName] = useState()
+ const [email, setEmail] = useState()
+ const [bio, setBio] = useState()
- useEffect(() => {
- console.log(user)
- if (user?.displayName) setName(user.displayName)
- if (user?.email) setEmail(user.email)
- if (user?.bio) setBio(user.bio)
- }, [user])
+ useEffect(() => {
+ if (user?.displayName) setName(user.displayName)
+ if (user?.email) setEmail(user.email)
+ if (user?.bio) setBio(user.bio)
+ }, [user])
- const { setToast } = useToasts()
+ const { setToast } = useToasts()
- const handleNameChange = (e: React.ChangeEvent) => {
- setName(e.target.value)
- }
+ const handleNameChange = (e: React.ChangeEvent) => {
+ setName(e.target.value)
+ }
- const handleEmailChange = (e: React.ChangeEvent) => {
- setEmail(e.target.value)
- }
+ const handleEmailChange = (e: React.ChangeEvent) => {
+ setEmail(e.target.value)
+ }
- const handleBioChange = (e: React.ChangeEvent) => {
- setBio(e.target.value)
- }
+ const handleBioChange = (e: React.ChangeEvent) => {
+ setBio(e.target.value)
+ }
- const onSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- if (!name && !email && !bio) {
- setToast({
- text: "Please fill out at least one field",
- type: "error",
- })
- return
- }
+ const onSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!name && !email && !bio) {
+ setToast({
+ text: "Please fill out at least one field",
+ type: "error"
+ })
+ return
+ }
- const data = {
- displayName: name,
- email,
- bio,
- }
+ const data = {
+ displayName: name,
+ email,
+ bio
+ }
- const res = await fetch("/server-api/user/profile", {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- "Authorization": `Bearer ${Cookies.get("drift-token")}`,
- },
- body: JSON.stringify(data),
- })
+ const res = await fetch("/server-api/user/profile", {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${Cookies.get("drift-token")}`
+ },
+ body: JSON.stringify(data)
+ })
- if (res.status === 200) {
- setToast({
- text: "Profile updated",
- type: "success",
- })
- } else {
- setToast({
- text: "Something went wrong updating your profile",
- type: "error",
- })
- }
- }
+ if (res.status === 200) {
+ setToast({
+ text: "Profile updated",
+ type: "success"
+ })
+ } else {
+ setToast({
+ text: "Something went wrong updating your profile",
+ type: "error"
+ })
+ }
+ }
- return (<>
-
- This information will be publicly available on your profile
-
-
-
- Display name
-
-
-
- Email
-
-
-
- Biography (max 250 characters)
-
-
- Submit
- >)
+ return (
+ <>
+
+ This information will be publicly available on your profile
+
+
+
+ Display name
+
+
+
+ Email
+
+
+
+ Biography (max 250 characters)
+
+
+
+ Submit
+
+
+ >
+ )
}
-export default Profile
\ No newline at end of file
+export default Profile
diff --git a/client/pages/index.tsx b/client/pages/index.tsx
index 048a0fca..e560ed4a 100644
--- a/client/pages/index.tsx
+++ b/client/pages/index.tsx
@@ -3,14 +3,16 @@ import PageSeo from "@components/page-seo"
import HomeComponent from "@components/home"
import { Page, Text } from "@geist-ui/core"
import type { GetStaticProps } from "next"
-import { InferGetStaticPropsType } from 'next'
-type Props = {
- introContent: string
- introTitle: string
- rendered: string
-} | {
- error: boolean
-}
+import { InferGetStaticPropsType } from "next"
+type Props =
+ | {
+ introContent: string
+ introTitle: string
+ rendered: string
+ }
+ | {
+ error: boolean
+ }
export const getStaticProps: GetStaticProps = async () => {
try {
@@ -32,21 +34,26 @@ export const getStaticProps: GetStaticProps = async () => {
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most every 60 seconds
- revalidate: 60, // In seconds
+ revalidate: 60 // In seconds
}
} catch (err) {
// If there was an error, it's likely due to the server not running, so we attempt to regenerate the page
return {
props: {
- error: true,
+ error: true
},
- revalidate: 10, // In seconds
+ revalidate: 10 // In seconds
}
}
}
// TODO: fix props type
-const Home = ({ rendered, introContent, introTitle, error }: InferGetStaticPropsType) => {
+const Home = ({
+ rendered,
+ introContent,
+ introTitle,
+ error
+}: InferGetStaticPropsType) => {
return (
diff --git a/client/pages/post/[id].tsx b/client/pages/post/[id].tsx
index 10531bac..24894667 100644
--- a/client/pages/post/[id].tsx
+++ b/client/pages/post/[id].tsx
@@ -69,7 +69,6 @@ export const getServerSideProps: GetServerSideProps = async ({
}
}
-
return {
props: {
post: json,
diff --git a/client/pages/settings.tsx b/client/pages/settings.tsx
index ce40051e..9f2a2cb0 100644
--- a/client/pages/settings.tsx
+++ b/client/pages/settings.tsx
@@ -1,15 +1,27 @@
-import { Button, Divider, Text, Fieldset, Input, Page, Note, Textarea } from "@geist-ui/core"
+import {
+ Button,
+ Divider,
+ Text,
+ Fieldset,
+ Input,
+ Page,
+ Note,
+ Textarea
+} from "@geist-ui/core"
import PageSeo from "@components/page-seo"
import styles from "@styles/Home.module.css"
import SettingsPage from "@components/settings"
const Settings = () => (
-
-
-
-
-
-
+
+
+
+
+
+
)
export default Settings
diff --git a/server/src/app.ts b/server/src/app.ts
index cbb60c92..2929a1b5 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -6,6 +6,7 @@ import { errors } from "celebrate"
import secretKey from "@lib/middleware/secret-key"
import markdown from "@lib/render-markdown"
import config from "@lib/config"
+import { ServerInfo } from "@lib/models/ServerInfo"
export const app = express()
@@ -19,17 +20,25 @@ app.use("/files", files)
app.use("/admin", admin)
app.use("/health", health)
-app.get("/welcome", secretKey, (req, res) => {
- const introContent = config.welcome_content
- const introTitle = config.welcome_title
- if (!introContent || !introTitle) {
- return res.status(500).json({ error: "Missing welcome content" })
+app.get("/welcome", secretKey, async (req, res) => {
+ const serverInfo = await ServerInfo.findOne({
+ where: {
+ id: "1"
+ }
+ })
+
+ if (!serverInfo) {
+ return res.status(500).json({
+ message: "Server info not found."
+ })
}
+ const { welcomeMessage, welcomeTitle } = serverInfo
+
return res.json({
- title: introTitle,
- content: introContent,
- rendered: markdown(introContent)
+ title: welcomeTitle,
+ content: welcomeMessage,
+ rendered: markdown(welcomeMessage)
})
})
diff --git a/server/src/database.ts b/server/src/database.ts
index 17f0bc31..72fe7c96 100644
--- a/server/src/database.ts
+++ b/server/src/database.ts
@@ -2,6 +2,7 @@ import config from "@lib/config"
import databasePath from "@lib/get-database-path"
import { Sequelize } from "sequelize-typescript"
import { SequelizeStorage, Umzug } from "umzug"
+import { QueryTypes } from "sequelize"
export const sequelize = new Sequelize({
dialect: "sqlite",
@@ -11,10 +12,24 @@ export const sequelize = new Sequelize({
logging: console.log
})
-if (config.memory_db) {
- console.log("Using in-memory database")
-} else {
- console.log(`Database path: ${databasePath}`)
+export const initServerInfo = async () => {
+ const serverInfo = await sequelize.query(
+ "SELECT * FROM `server-info` WHERE id = '1'",
+ {
+ type: QueryTypes.SELECT
+ }
+ )
+
+ if (serverInfo.length === 0) {
+ console.log("server-info table not found, creating...")
+ console.log(
+ "You can change the welcome message and title on the admin settings page."
+ )
+ await sequelize.query("INSERT INTO `server-info` (id) VALUES ('1')", {
+ type: QueryTypes.INSERT
+ })
+ console.log("server-info table created.")
+ }
}
export const umzug = new Umzug({
@@ -30,6 +45,12 @@ export const umzug = new Umzug({
export type Migration = typeof umzug._types.migration
+if (config.memory_db) {
+ console.log("Using in-memory database")
+} else {
+ console.log(`Database path: ${databasePath}`)
+}
+
// If you're in a development environment, you can manually migrate with `yarn migrate:{up,down}` in the `server` folder
if (config.is_production) {
;(async () => {
diff --git a/server/src/lib/models/ServerInfo.ts b/server/src/lib/models/ServerInfo.ts
new file mode 100644
index 00000000..7efb4803
--- /dev/null
+++ b/server/src/lib/models/ServerInfo.ts
@@ -0,0 +1,33 @@
+import {
+ Model,
+ Column,
+ Table,
+ IsUUID,
+ PrimaryKey,
+ DataType,
+ Unique
+} from "sequelize-typescript"
+
+@Table({
+ tableName: "server-info"
+})
+export class ServerInfo extends Model {
+ @IsUUID(4)
+ @PrimaryKey
+ @Unique
+ @Column({
+ type: DataType.UUID,
+ defaultValue: DataType.UUIDV4
+ })
+ id!: string
+
+ @Column({
+ type: DataType.STRING
+ })
+ welcomeMessage!: string
+
+ @Column({
+ type: DataType.STRING
+ })
+ welcomeTitle!: string
+}
diff --git a/server/src/migrations/09_add_more_user_settings.ts b/server/src/migrations/09_add_more_user_settings.ts
index 4624306e..9547530a 100644
--- a/server/src/migrations/09_add_more_user_settings.ts
+++ b/server/src/migrations/09_add_more_user_settings.ts
@@ -3,24 +3,24 @@ import { DataTypes } from "sequelize"
import type { Migration } from "../database"
export const up: Migration = async ({ context: queryInterface }) =>
- Promise.all([
- queryInterface.addColumn("users", "email", {
- type: DataTypes.STRING,
- allowNull: true
- }),
- queryInterface.addColumn("users", "displayName", {
- type: DataTypes.STRING,
- allowNull: true,
- }),
- queryInterface.addColumn("users", "bio", {
- type: DataTypes.STRING,
- allowNull: true,
- }),
- ])
+ Promise.all([
+ queryInterface.addColumn("users", "email", {
+ type: DataTypes.STRING,
+ allowNull: true
+ }),
+ queryInterface.addColumn("users", "displayName", {
+ type: DataTypes.STRING,
+ allowNull: true
+ }),
+ queryInterface.addColumn("users", "bio", {
+ type: DataTypes.STRING,
+ allowNull: true
+ })
+ ])
export const down: Migration = async ({ context: queryInterface }) =>
- Promise.all([
- queryInterface.removeColumn("users", "email"),
- queryInterface.removeColumn("users", "displayName"),
- queryInterface.removeColumn("users", "bio"),
- ])
+ Promise.all([
+ queryInterface.removeColumn("users", "email"),
+ queryInterface.removeColumn("users", "displayName"),
+ queryInterface.removeColumn("users", "bio")
+ ])
diff --git a/server/src/migrations/10_add_server_info_table.ts b/server/src/migrations/10_add_server_info_table.ts
new file mode 100644
index 00000000..f9f79a39
--- /dev/null
+++ b/server/src/migrations/10_add_server_info_table.ts
@@ -0,0 +1,37 @@
+"use strict"
+import { DataTypes } from "sequelize"
+import type { Migration } from "../database"
+
+export const up: Migration = async ({ context: queryInterface }) =>
+ queryInterface.createTable("server-info", {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ defaultValue: 1
+ },
+ welcomeMessage: {
+ type: DataTypes.TEXT,
+ defaultValue:
+ "## Drift is a self-hostable clone of GitHub Gist. \nIt is a simple way to share code and text snippets with your friends, with support for the following:\n \n - Render GitHub Extended Markdown (including images)\n - User authentication\n - Private, public, and password protected posts\n - Markdown is rendered and stored on the server\n - Syntax highlighting and automatic language detection\n - Drag-and-drop file uploading\n\n If you want to signup, you can join at [/signup](/signup) as long as you have a passcode provided by the administrator (which you don't need for this demo). **This demo is on a memory-only database, so accounts and pastes can be deleted at any time.** \n\nYou can find the source code on [GitHub](https://github.com/MaxLeiter/drift).",
+ allowNull: true
+ },
+ welcomeTitle: {
+ type: DataTypes.TEXT,
+ defaultValue: "Welcome to Drift",
+ allowNull: true
+ },
+ createdAt: {
+ type: DataTypes.DATE,
+ defaultValue: DataTypes.NOW,
+ allowNull: true
+ },
+ updatedAt: {
+ type: DataTypes.DATE,
+ defaultValue: DataTypes.NOW,
+ allowNull: true
+ }
+ })
+
+export const down: Migration = async ({ context: queryInterface }) =>
+ queryInterface.dropTable("server-info")
diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts
index 026e4b81..665df204 100644
--- a/server/src/routes/admin.ts
+++ b/server/src/routes/admin.ts
@@ -4,6 +4,7 @@ import { User } from "@lib/models/User"
import { File } from "@lib/models/File"
import { Router } from "express"
import { celebrate, Joi } from "celebrate"
+import { ServerInfo } from "@lib/models/ServerInfo"
export const admin = Router()
@@ -197,3 +198,54 @@ admin.delete("/post/:id", async (req, res, next) => {
next(e)
}
})
+
+admin.get("/server-info", async (req, res, next) => {
+ try {
+ const info = await ServerInfo.findOne({
+ where: {
+ id: 1
+ }
+ })
+
+ res.json(info)
+ } catch (e) {
+ next(e)
+ }
+})
+
+admin.put(
+ "/server-info",
+ celebrate({
+ body: {
+ title: Joi.string().required(),
+ description: Joi.string().required()
+ }
+ }),
+ async (req, res, next) => {
+ try {
+ const { description, title } = req.body
+ const serverInfo = await ServerInfo.findOne({
+ where: {
+ id: 1
+ }
+ })
+
+ if (!serverInfo) {
+ return res.status(404).json({
+ error: "Server info not found"
+ })
+ }
+
+ await serverInfo.update({
+ welcomeMessage: description,
+ welcomeTitle: title
+ })
+
+ res.json({
+ success: true
+ })
+ } catch (e) {
+ next(e)
+ }
+ }
+)
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
index 742adcd2..ca763598 100644
--- a/server/src/routes/auth.ts
+++ b/server/src/routes/auth.ts
@@ -195,7 +195,8 @@ auth.post("/signout", secretKey, async (req, res, next) => {
}
})
-auth.put("/change-password",
+auth.put(
+ "/change-password",
jwt,
celebrate({
body: {
diff --git a/server/src/routes/user.ts b/server/src/routes/user.ts
index e4226a5f..46368d40 100644
--- a/server/src/routes/user.ts
+++ b/server/src/routes/user.ts
@@ -30,13 +30,14 @@ user.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
}
})
-user.put("/profile",
+user.put(
+ "/profile",
jwt,
celebrate({
body: {
displayName: Joi.string().optional().allow(""),
bio: Joi.string().optional().allow(""),
- email: Joi.string().optional().email().allow(""),
+ email: Joi.string().optional().email().allow("")
}
}),
async (req: UserJwtRequest, res, next) => {
diff --git a/server/src/server.ts b/server/src/server.ts
index 2d6db754..abd42e11 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -2,8 +2,10 @@ import { createServer } from "http"
import { app } from "./app"
import config from "./lib/config"
import "./database"
+import { initServerInfo } from "./database"
+
;(async () => {
- // await sequelize.sync()
+ initServerInfo()
createServer(app).listen(config.port, () =>
console.info(`Server running on port ${config.port}`)
)