Revert "client/server: lint and add functionality for admin to update homepage"

This reverts commit b5024e3f45.
This commit is contained in:
Max Leiter 2022-04-20 01:52:07 -07:00
parent b5024e3f45
commit bceeb5cee8
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG key ID: A3512F2F2F17EBDA
26 changed files with 313 additions and 617 deletions

View file

@ -4,12 +4,10 @@ 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. 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).
<hr /> <hr />
**Contents:** **Contents:**
- [Setup](#setup) - [Setup](#setup)
- [Development](#development) - [Development](#development)
- [Production](#production) - [Production](#production)
@ -29,7 +27,7 @@ To migrate the sqlite database in development, you can use `yarn migrate` to see
### Production ### 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/`. If you're deploying the front-end to something like Vercel, you'll need to set the root folder to `client/`.
@ -52,6 +50,8 @@ 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. - `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. - `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 - `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 - `ENABLE_ADMIN`: the first account created is an administrator account
- `DRIFT_HOME`: defaults to ~/.drift, the directory for storing the database and eventually images - `DRIFT_HOME`: defaults to ~/.drift, the directory for storing the database and eventually images

View file

@ -2,37 +2,42 @@ import { Popover, Button } from "@geist-ui/core"
import { MoreVertical } from "@geist-ui/icons" import { MoreVertical } from "@geist-ui/icons"
type Action = { type Action = {
title: string title: string
onClick: () => void onClick: () => void
} }
const ActionDropdown = ({ const ActionDropdown = ({
title = "Actions", title = "Actions",
actions, actions,
showTitle = false showTitle = false,
}: { }: {
title?: string title?: string,
showTitle?: boolean showTitle?: boolean,
actions: Action[] actions: Action[]
}) => { }) => {
return ( return (
<Popover <Popover
title={title} title={title}
content={ content={
<> <>
{showTitle && <Popover.Item title>{title}</Popover.Item>} {showTitle && <Popover.Item title>
{actions.map((action) => ( {title}
<Popover.Item onClick={action.onClick} key={action.title}> </Popover.Item>}
{action.title} {actions.map(action => (
</Popover.Item> <Popover.Item
))} onClick={action.onClick}
</> key={action.title}
} >
hideArrow {action.title}
> </Popover.Item>
<Button iconRight={<MoreVertical />} auto></Button> ))}
</Popover> </>
) }
hideArrow
>
<Button iconRight={<MoreVertical />} auto></Button>
</Popover>
)
} }
export default ActionDropdown export default ActionDropdown

View file

@ -2,7 +2,6 @@ import { Text, Spacer } from "@geist-ui/core"
import Cookies from "js-cookie" import Cookies from "js-cookie"
import styles from "./admin.module.css" import styles from "./admin.module.css"
import PostTable from "./post-table" import PostTable from "./post-table"
import ServerInfo from "./server-info"
import UserTable from "./user-table" import UserTable from "./user-table"
export const adminFetcher = async ( export const adminFetcher = async (
@ -25,8 +24,6 @@ const Admin = () => {
return ( return (
<div className={styles.adminWrapper}> <div className={styles.adminWrapper}>
<Text h2>Administration</Text> <Text h2>Administration</Text>
<ServerInfo />
<Spacer height={1} />
<UserTable /> <UserTable />
<Spacer height={1} /> <Spacer height={1} />
<PostTable /> <PostTable />

View file

@ -9,7 +9,7 @@ import ActionDropdown from "./action-dropdown"
const PostTable = () => { const PostTable = () => {
const [posts, setPosts] = useState<Post[]>() const [posts, setPosts] = useState<Post[]>()
// const { setToast } = useToasts() const { setToast } = useToasts()
useEffect(() => { useEffect(() => {
const fetchPosts = async () => { const fetchPosts = async () => {
@ -35,8 +35,8 @@ const PostTable = () => {
visibility: post.visibility, visibility: post.visibility,
size: post.files size: post.files
? byteToMB( ? byteToMB(
post.files.reduce((acc, file) => acc + file.html.length, 0) post.files.reduce((acc, file) => acc + file.html.length, 0)
) )
: 0, : 0,
actions: "" actions: ""
} }
@ -109,14 +109,14 @@ const PostTable = () => {
dataIndex: "", dataIndex: "",
key: "actions", key: "actions",
width: 50, width: 50,
render() { render(post: Post) {
return ( return (
<ActionDropdown <ActionDropdown
title="Actions" title="Actions"
actions={[ actions={[
{ {
title: "Delete", title: "Delete",
onClick: () => deletePost() onClick: () => deletePost(post.id)
} }
]} ]}
/> />
@ -128,11 +128,7 @@ const PostTable = () => {
return ( return (
<SettingsGroup title="Posts"> <SettingsGroup title="Posts">
{!posts && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>} {!posts && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
{posts && ( {posts && <Fieldset.Subtitle><h5>{posts.length} posts</h5></Fieldset.Subtitle>}
<Fieldset.Subtitle>
<h5>{posts.length} posts</h5>
</Fieldset.Subtitle>
)}
{posts && <Table columns={tableColumns} data={tablePosts} />} {posts && <Table columns={tableColumns} data={tablePosts} />}
</SettingsGroup> </SettingsGroup>
) )

View file

@ -1,75 +0,0 @@
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<string>("")
const [title, setTitle] = useState<string>("")
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<HTMLFormElement>) => {
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 (
<SettingsGroup title="Homepage">
<form onSubmit={onSubmit}>
<div>
<label htmlFor="title">Title</label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<Spacer height={1} />
<div>
<label htmlFor="message">Description (markdown)</label>
<Textarea
width={"100%"}
height={10}
id="message"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<Spacer height={1} />
<Button htmlType="submit">Update</Button>
</form>
</SettingsGroup>
)
}
export default Homepage

View file

@ -131,8 +131,7 @@ const UserTable = () => {
actions={[ actions={[
{ {
title: user.role === "admin" ? "Change role" : "Make admin", title: user.role === "admin" ? "Change role" : "Make admin",
onClick: () => onClick: () => toggleRole(user.id, user.role === "admin" ? "user" : "admin")
toggleRole(user.id, user.role === "admin" ? "user" : "admin")
}, },
{ {
title: "Delete", title: "Delete",
@ -148,11 +147,7 @@ const UserTable = () => {
return ( return (
<SettingsGroup title="Users"> <SettingsGroup title="Users">
{!users && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>} {!users && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
{users && ( {users && <Fieldset.Subtitle><h5>{users.length} users</h5></Fieldset.Subtitle>}
<Fieldset.Subtitle>
<h5>{users.length} users</h5>
</Fieldset.Subtitle>
)}
{users && <Table columns={usernameColumns} data={tableUsers} />} {users && <Table columns={usernameColumns} data={tableUsers} />}
</SettingsGroup> </SettingsGroup>
) )

View file

@ -89,10 +89,10 @@ const Header = () => {
href: "/mine" href: "/mine"
}, },
{ {
name: "settings", name: 'settings',
icon: <SettingsIcon />, icon: <SettingsIcon />,
value: "settings", value: 'settings',
href: "/settings" href: '/settings'
}, },
{ {
name: "sign out", name: "sign out",

View file

@ -1,4 +1,9 @@
import { Button, useToasts, ButtonDropdown, Input } from "@geist-ui/core" import {
Button,
useToasts,
ButtonDropdown,
Input,
} from "@geist-ui/core"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import generateUUID from "@lib/generate-uuid" import generateUUID from "@lib/generate-uuid"

View file

@ -97,7 +97,10 @@ const ListItem = ({
{post.files?.map((file: File) => { {post.files?.map((file: File) => {
return ( return (
<div key={file.id}> <div key={file.id}>
<Link color href={`/post/${post.id}#${file.title}`}> <Link
color
href={`/post/${post.id}#${file.title}`}
>
{file.title || "Untitled file"} {file.title || "Untitled file"}
</Link> </Link>
</div> </div>

View file

@ -85,7 +85,9 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
} }
const viewParentClick = () => { const viewParentClick = () => {
router.push(`/post/${post.parent!.id}`) router.push(
`/post/${post.parent!.id}`
)
} }
if (isLoading) { if (isLoading) {
@ -121,7 +123,11 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
Edit a Copy Edit a Copy
</Button> </Button>
{post.parent && ( {post.parent && (
<Button auto icon={<Parent />} onClick={viewParentClick}> <Button
auto
icon={<Parent />}
onClick={viewParentClick}
>
View Parent View Parent
</Button> </Button>
)} )}

View file

@ -1,21 +1,24 @@
import { Fieldset, Text, Divider } from "@geist-ui/core" import { Fieldset, Text, Divider } from "@geist-ui/core"
import styles from "./settings-group.module.css" import styles from './settings-group.module.css'
type Props = { type Props = {
title: string title: string,
children: React.ReactNode | React.ReactNode[] children: React.ReactNode | React.ReactNode[],
} }
const SettingsGroup = ({ title, children }: Props) => { const SettingsGroup = ({
return ( title,
<Fieldset> children,
<Fieldset.Content> }: Props) => {
<Text h4>{title}</Text> return <Fieldset>
</Fieldset.Content> <Fieldset.Content>
<Divider /> <Text h4>{title}</Text>
<Fieldset.Content className={styles.content}>{children}</Fieldset.Content> </Fieldset.Content>
</Fieldset> <Divider />
) <Fieldset.Content className={styles.content}>
{children}
</Fieldset.Content>
</Fieldset>
} }
export default SettingsGroup export default SettingsGroup

View file

@ -3,24 +3,20 @@ import Profile from "./sections/profile"
import SettingsGroup from "../settings-group" import SettingsGroup from "../settings-group"
const SettingsPage = () => { const SettingsPage = () => {
return ( return (<div style={{
<div display: 'flex',
style={{ flexDirection: 'column',
display: "flex", gap: 'var(--gap)',
flexDirection: "column", marginBottom: 'var(--gap)',
gap: "var(--gap)", }}>
marginBottom: "var(--gap)" <h1>Settings</h1>
}} <SettingsGroup title="Profile">
> <Profile />
<h1>Settings</h1> </SettingsGroup>
<SettingsGroup title="Profile"> <SettingsGroup title="Password">
<Profile /> <Password />
</SettingsGroup> </SettingsGroup>
<SettingsGroup title="Password"> </div>)
<Password />
</SettingsGroup>
</div>
)
} }
export default SettingsPage export default SettingsPage

View file

@ -3,132 +3,95 @@ import Cookies from "js-cookie"
import { useState } from "react" import { useState } from "react"
const Password = () => { const Password = () => {
const [password, setPassword] = useState<string>("") const [password, setPassword] = useState<string>('')
const [newPassword, setNewPassword] = useState<string>("") const [newPassword, setNewPassword] = useState<string>('')
const [confirmPassword, setConfirmPassword] = useState<string>("") const [confirmPassword, setConfirmPassword] = useState<string>('')
const { setToast } = useToasts() const { setToast } = useToasts()
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value) setPassword(e.target.value)
} }
const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewPassword(e.target.value) setNewPassword(e.target.value)
} }
const handleConfirmPasswordChange = ( const handleConfirmPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e: React.ChangeEvent<HTMLInputElement> setConfirmPassword(e.target.value)
) => { }
setConfirmPassword(e.target.value)
}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
if (!password || !newPassword || !confirmPassword) { if (!password || !newPassword || !confirmPassword) {
setToast({ setToast({
text: "Please fill out all fields", text: "Please fill out all fields",
type: "error" type: "error",
}) })
} }
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
setToast({ setToast({
text: "New password and confirm password do not match", text: "New password and confirm password do not match",
type: "error" type: "error",
}) })
} }
const res = await fetch("/server-api/auth/change-password", { const res = await fetch("/server-api/auth/change-password", {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("drift-token")}` "Authorization": `Bearer ${Cookies.get("drift-token")}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
oldPassword: password, oldPassword: password,
newPassword newPassword,
}) }),
}) })
if (res.status === 200) {
setToast({
text: "Password updated successfully",
type: "success"
})
setPassword("")
setNewPassword("")
setConfirmPassword("")
} else {
const data = await res.json()
setToast({ if (res.status === 200) {
text: data.error ?? "Failed to update password", setToast({
type: "error" text: "Password updated successfully",
}) type: "success",
} })
} setPassword('')
setNewPassword('')
setConfirmPassword('')
} else {
const data = await res.json()
return ( setToast({
<form text: data.error ?? "Failed to update password",
style={{ type: "error",
display: "flex", })
flexDirection: "column", }
gap: "var(--gap)", }
maxWidth: "300px"
}} return (
onSubmit={onSubmit} <form
> style={{
<div> display: "flex",
<label htmlFor="current-password">Current password</label> flexDirection: "column",
<Input gap: "var(--gap)",
onChange={handlePasswordChange} maxWidth: "300px",
minLength={6} }}
maxLength={128} onSubmit={onSubmit}
value={password} >
id="current-password" <div>
htmlType="password" <label htmlFor="current-password">Current password</label>
required <Input onChange={handlePasswordChange} minLength={6} maxLength={128} value={password} id="current-password" htmlType="password" required autoComplete="current-password" placeholder="Current Password" width={"100%"} />
autoComplete="current-password" </div>
placeholder="Current Password" <div>
width={"100%"} <label htmlFor="new-password">New password</label>
/> <Input onChange={handleNewPasswordChange} minLength={6} maxLength={128} value={newPassword} id="new-password" htmlType="password" required autoComplete="new-password" placeholder="New Password" width={"100%"} />
</div> </div>
<div> <div>
<label htmlFor="new-password">New password</label> <label htmlFor="confirm-password">Confirm password</label>
<Input <Input onChange={handleConfirmPasswordChange} minLength={6} maxLength={128} value={confirmPassword} id="confirm-password" htmlType="password" required autoComplete="confirm-password" placeholder="Confirm Password" width={"100%"} />
onChange={handleNewPasswordChange} </div>
minLength={6} <Button htmlType="submit" auto>Change Password</Button>
maxLength={128} </form>)
value={newPassword}
id="new-password"
htmlType="password"
required
autoComplete="new-password"
placeholder="New Password"
width={"100%"}
/>
</div>
<div>
<label htmlFor="confirm-password">Confirm password</label>
<Input
onChange={handleConfirmPasswordChange}
minLength={6}
maxLength={128}
value={confirmPassword}
id="confirm-password"
htmlType="password"
required
autoComplete="confirm-password"
placeholder="Confirm Password"
width={"100%"}
/>
</div>
<Button htmlType="submit" auto>
Change Password
</Button>
</form>
)
} }
export default Password export default Password

