post generation rework with static paths/props
This commit is contained in:
parent
3efbeb726f
commit
90fa28ad65
26 changed files with 302 additions and 102 deletions
|
@ -30,6 +30,7 @@ You can change these to your liking.
|
||||||
- `API_URL`: defaults to localhost:3001, but allows you to host the front-end separately from the backend on a service like Vercel or Netlify
|
- `API_URL`: defaults to localhost:3001, but allows you to host the front-end separately from the backend on a service like Vercel or Netlify
|
||||||
- `WELCOME_CONTENT`: a markdown string (with \n newlines) that's rendered on the home page
|
- `WELCOME_CONTENT`: a markdown string (with \n newlines) that's rendered on the home page
|
||||||
- `WELCOME_TITLE`: the file title for the post on the homepage.
|
- `WELCOME_TITLE`: the file title for the post on the homepage.
|
||||||
|
- `SECRET_KEY`: a secret key used for validating API requests that is never exposed to the browser
|
||||||
|
|
||||||
`server/.env`:
|
`server/.env`:
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ You can change these to your liking.
|
||||||
- `JWT_SECRET`: a secure token for JWT tokens. You can generate one [here](https://www.grc.com/passwords.htm).
|
- `JWT_SECRET`: a secure token for JWT tokens. You can generate one [here](https://www.grc.com/passwords.htm).
|
||||||
- `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 MEMORY_DB is not `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 password will be required.
|
- `REGISTRATION_PASSWORD`: if MEMORY_DB is not `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 password will be required.
|
||||||
|
- `SECRET_KEY`: the same secret key as the client
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
API_URL=http://localhost:3000
|
API_URL=http://localhost:3000
|
||||||
WELCOME_TITLE="Welcome to Drift"
|
WELCOME_TITLE="Welcome to Drift"
|
||||||
WELCOME_CONTENT="### 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 secret posts\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).\n **This demo is on a memory-only database, so accounts and pastes can be deleted at any time.**\n You can find the source code on [GitHub](https://github.com/MaxLeiter/drift).\n \n Drift was inspired by [this tweet](https://twitter.com/emilyst/status/1499858264346935297):\n > What is the absolute closest thing to GitHub Gist that can be self-hosted?\n In terms of design and functionality. Hosts images and markdown, rendered. Creates links that can be private or public. Uses/requires registration. I have looked at dozens of pastebin-like things."
|
WELCOME_CONTENT="### 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 secret posts\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).\n **This demo is on a memory-only database, so accounts and pastes can be deleted at any time.**\n You can find the source code on [GitHub](https://github.com/MaxLeiter/drift).\n \n Drift was inspired by [this tweet](https://twitter.com/emilyst/status/1499858264346935297):\n > What is the absolute closest thing to GitHub Gist that can be self-hosted?\n In terms of design and functionality. Hosts images and markdown, rendered. Creates links that can be private or public. Uses/requires registration. I have looked at dozens of pastebin-like things."
|
||||||
|
SECRET_KEY=secret
|
|
@ -1,17 +1,7 @@
|
||||||
import useSWR from "swr"
|
|
||||||
import PostList from "../post-list"
|
import PostList from "../post-list"
|
||||||
import Cookies from "js-cookie"
|
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url, {
|
const MyPosts = ({ posts, error }: { posts: any, error: any }) => {
|
||||||
headers: {
|
return <PostList posts={posts} error={error} />
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${Cookies.get("drift-token")}`
|
|
||||||
},
|
|
||||||
}).then(r => r.json())
|
|
||||||
|
|
||||||
const MyPosts = () => {
|
|
||||||
const { data, error } = useSWR('/server-api/users/mine', fetcher)
|
|
||||||
return <PostList posts={data} error={error} />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyPosts
|
export default MyPosts
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Title from './title';
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
import type { PostVisibility, Document as DocumentType } from '@lib/types';
|
import type { PostVisibility, Document as DocumentType } from '@lib/types';
|
||||||
import PasswordModal from './password';
|
import PasswordModal from './password';
|
||||||
|
import getPostPath from '@lib/get-post-path';
|
||||||
|
|
||||||
const Post = () => {
|
const Post = () => {
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
|
@ -36,7 +37,7 @@ const Post = () => {
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
router.push(`/post/${json.id}`)
|
router.push(getPostPath(json.visibility, json.id))
|
||||||
} else {
|
} else {
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
setToast({
|
setToast({
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"
|
||||||
import timeAgo from "@lib/time-ago"
|
import timeAgo from "@lib/time-ago"
|
||||||
import ShiftBy from "../shift-by"
|
import ShiftBy from "../shift-by"
|
||||||
import VisibilityBadge from "../visibility-badge"
|
import VisibilityBadge from "../visibility-badge"
|
||||||
|
import getPostPath from "@lib/get-post-path"
|
||||||
|
|
||||||
const FilenameInput = ({ title }: { title: string }) => <Input
|
const FilenameInput = ({ title }: { title: string }) => <Input
|
||||||
value={title}
|
value={title}
|
||||||
|
@ -33,7 +34,7 @@ const ListItem = ({ post }: { post: any }) => {
|
||||||
<Grid.Container>
|
<Grid.Container>
|
||||||
<Grid md={14} xs={14}>
|
<Grid md={14} xs={14}>
|
||||||
<Text h3 paddingLeft={1 / 2} >
|
<Text h3 paddingLeft={1 / 2} >
|
||||||
<NextLink passHref={true} href={`/post/${post.id}`}>
|
<NextLink passHref={true} href={getPostPath(post.visibility, post.id)}>
|
||||||
<Link color>{post.title}
|
<Link color>{post.title}
|
||||||
<ShiftBy y={-1}><VisibilityBadge visibility={post.visibility} /></ShiftBy>
|
<ShiftBy y={-1}><VisibilityBadge visibility={post.visibility} /></ShiftBy>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import ReactMarkdown from "react-markdown"
|
import ReactMarkdown from "react-markdown"
|
||||||
import remarkGfm from "remark-gfm"
|
import remarkGfm from "remark-gfm"
|
||||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/prism-async-light';
|
||||||
import rehypeSlug from 'rehype-slug'
|
import rehypeSlug from 'rehype-slug'
|
||||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||||
|
|
||||||
|
|
13
client/lib/get-post-path.ts
Normal file
13
client/lib/get-post-path.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import type { PostVisibility } from "./types"
|
||||||
|
|
||||||
|
export default function getPostPath(visibility: PostVisibility, id: string) {
|
||||||
|
switch (visibility) {
|
||||||
|
case "private":
|
||||||
|
return `/post/private/${id}`
|
||||||
|
case "protected":
|
||||||
|
return `/post/protected/${id}`
|
||||||
|
case "unlisted":
|
||||||
|
case "public":
|
||||||
|
return `/post/${id}`
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,13 +2,20 @@ import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { id, download } = req.query
|
const { id, download } = req.query
|
||||||
const file = await fetch(`${process.env.API_URL}/files/raw/${id}`)
|
const file = await fetch(`${process.env.API_URL}/files/raw/${id}`, {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'text/plain',
|
||||||
|
'x-secret-key': process.env.SECRET_KEY || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
res.setHeader("Content-Type", "text/plain")
|
||||||
|
res.setHeader('Cache-Control', 's-maxage=86400');
|
||||||
|
|
||||||
if (file.ok) {
|
if (file.ok) {
|
||||||
const data = await file.json()
|
const data = await file.json()
|
||||||
const { title, content } = data
|
const { title, content } = data
|
||||||
// serve the file raw as plain text
|
// serve the file raw as plain text
|
||||||
res.setHeader("Content-Type", "text/plain")
|
|
||||||
res.setHeader('Cache-Control', 's-maxage=86400');
|
|
||||||
if (download) {
|
if (download) {
|
||||||
res.setHeader("Content-Disposition", `attachment; filename="${title}"`)
|
res.setHeader("Content-Disposition", `attachment; filename="${title}"`)
|
||||||
} else {
|
} else {
|
||||||
|
|
24
client/pages/api/revalidate.ts
Normal file
24
client/pages/api/revalidate.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.headers['x-secret-key'] !== process.env.SECRET_KEY) {
|
||||||
|
return res.status(401).send('Unauthorized')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { path } = req.query
|
||||||
|
|
||||||
|
if (!path || typeof path !== 'string') {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Missing path"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await res.unstable_revalidate(path)
|
||||||
|
return res.json({ revalidated: true })
|
||||||
|
} catch (err) {
|
||||||
|
// If there was an error, Next.js will continue
|
||||||
|
// to show the last successfully generated page
|
||||||
|
return res.status(500).send('Error revalidating')
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,18 +3,48 @@ import { Page } from '@geist-ui/core'
|
||||||
|
|
||||||
import Header from '@components/header'
|
import Header from '@components/header'
|
||||||
import MyPosts from '@components/my-posts'
|
import MyPosts from '@components/my-posts'
|
||||||
|
import cookie from "cookie";
|
||||||
|
import { GetServerSideProps } from 'next';
|
||||||
|
import { ThemeProps } from '@lib/types';
|
||||||
|
|
||||||
const Home = ({ theme, changeTheme }: { theme: "light" | "dark", changeTheme: () => void }) => {
|
const Home = ({ posts, error, theme, changeTheme }: ThemeProps & { posts: any; error: any; }) => {
|
||||||
return (
|
return (
|
||||||
<Page className={styles.container} width="100%">
|
<Page className={styles.container} width="100%">
|
||||||
<Page.Header>
|
<Page.Header>
|
||||||
<Header theme={theme} changeTheme={changeTheme} />
|
<Header theme={theme} changeTheme={changeTheme} />
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
<Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
|
<Page.Content paddingTop={"var(--gap)"} width={"var(--main-content-width)"} margin="0 auto" className={styles.main}>
|
||||||
<MyPosts />
|
<MyPosts error={error} posts={posts} />
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
</Page >
|
</Page >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// get server side props
|
||||||
|
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
|
||||||
|
const driftToken = cookie.parse(req.headers.cookie || '')[`drift-token`]
|
||||||
|
if (!driftToken) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: '/',
|
||||||
|
permanent: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = await fetch('http://localhost:3000/server-api/users/mine', {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${driftToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
posts: await posts.json(),
|
||||||
|
error: posts.status !== 200,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Home
|
export default Home
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { Button, Page, Text } from "@geist-ui/core";
|
import { Button, Page, Text } from "@geist-ui/core";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import Document from '@components/document'
|
||||||
import Document from '../../components/document'
|
import Header from "@components/header";
|
||||||
import Header from "../../components/header";
|
import VisibilityBadge from "@components/visibility-badge";
|
||||||
import VisibilityBadge from "../../components/visibility-badge";
|
|
||||||
import PageSeo from "components/page-seo";
|
import PageSeo from "components/page-seo";
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
import cookie from "cookie";
|
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||||
import { GetServerSideProps, GetStaticPaths, GetStaticProps } from "next";
|
|
||||||
import { PostVisibility, ThemeProps } from "@lib/types";
|
import { PostVisibility, ThemeProps } from "@lib/types";
|
||||||
|
|
||||||
type File = {
|
type File = {
|
||||||
|
@ -51,7 +49,7 @@ const Post = ({ post, theme, changeTheme }: PostProps) => {
|
||||||
<PageSeo
|
<PageSeo
|
||||||
title={`${post.title} - Drift`}
|
title={`${post.title} - Drift`}
|
||||||
description={post.description}
|
description={post.description}
|
||||||
isPrivate={post.visibility !== 'public'}
|
isPrivate={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Page.Header>
|
<Page.Header>
|
||||||
|
@ -83,53 +81,37 @@ const Post = ({ post, theme, changeTheme }: PostProps) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
const headers = context.req.headers
|
const posts = await fetch(process.env.API_URL + `/posts/`, {
|
||||||
const host = headers.host
|
method: "GET",
|
||||||
const driftToken = cookie.parse(headers.cookie || '')[`drift-token`]
|
headers: {
|
||||||
let driftTheme = cookie.parse(headers.cookie || '')[`drift-theme`]
|
"Content-Type": "application/json",
|
||||||
if (driftTheme !== "light" && driftTheme !== "dark") {
|
"x-secret-key": process.env.SECRET_KEY || "",
|
||||||
driftTheme = "light"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.query.id) {
|
|
||||||
const post = await fetch('http://' + host + `/server-api/posts/${context.query.id}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${driftToken}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!post.ok || post.status !== 200) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: '/',
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
try {
|
})
|
||||||
const json = await post.json();
|
|
||||||
const maxAge = 60 * 60 * 24 * 365;
|
const json = await posts.json()
|
||||||
context.res.setHeader(
|
const filtered = json.filter((post: any) => post.visibility === "public" || post.visibility === "unlisted")
|
||||||
'Cache-Control',
|
const paths = filtered.map((post: any) => ({
|
||||||
`${json.visibility === "public" ? "public" : "private"}, s-maxage=${maxAge}`
|
params: { id: post.id }
|
||||||
)
|
}))
|
||||||
return {
|
|
||||||
props: {
|
return { paths, fallback: 'blocking' }
|
||||||
post: json
|
}
|
||||||
}
|
|
||||||
}
|
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
||||||
} catch (e) {
|
const post = await fetch(process.env.API_URL + `/posts/${params?.id}`, {
|
||||||
console.log(e)
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-secret-key": process.env.SECRET_KEY || "",
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
post: null
|
post: await post.json()
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
128
client/pages/post/private/[id].tsx
Normal file
128
client/pages/post/private/[id].tsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import { Button, Page, Text } from "@geist-ui/core";
|
||||||
|
|
||||||
|
import Document from '@components/document'
|
||||||
|
import Header from "@components/header";
|
||||||
|
import VisibilityBadge from "@components/visibility-badge";
|
||||||
|
import PageSeo from "components/page-seo";
|
||||||
|
import styles from '../styles.module.css';
|
||||||
|
import cookie from "cookie";
|
||||||
|
import type { GetServerSideProps } from "next";
|
||||||
|
import { PostVisibility, ThemeProps } from "@lib/types";
|
||||||
|
|
||||||
|
type File = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Files = File[]
|
||||||
|
|
||||||
|
export type PostProps = ThemeProps & {
|
||||||
|
post: {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
visibility: PostVisibility
|
||||||
|
files: Files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Post = ({ post, theme, changeTheme }: PostProps) => {
|
||||||
|
const download = async () => {
|
||||||
|
const clientZip = require("client-zip")
|
||||||
|
|
||||||
|
const blob = await clientZip.downloadZip(post.files.map((file: any) => {
|
||||||
|
return {
|
||||||
|
name: file.title,
|
||||||
|
input: file.content,
|
||||||
|
lastModified: new Date(file.updatedAt)
|
||||||
|
}
|
||||||
|
})).blob()
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = `${post.title}.zip`
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page width={"100%"}>
|
||||||
|
<PageSeo
|
||||||
|
title={`${post.title} - Drift`}
|
||||||
|
description={post.description}
|
||||||
|
isPrivate={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Page.Header>
|
||||||
|
<Header theme={theme} changeTheme={changeTheme} />
|
||||||
|
</Page.Header>
|
||||||
|
<Page.Content width={"var(--main-content-width)"} margin="auto">
|
||||||
|
{/* {!isLoading && <PostFileExplorer files={post.files} />} */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.titleAndBadge}>
|
||||||
|
<Text h2>{post.title}</Text>
|
||||||
|
<span><VisibilityBadge visibility={post.visibility} /></span>
|
||||||
|
</div>
|
||||||
|
<Button auto onClick={download}>
|
||||||
|
Download as ZIP archive
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{post.files.map(({ id, content, title }: { id: any, content: string, title: string }) => (
|
||||||
|
<Document
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
content={content}
|
||||||
|
title={title}
|
||||||
|
editable={false}
|
||||||
|
initialTab={'preview'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Page.Content>
|
||||||
|
</Page >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
|
const headers = context.req.headers
|
||||||
|
const host = headers.host
|
||||||
|
const driftToken = cookie.parse(headers.cookie || '')[`drift-token`]
|
||||||
|
|
||||||
|
if (context.query.id) {
|
||||||
|
const post = await fetch('http://' + host + `/server-api/posts/${context.query.id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${driftToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!post.ok || post.status !== 200) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: '/',
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const json = await post.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
post: json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
post: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Post
|
||||||
|
|
14
server/src/lib/middleware/secret-key.ts
Normal file
14
server/src/lib/middleware/secret-key.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
const key = process.env.SECRET_KEY;
|
||||||
|
if (!key) {
|
||||||
|
throw new Error('SECRET_KEY is not set.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function authenticateToken(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const requestKey = req.headers['x-secret-key']
|
||||||
|
if (requestKey !== key) {
|
||||||
|
return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import { genSalt, hash, compare } from "bcrypt"
|
import { genSalt, hash, compare } from "bcrypt"
|
||||||
import { User } from '../../lib/models/User'
|
import { User } from '../lib/models/User'
|
||||||
import { sign } from 'jsonwebtoken'
|
import { sign } from 'jsonwebtoken'
|
||||||
import config from '../../lib/config'
|
import config from '../lib/config'
|
||||||
import jwt from '../../lib/middleware/jwt'
|
import jwt from '../lib/middleware/jwt'
|
||||||
|
|
||||||
const NO_EMPTY_SPACE_REGEX = /^\S*$/
|
const NO_EMPTY_SPACE_REGEX = /^\S*$/
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
|
import secretKey from '../lib/middleware/secret-key';
|
||||||
// import { Movie } from '../models/Post'
|
// import { Movie } from '../models/Post'
|
||||||
import { File } from '../../lib/models/File'
|
import { File } from '../lib/models/File'
|
||||||
|
|
||||||
export const files = Router()
|
export const files = Router()
|
||||||
|
|
||||||
files.get("/raw/:id", async (req, res, next) => {
|
files.get("/raw/:id", secretKey, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const file = await File.findOne({
|
const file = await File.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
@ -12,18 +13,9 @@ files.get("/raw/:id", async (req, res, next) => {
|
||||||
},
|
},
|
||||||
attributes: ["title", "content"],
|
attributes: ["title", "content"],
|
||||||
})
|
})
|
||||||
// TODO: fix post inclusion
|
|
||||||
// if (file?.post.visibility === 'public' || file?.post.visibility === 'unlisted') {
|
|
||||||
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
||||||
res.json(file);
|
res.json(file);
|
||||||
// } else {
|
|
||||||
// TODO: should this be `private, `?
|
|
||||||
// res.setHeader("Cache-Control", "max-age=86400");
|
|
||||||
// res.json(file);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
// import { Movie } from '../models/Post'
|
// import { Movie } from '../models/Post'
|
||||||
import { File } from '../../lib/models/File'
|
import { File } from '../lib/models/File'
|
||||||
import { Post } from '../../lib/models/Post';
|
import { Post } from '../lib/models/Post';
|
||||||
import jwt, { UserJwtRequest } from '../../lib/middleware/jwt';
|
import jwt, { UserJwtRequest } from '../lib/middleware/jwt';
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import { User } from '../../lib/models/User';
|
import { User } from '../lib/models/User';
|
||||||
|
import secretKey from '../lib/middleware/secret-key';
|
||||||
|
|
||||||
export const posts = Router()
|
export const posts = Router()
|
||||||
|
|
||||||
|
@ -57,7 +58,18 @@ posts.post('/create', jwt, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
posts.get("/:id", async (req: UserJwtRequest, res, next) => {
|
posts.get("/", secretKey, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const posts = await Post.findAll({
|
||||||
|
attributes: ["id", "title", "visibility", "createdAt"],
|
||||||
|
})
|
||||||
|
res.json(posts);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
posts.get("/:id", secretKey, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const post = await Post.findOne({
|
const post = await Post.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
@ -76,16 +88,19 @@ posts.get("/:id", async (req: UserJwtRequest, res, next) => {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
if (!post) {
|
||||||
|
throw new Error("Post not found.")
|
||||||
|
}
|
||||||
|
|
||||||
if (post?.visibility === 'public' || post?.visibility === 'unlisted') {
|
if (post.visibility === 'public' || post?.visibility === 'unlisted') {
|
||||||
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
||||||
res.json(post);
|
res.json(post);
|
||||||
} else {
|
} else if (post.visibility === 'private') {
|
||||||
// TODO: should this be `private, `?
|
console.log("here")
|
||||||
res.setHeader("Cache-Control", "max-age=86400");
|
jwt(req as UserJwtRequest, res, () => {
|
||||||
jwt(req, res, () => {
|
|
||||||
res.json(post);
|
res.json(post);
|
||||||
});
|
})
|
||||||
|
} else if (post.visibility === 'protected') {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
// import { Movie } from '../models/Post'
|
// import { Movie } from '../models/Post'
|
||||||
import { User } from '../../lib/models/User'
|
import { User } from '../lib/models/User'
|
||||||
import { File } from '../../lib/models/File'
|
import { File } from '../lib/models/File'
|
||||||
import jwt, { UserJwtRequest } from '../../lib/middleware/jwt'
|
import jwt, { UserJwtRequest } from '../lib/middleware/jwt'
|
||||||
import { Post } from '../../lib/models/Post'
|
import { Post } from '../lib/models/Post'
|
||||||
|
|
||||||
export const users = Router()
|
export const users = Router()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
import { app } from './app';
|
import { app } from './app';
|
||||||
import config from '../lib/config';
|
import config from './lib/config';
|
||||||
import { sequelize } from '../lib/sequelize';
|
import { sequelize } from './lib/sequelize';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await sequelize.sync();
|
await sequelize.sync();
|
||||||
|
|
|
@ -15,6 +15,6 @@
|
||||||
"strictPropertyInitialization": true,
|
"strictPropertyInitialization": true,
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["lib/**/*.ts", "index.ts", "src/**/*.ts"],
|
"include": ["index.ts", "src/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue