diff --git a/README.md b/README.md
index 74a7f5cd..cb12ef79 100644
--- a/README.md
+++ b/README.md
@@ -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
- `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.
+- `SECRET_KEY`: a secret key used for validating API requests that is never exposed to the browser
`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).
- `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.
+- `SECRET_KEY`: the same secret key as the client
## Current status
diff --git a/client/.env.local b/client/.env.local
index 3bcbece7..a4e28f86 100644
--- a/client/.env.local
+++ b/client/.env.local
@@ -1,3 +1,4 @@
API_URL=http://localhost:3000
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."
+SECRET_KEY=secret
\ No newline at end of file
diff --git a/client/components/my-posts/index.tsx b/client/components/my-posts/index.tsx
index 15ea5892..80fbdba3 100644
--- a/client/components/my-posts/index.tsx
+++ b/client/components/my-posts/index.tsx
@@ -1,17 +1,7 @@
-import useSWR from "swr"
import PostList from "../post-list"
-import Cookies from "js-cookie"
-const fetcher = (url: string) => fetch(url, {
- headers: {
- '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
+const MyPosts = ({ posts, error }: { posts: any, error: any }) => {
+ return
}
export default MyPosts
diff --git a/client/components/new-post/index.tsx b/client/components/new-post/index.tsx
index cd77a43e..a962bb8e 100644
--- a/client/components/new-post/index.tsx
+++ b/client/components/new-post/index.tsx
@@ -9,6 +9,7 @@ import Title from './title';
import Cookies from 'js-cookie'
import type { PostVisibility, Document as DocumentType } from '@lib/types';
import PasswordModal from './password';
+import getPostPath from '@lib/get-post-path';
const Post = () => {
const { setToast } = useToasts()
@@ -36,7 +37,7 @@ const Post = () => {
if (res.ok) {
const json = await res.json()
- router.push(`/post/${json.id}`)
+ router.push(getPostPath(json.visibility, json.id))
} else {
const json = await res.json()
setToast({
diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx
index 30c37ecd..c034117f 100644
--- a/client/components/post-list/list-item.tsx
+++ b/client/components/post-list/list-item.tsx
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"
import timeAgo from "@lib/time-ago"
import ShiftBy from "../shift-by"
import VisibilityBadge from "../visibility-badge"
+import getPostPath from "@lib/get-post-path"
const FilenameInput = ({ title }: { title: string }) => {
-
+
{post.title}
diff --git a/client/components/preview/react-markdown-preview.tsx b/client/components/preview/react-markdown-preview.tsx
index b02ac3e1..5507aae2 100644
--- a/client/components/preview/react-markdown-preview.tsx
+++ b/client/components/preview/react-markdown-preview.tsx
@@ -1,6 +1,6 @@
import ReactMarkdown from "react-markdown"
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 rehypeAutolinkHeadings from 'rehype-autolink-headings'
diff --git a/client/lib/get-post-path.ts b/client/lib/get-post-path.ts
new file mode 100644
index 00000000..47745319
--- /dev/null
+++ b/client/lib/get-post-path.ts
@@ -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}`
+ }
+}
diff --git a/client/pages/api/raw/[id].ts b/client/pages/api/raw/[id].ts
index 62ef2745..f8f4d512 100644
--- a/client/pages/api/raw/[id].ts
+++ b/client/pages/api/raw/[id].ts
@@ -2,13 +2,20 @@ import { NextApiRequest, NextApiResponse } from "next"
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
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) {
const data = await file.json()
const { title, content } = data
// serve the file raw as plain text
- res.setHeader("Content-Type", "text/plain")
- res.setHeader('Cache-Control', 's-maxage=86400');
+
if (download) {
res.setHeader("Content-Disposition", `attachment; filename="${title}"`)
} else {
diff --git a/client/pages/api/revalidate.ts b/client/pages/api/revalidate.ts
new file mode 100644
index 00000000..07c62794
--- /dev/null
+++ b/client/pages/api/revalidate.ts
@@ -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')
+ }
+}
diff --git a/client/pages/mine.tsx b/client/pages/mine.tsx
index b273d548..9e502c20 100644
--- a/client/pages/mine.tsx
+++ b/client/pages/mine.tsx
@@ -3,18 +3,48 @@ import { Page } from '@geist-ui/core'
import Header from '@components/header'
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 (
-
+
)
}
+// 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
diff --git a/client/pages/post/[id].tsx b/client/pages/post/[id].tsx
index 5ddeb3f8..20c91bc8 100644
--- a/client/pages/post/[id].tsx
+++ b/client/pages/post/[id].tsx
@@ -1,13 +1,11 @@
import { Button, Page, Text } from "@geist-ui/core";
-import { useRouter } from "next/router";
-import Document from '../../components/document'
-import Header from "../../components/header";
-import VisibilityBadge from "../../components/visibility-badge";
+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 { GetServerSideProps, GetStaticPaths, GetStaticProps } from "next";
+import type { GetStaticPaths, GetStaticProps } from "next";
import { PostVisibility, ThemeProps } from "@lib/types";
type File = {
@@ -51,7 +49,7 @@ const Post = ({ post, theme, changeTheme }: PostProps) => {
@@ -83,53 +81,37 @@ const Post = ({ post, theme, changeTheme }: PostProps) => {
)
}
-export const getServerSideProps: GetServerSideProps = async (context) => {
- const headers = context.req.headers
- const host = headers.host
- const driftToken = cookie.parse(headers.cookie || '')[`drift-token`]
- let driftTheme = cookie.parse(headers.cookie || '')[`drift-theme`]
- if (driftTheme !== "light" && driftTheme !== "dark") {
- 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,
- },
- }
+export const getStaticPaths: GetStaticPaths = async () => {
+ const posts = await fetch(process.env.API_URL + `/posts/`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "x-secret-key": process.env.SECRET_KEY || "",
}
- try {
- const json = await post.json();
- const maxAge = 60 * 60 * 24 * 365;
- context.res.setHeader(
- 'Cache-Control',
- `${json.visibility === "public" ? "public" : "private"}, s-maxage=${maxAge}`
- )
- return {
- props: {
- post: json
- }
- }
- } catch (e) {
- console.log(e)
+ })
+
+ const json = await posts.json()
+ const filtered = json.filter((post: any) => post.visibility === "public" || post.visibility === "unlisted")
+ const paths = filtered.map((post: any) => ({
+ params: { id: post.id }
+ }))
+
+ return { paths, fallback: 'blocking' }
+}
+
+export const getStaticProps: GetStaticProps = async ({ params }) => {
+ const post = await fetch(process.env.API_URL + `/posts/${params?.id}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "x-secret-key": process.env.SECRET_KEY || "",
}
- }
+ })
return {
props: {
- post: null
- }
+ post: await post.json()
+ },
}
}
diff --git a/client/pages/post/private/[id].tsx b/client/pages/post/private/[id].tsx
new file mode 100644
index 00000000..2dfdb3c0
--- /dev/null
+++ b/client/pages/post/private/[id].tsx
@@ -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 (
+
+
+
+
+
+
+
+ {/* {!isLoading && } */}
+
+
+ {post.title}
+
+
+
+
+ {post.files.map(({ id, content, title }: { id: any, content: string, title: string }) => (
+
+ ))}
+
+
+ )
+}
+
+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
+
diff --git a/server/lib/config.ts b/server/src/lib/config.ts
similarity index 100%
rename from server/lib/config.ts
rename to server/src/lib/config.ts
diff --git a/server/lib/middleware/jwt.ts b/server/src/lib/middleware/jwt.ts
similarity index 100%
rename from server/lib/middleware/jwt.ts
rename to server/src/lib/middleware/jwt.ts
diff --git a/server/src/lib/middleware/secret-key.ts b/server/src/lib/middleware/secret-key.ts
new file mode 100644
index 00000000..241fe647
--- /dev/null
+++ b/server/src/lib/middleware/secret-key.ts
@@ -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()
+}
diff --git a/server/lib/models/File.ts b/server/src/lib/models/File.ts
similarity index 100%
rename from server/lib/models/File.ts
rename to server/src/lib/models/File.ts
diff --git a/server/lib/models/Post.ts b/server/src/lib/models/Post.ts
similarity index 100%
rename from server/lib/models/Post.ts
rename to server/src/lib/models/Post.ts
diff --git a/server/lib/models/PostAuthor.ts b/server/src/lib/models/PostAuthor.ts
similarity index 100%
rename from server/lib/models/PostAuthor.ts
rename to server/src/lib/models/PostAuthor.ts
diff --git a/server/lib/models/User.ts b/server/src/lib/models/User.ts
similarity index 100%
rename from server/lib/models/User.ts
rename to server/src/lib/models/User.ts
diff --git a/server/lib/sequelize.ts b/server/src/lib/sequelize.ts
similarity index 100%
rename from server/lib/sequelize.ts
rename to server/src/lib/sequelize.ts
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
index 049445c0..9e77a93a 100644
--- a/server/src/routes/auth.ts
+++ b/server/src/routes/auth.ts
@@ -1,9 +1,9 @@
import { Router } from 'express'
import { genSalt, hash, compare } from "bcrypt"
-import { User } from '../../lib/models/User'
+import { User } from '../lib/models/User'
import { sign } from 'jsonwebtoken'
-import config from '../../lib/config'
-import jwt from '../../lib/middleware/jwt'
+import config from '../lib/config'
+import jwt from '../lib/middleware/jwt'
const NO_EMPTY_SPACE_REGEX = /^\S*$/
diff --git a/server/src/routes/files.ts b/server/src/routes/files.ts
index 7a23751a..13efe551 100644
--- a/server/src/routes/files.ts
+++ b/server/src/routes/files.ts
@@ -1,10 +1,11 @@
import { Router } from 'express'
+import secretKey from '../lib/middleware/secret-key';
// import { Movie } from '../models/Post'
-import { File } from '../../lib/models/File'
+import { File } from '../lib/models/File'
export const files = Router()
-files.get("/raw/:id", async (req, res, next) => {
+files.get("/raw/:id", secretKey, async (req, res, next) => {
try {
const file = await File.findOne({
where: {
@@ -12,18 +13,9 @@ files.get("/raw/:id", async (req, res, next) => {
},
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);
- // } else {
- // TODO: should this be `private, `?
- // res.setHeader("Cache-Control", "max-age=86400");
- // res.json(file);
- // }
}
catch (e) {
next(e);
}
});
-
diff --git a/server/src/routes/posts.ts b/server/src/routes/posts.ts
index 6456458e..70f54bf6 100644
--- a/server/src/routes/posts.ts
+++ b/server/src/routes/posts.ts
@@ -1,10 +1,11 @@
import { Router } from 'express'
// import { Movie } from '../models/Post'
-import { File } from '../../lib/models/File'
-import { Post } from '../../lib/models/Post';
-import jwt, { UserJwtRequest } from '../../lib/middleware/jwt';
+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 { User } from '../lib/models/User';
+import secretKey from '../lib/middleware/secret-key';
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 {
const post = await Post.findOne({
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') {
- res.setHeader("Cache-Control", "public, max-age=86400");
+ if (post.visibility === 'public' || post?.visibility === 'unlisted') {
res.json(post);
- } else {
- // TODO: should this be `private, `?
- res.setHeader("Cache-Control", "max-age=86400");
- jwt(req, res, () => {
+ } else if (post.visibility === 'private') {
+ console.log("here")
+ jwt(req as UserJwtRequest, res, () => {
res.json(post);
- });
+ })
+ } else if (post.visibility === 'protected') {
+
}
}
catch (e) {
diff --git a/server/src/routes/users.ts b/server/src/routes/users.ts
index 3ffefd01..67c793f5 100644
--- a/server/src/routes/users.ts
+++ b/server/src/routes/users.ts
@@ -1,9 +1,9 @@
import { Router } from 'express'
// import { Movie } from '../models/Post'
-import { User } from '../../lib/models/User'
-import { File } from '../../lib/models/File'
-import jwt, { UserJwtRequest } from '../../lib/middleware/jwt'
-import { Post } from '../../lib/models/Post'
+import { User } from '../lib/models/User'
+import { File } from '../lib/models/File'
+import jwt, { UserJwtRequest } from '../lib/middleware/jwt'
+import { Post } from '../lib/models/Post'
export const users = Router()
diff --git a/server/src/server.ts b/server/src/server.ts
index 2e96ea8d..b3670024 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -1,7 +1,7 @@
import { createServer } from 'http';
import { app } from './app';
-import config from '../lib/config';
-import { sequelize } from '../lib/sequelize';
+import config from './lib/config';
+import { sequelize } from './lib/sequelize';
(async () => {
await sequelize.sync();
diff --git a/server/tsconfig.json b/server/tsconfig.json
index e5fbe651..765d8a8c 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -15,6 +15,6 @@
"strictPropertyInitialization": true,
"outDir": "dist"
},
- "include": ["lib/**/*.ts", "index.ts", "src/**/*.ts"],
+ "include": ["index.ts", "src/**/*.ts"],
"exclude": ["node_modules"]
}