View file

@ -4,121 +4,97 @@ import Cookies from "js-cookie"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
const Profile = () => { const Profile = () => {
const user = useUserData() const user = useUserData()
const [name, setName] = useState<string>() const [name, setName] = useState<string>()
const [email, setEmail] = useState<string>() const [email, setEmail] = useState<string>()
const [bio, setBio] = useState<string>() const [bio, setBio] = useState<string>()
useEffect(() => { useEffect(() => {
if (user?.displayName) setName(user.displayName) console.log(user)
if (user?.email) setEmail(user.email) if (user?.displayName) setName(user.displayName)
if (user?.bio) setBio(user.bio) if (user?.email) setEmail(user.email)
}, [user]) if (user?.bio) setBio(user.bio)
}, [user])
const { setToast } = useToasts() const { setToast } = useToasts()
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value) setName(e.target.value)
} }
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value) setEmail(e.target.value)
} }
const handleBioChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleBioChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setBio(e.target.value) setBio(e.target.value)
} }
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
if (!name && !email && !bio) { if (!name && !email && !bio) {
setToast({ setToast({
text: "Please fill out at least one field", text: "Please fill out at least one field",
type: "error" type: "error",
}) })
return return
} }
const data = { const data = {
displayName: name, displayName: name,
email, email,
bio bio,
} }
const res = await fetch("/server-api/user/profile", { const res = await fetch("/server-api/user/profile", {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("drift-token")}` "Authorization": `Bearer ${Cookies.get("drift-token")}`,
}, },
body: JSON.stringify(data) body: JSON.stringify(data),
}) })
if (res.status === 200) { if (res.status === 200) {
setToast({ setToast({
text: "Profile updated", text: "Profile updated",
type: "success" type: "success",
}) })
} else { } else {
setToast({ setToast({
text: "Something went wrong updating your profile", text: "Something went wrong updating your profile",
type: "error" type: "error",
}) })
} }
} }
return ( return (<>
<> <Note type="warning" marginBottom={"var(--gap)"}>
<Note type="warning" marginBottom={"var(--gap)"}> This information will be publicly available on your profile
This information will be publicly available on your profile </Note>
</Note> <form
<form style={{
style={{ display: "flex",
display: "flex", flexDirection: "column",
flexDirection: "column", gap: "var(--gap)",
gap: "var(--gap)", maxWidth: "300px",
maxWidth: "300px" }}
}} onSubmit={onSubmit}
onSubmit={onSubmit} >
> <div>
<div> <label htmlFor="displayName">Display name</label>
<label htmlFor="displayName">Display name</label> <Input id="displayName" width={"100%"} placeholder="my name" value={name || ''} onChange={handleNameChange} />
<Input </div>
id="displayName" <div>
width={"100%"} <label htmlFor="email">Email</label>
placeholder="my name" <Input id="email" htmlType="email" width={"100%"} placeholder="my@email.io" value={email || ''} onChange={handleEmailChange} />
value={name || ""} </div>
onChange={handleNameChange} <div>
/> <label htmlFor="bio">Biography (max 250 characters)</label>
</div> <Textarea id="bio" width="100%" maxLength={250} placeholder="I enjoy..." value={bio || ''} onChange={handleBioChange} />
<div> </div>
<label htmlFor="email">Email</label> <Button htmlType="submit" auto>Submit</Button>
<Input </form></>)
id="email"
htmlType="email"
width={"100%"}
placeholder="my@email.io"
value={email || ""}
onChange={handleEmailChange}
/>
</div>
<div>
<label htmlFor="bio">Biography (max 250 characters)</label>
<Textarea
id="bio"
width="100%"
maxLength={250}
placeholder="I enjoy..."
value={bio || ""}
onChange={handleBioChange}
/>
</div>
<Button htmlType="submit" auto>
Submit
</Button>
</form>
</>
)
} }
export default Profile export default Profile

View file

@ -3,16 +3,14 @@ import PageSeo from "@components/page-seo"
import HomeComponent from "@components/home" import HomeComponent from "@components/home"
import { Page, Text } from "@geist-ui/core" import { Page, Text } from "@geist-ui/core"
import type { GetStaticProps } from "next" import type { GetStaticProps } from "next"
import { InferGetStaticPropsType } from "next" import { InferGetStaticPropsType } from 'next'
type Props = type Props = {
| { introContent: string
introContent: string introTitle: string
introTitle: string rendered: string
rendered: string } | {
} error: boolean
| { }
error: boolean
}
export const getStaticProps: GetStaticProps = async () => { export const getStaticProps: GetStaticProps = async () => {
try { try {
@ -34,26 +32,21 @@ export const getStaticProps: GetStaticProps = async () => {
// Next.js will attempt to re-generate the page: // Next.js will attempt to re-generate the page:
// - When a request comes in // - When a request comes in
// - At most every 60 seconds // - At most every 60 seconds
revalidate: 60 // In seconds revalidate: 60, // In seconds
} }
} catch (err) { } catch (err) {
// If there was an error, it's likely due to the server not running, so we attempt to regenerate the page // If there was an error, it's likely due to the server not running, so we attempt to regenerate the page
return { return {
props: { props: {
error: true error: true,
}, },
revalidate: 10 // In seconds revalidate: 10, // In seconds
} }
} }
} }
// TODO: fix props type // TODO: fix props type
const Home = ({ const Home = ({ rendered, introContent, introTitle, error }: InferGetStaticPropsType<typeof getStaticProps>) => {
rendered,
introContent,
introTitle,
error
}: InferGetStaticPropsType<typeof getStaticProps>) => {
return ( return (
<Page className={styles.wrapper}> <Page className={styles.wrapper}>
<PageSeo /> <PageSeo />

View file

@ -69,6 +69,7 @@ export const getServerSideProps: GetServerSideProps = async ({
} }
} }
return { return {
props: { props: {
post: json, post: json,

View file

@ -1,27 +1,15 @@
import { import { Button, Divider, Text, Fieldset, Input, Page, Note, Textarea } from "@geist-ui/core"
Button,
Divider,
Text,
Fieldset,
Input,
Page,
Note,
Textarea
} from "@geist-ui/core"
import PageSeo from "@components/page-seo" import PageSeo from "@components/page-seo"
import styles from "@styles/Home.module.css" import styles from "@styles/Home.module.css"
import SettingsPage from "@components/settings" import SettingsPage from "@components/settings"
const Settings = () => ( const Settings = () => (
<Page width={"100%"}> <Page width={"100%"}>
<PageSeo title="Drift - Settings" /> <PageSeo title="Drift - Settings" />
<Page.Content <Page.Content className={styles.main} style={{ gap: 'var(--gap)', display: 'flex', flexDirection: 'column' }}>
className={styles.main} <SettingsPage />
style={{ gap: "var(--gap)", display: "flex", flexDirection: "column" }} </Page.Content>
> </Page>
<SettingsPage />
</Page.Content>
</Page>
) )
export default Settings export default Settings

View file

@ -6,7 +6,6 @@ import { errors } from "celebrate"
import secretKey from "@lib/middleware/secret-key" import secretKey from "@lib/middleware/secret-key"
import markdown from "@lib/render-markdown" import markdown from "@lib/render-markdown"
import config from "@lib/config" import config from "@lib/config"
import { ServerInfo } from "@lib/models/ServerInfo"
export const app = express() export const app = express()
@ -20,25 +19,17 @@ app.use("/files", files)
app.use("/admin", admin) app.use("/admin", admin)
app.use("/health", health) app.use("/health", health)
app.get("/welcome", secretKey, async (req, res) => { app.get("/welcome", secretKey, (req, res) => {
const serverInfo = await ServerInfo.findOne({ const introContent = config.welcome_content
where: { const introTitle = config.welcome_title
id: "1" if (!introContent || !introTitle) {
} return res.status(500).json({ error: "Missing welcome content" })
})
if (!serverInfo) {
return res.status(500).json({
message: "Server info not found."
})
} }
const { welcomeMessage, welcomeTitle } = serverInfo
return res.json({ return res.json({
title: welcomeTitle, title: introTitle,
content: welcomeMessage, content: introContent,
rendered: markdown(welcomeMessage) rendered: markdown(introContent)
}) })
}) })

View file

@ -2,7 +2,6 @@ import config from "@lib/config"
import databasePath from "@lib/get-database-path" import databasePath from "@lib/get-database-path"
import { Sequelize } from "sequelize-typescript" import { Sequelize } from "sequelize-typescript"
import { SequelizeStorage, Umzug } from "umzug" import { SequelizeStorage, Umzug } from "umzug"
import { QueryTypes } from "sequelize"
export const sequelize = new Sequelize({ export const sequelize = new Sequelize({
dialect: "sqlite", dialect: "sqlite",
@ -12,24 +11,10 @@ export const sequelize = new Sequelize({
logging: console.log logging: console.log
}) })
export const initServerInfo = async () => { if (config.memory_db) {
const serverInfo = await sequelize.query( console.log("Using in-memory database")
"SELECT * FROM `server-info` WHERE id = '1'", } else {
{ console.log(`Database path: ${databasePath}`)
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({ export const umzug = new Umzug({
@ -45,12 +30,6 @@ export const umzug = new Umzug({
export type Migration = typeof umzug._types.migration 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 you're in a development environment, you can manually migrate with `yarn migrate:{up,down}` in the `server` folder
if (config.is_production) { if (config.is_production) {
;(async () => { ;(async () => {

View file

@ -1,33 +0,0 @@
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
}

View file

@ -3,24 +3,24 @@ import { DataTypes } from "sequelize"
import type { Migration } from "../database" import type { Migration } from "../database"
export const up: Migration = async ({ context: queryInterface }) => export const up: Migration = async ({ context: queryInterface }) =>
Promise.all([ Promise.all([
queryInterface.addColumn("users", "email", { queryInterface.addColumn("users", "email", {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
}), }),
queryInterface.addColumn("users", "displayName", { queryInterface.addColumn("users", "displayName", {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}), }),
queryInterface.addColumn("users", "bio", { queryInterface.addColumn("users", "bio", {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}) }),
]) ])
export const down: Migration = async ({ context: queryInterface }) => export const down: Migration = async ({ context: queryInterface }) =>
Promise.all([ Promise.all([
queryInterface.removeColumn("users", "email"), queryInterface.removeColumn("users", "email"),
queryInterface.removeColumn("users", "displayName"), queryInterface.removeColumn("users", "displayName"),
queryInterface.removeColumn("users", "bio") queryInterface.removeColumn("users", "bio"),
]) ])

View file

@ -1,37 +0,0 @@
"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")

View file

@ -4,7 +4,6 @@ import { User } from "@lib/models/User"
import { File } from "@lib/models/File" import { File } from "@lib/models/File"
import { Router } from "express" import { Router } from "express"
import { celebrate, Joi } from "celebrate" import { celebrate, Joi } from "celebrate"
import { ServerInfo } from "@lib/models/ServerInfo"
export const admin = Router() export const admin = Router()
@ -198,54 +197,3 @@ admin.delete("/post/:id", async (req, res, next) => {
next(e) 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)
}
}
)

View file

@ -195,8 +195,7 @@ auth.post("/signout", secretKey, async (req, res, next) => {
} }
}) })
auth.put( auth.put("/change-password",
"/change-password",
jwt, jwt,
celebrate({ celebrate({
body: { body: {

View file

@ -30,14 +30,13 @@ user.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
} }
}) })
user.put( user.put("/profile",
"/profile",
jwt, jwt,
celebrate({ celebrate({
body: { body: {
displayName: Joi.string().optional().allow(""), displayName: Joi.string().optional().allow(""),
bio: 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) => { async (req: UserJwtRequest, res, next) => {

View file

@ -2,10 +2,8 @@ import { createServer } from "http"
import { app } from "./app" import { app } from "./app"
import config from "./lib/config" import config from "./lib/config"
import "./database" import "./database"
import { initServerInfo } from "./database"
;(async () => { ;(async () => {
initServerInfo() // await sequelize.sync()
createServer(app).listen(config.port, () => createServer(app).listen(config.port, () =>
console.info(`Server running on port ${config.port}`) console.info(`Server running on port ${config.port}`)
) )