rm server code, add markdown rendering, html saving, visibility updating
This commit is contained in:
parent
096cf41eee
commit
c41cf7c5ef
76 changed files with 2853 additions and 11852 deletions
|
@ -1,11 +1,10 @@
|
|||
import Auth from "../components"
|
||||
import Header from "@components/header"
|
||||
import PageWrapper from "@components/page-wrapper"
|
||||
|
||||
export default function SignInPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<PageWrapper>
|
||||
<Auth page="signin" />
|
||||
</>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Auth from "../components"
|
||||
import Header from "@components/header"
|
||||
import { getRequiresPasscode } from "pages/api/auth/requires-passcode"
|
||||
import PageWrapper from "@components/page-wrapper"
|
||||
|
||||
const getPasscode = async () => {
|
||||
return await getRequiresPasscode()
|
||||
|
@ -9,9 +10,8 @@ const getPasscode = async () => {
|
|||
export default async function SignUpPage() {
|
||||
const requiresPasscode = await getPasscode()
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<PageWrapper signedIn={false}>
|
||||
<Auth page="signup" requiresServerPassword={requiresPasscode} />
|
||||
</>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { memo, useEffect, useState } from "react"
|
||||
import styles from "./preview.module.css"
|
||||
import "@styles/markdown.css"
|
||||
import "./marked.css"
|
||||
|
||||
type Props = {
|
||||
height?: number | string
|
||||
|
@ -8,8 +10,13 @@ type Props = {
|
|||
title?: string
|
||||
}
|
||||
|
||||
const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
|
||||
const [preview, setPreview] = useState<string>(content || "")
|
||||
const MarkdownPreview = ({
|
||||
height = 500,
|
||||
fileId,
|
||||
content: initial = "",
|
||||
title
|
||||
}: Props) => {
|
||||
const [content, setPreview] = useState<string>(initial)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
useEffect(() => {
|
||||
async function fetchPost() {
|
||||
|
@ -47,16 +54,28 @@ const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
|
|||
{isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
<article
|
||||
className={styles.markdownPreview}
|
||||
dangerouslySetInnerHTML={{ __html: preview }}
|
||||
style={{
|
||||
height
|
||||
}}
|
||||
/>
|
||||
<StaticPreview content={content} height={height} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(MarkdownPreview)
|
||||
export default MarkdownPreview
|
||||
|
||||
export const StaticPreview = ({
|
||||
content,
|
||||
height = 500
|
||||
}: {
|
||||
content: string
|
||||
height: string | number
|
||||
}) => {
|
||||
return (
|
||||
<article
|
||||
className={styles.markdownPreview}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
style={{
|
||||
height
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
1
client/app/(posts)/components/preview/katex.min.css
vendored
Normal file
1
client/app/(posts)/components/preview/katex.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1011
client/app/(posts)/components/preview/marked.css
Normal file
1011
client/app/(posts)/components/preview/marked.css
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,7 @@
|
|||
import NewPost from "../../components/new"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Header from "@components/header"
|
||||
import { notFound } from "next/navigation"
|
||||
import { getPostById } from "@lib/server/prisma"
|
||||
import PageWrapper from "@components/page-wrapper"
|
||||
|
||||
const NewFromExisting = async ({
|
||||
params
|
||||
|
@ -11,20 +11,17 @@ const NewFromExisting = async ({
|
|||
}
|
||||
}) => {
|
||||
const { id } = params
|
||||
const router = useRouter()
|
||||
|
||||
if (!id) {
|
||||
router.push("/new")
|
||||
return;
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const post = await getPostById(id, true)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header signedIn />
|
||||
<PageWrapper signedIn>
|
||||
<NewPost initialPost={post} newPostParent={id} />
|
||||
</>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import Header from "@components/header"
|
||||
import NewPost from "app/(posts)/new/components/new"
|
||||
import "@styles/react-datepicker.css"
|
||||
import PageWrapper from "@components/page-wrapper"
|
||||
|
||||
const New = () => <>
|
||||
<Header signedIn />
|
||||
<NewPost />
|
||||
</>
|
||||
const New = () => (
|
||||
<PageWrapper signedIn>
|
||||
<NewPost />
|
||||
</PageWrapper>
|
||||
)
|
||||
|
||||
export default New
|
||||
|
|
|
@ -142,13 +142,14 @@ const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
|
|||
</div>
|
||||
)}
|
||||
{/* {post.files.length > 1 && <FileTree files={post.files} />} */}
|
||||
{post.files?.map(({ id, content, title }: File) => (
|
||||
{post.files?.map(({ id, content, title, html }: File) => (
|
||||
<DocumentComponent
|
||||
key={id}
|
||||
title={title}
|
||||
initialTab={"preview"}
|
||||
id={id}
|
||||
content={content}
|
||||
preview={html}
|
||||
/>
|
||||
))}
|
||||
{isAuthor && (
|
||||
|
|
|
@ -15,15 +15,12 @@ const PasswordModalPage = ({ setPost, postId }: Props) => {
|
|||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true)
|
||||
|
||||
const onSubmit = async (password: string) => {
|
||||
const res = await fetch(
|
||||
`/api/posts/authenticate?id=${postId}&password=${password}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
const res = await fetch(`/api/post/${postId}?password=${password}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
setToast({
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
Tooltip,
|
||||
Tag
|
||||
} from "@geist-ui/core/dist"
|
||||
import HtmlPreview from "app/(posts)/components/preview"
|
||||
import { StaticPreview } from "app/(posts)/components/preview"
|
||||
import FadeIn from "@components/fade-in"
|
||||
|
||||
// import Link from "next/link"
|
||||
|
@ -24,6 +24,7 @@ type Props = {
|
|||
skeleton?: boolean
|
||||
id: string
|
||||
content: string
|
||||
preview: string
|
||||
}
|
||||
|
||||
const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
||||
|
@ -63,6 +64,7 @@ const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
|||
|
||||
const Document = ({
|
||||
content,
|
||||
preview,
|
||||
title,
|
||||
initialTab = "edit",
|
||||
skeleton,
|
||||
|
@ -150,11 +152,11 @@ const Document = ({
|
|||
</Tabs.Item>
|
||||
<Tabs.Item label="Preview" value="preview">
|
||||
<div style={{ marginTop: "var(--gap-half)" }}>
|
||||
<HtmlPreview
|
||||
<StaticPreview
|
||||
height={height}
|
||||
fileId={id}
|
||||
content={content}
|
||||
title={title}
|
||||
// fileId={id}
|
||||
content={preview}
|
||||
// title={title}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.Item>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { notFound } from "next/navigation"
|
|||
import { getPostById } from "@lib/server/prisma"
|
||||
import { getCurrentUser, getSession } from "@lib/server/session"
|
||||
import Header from "@components/header"
|
||||
import PageWrapper from "@components/page-wrapper"
|
||||
|
||||
export type PostProps = {
|
||||
post: Post
|
||||
|
@ -16,8 +17,8 @@ export type PostProps = {
|
|||
const getPost = async (id: string) => {
|
||||
const post = await getPostById(id, true)
|
||||
const user = await getCurrentUser()
|
||||
|
||||
console.log("my post", post)
|
||||
|
||||
console.log("post is", post)
|
||||
if (!post) {
|
||||
return notFound()
|
||||
}
|
||||
|
@ -40,9 +41,7 @@ const getPost = async (id: string) => {
|
|||
return notFound()
|
||||
}
|
||||
|
||||
console.log("HERE", post.visibility, isAuthor)
|
||||
if (post.visibility === "protected" && !isAuthor) {
|
||||
console.log("HERE2")
|
||||
return {
|
||||
post,
|
||||
isProtected: true,
|
||||
|
@ -63,10 +62,9 @@ const PostView = async ({
|
|||
}) => {
|
||||
const { post, isProtected, isAuthor, signedIn } = await getPost(params.id)
|
||||
return (
|
||||
<>
|
||||
<Header signedIn={signedIn} />
|
||||
<PageWrapper signedIn={signedIn}>
|
||||
<PostPage isAuthor={isAuthor} isProtected={isProtected} post={post} />
|
||||
</>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
|
|||
|
||||
const sendRequest = useCallback(
|
||||
async (visibility: string, password?: string) => {
|
||||
const res = await fetch(`/server-api/posts/${postId}`, {
|
||||
const res = await fetch(`/api/post/${postId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
|
@ -30,7 +30,6 @@ const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
|
|||
const json = await res.json()
|
||||
setVisibility(json.visibility)
|
||||
} else {
|
||||
const json = await res.json()
|
||||
setToast({
|
||||
text: "An error occurred",
|
||||
type: "error"
|
||||
|
|
7
client/app/components/mdx-remote-wrapper/index.tsx
Normal file
7
client/app/components/mdx-remote-wrapper/index.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { MDXRemote, MDXRemoteProps } from "next-mdx-remote";
|
||||
|
||||
export default function MDXRemoteWrapper(props: MDXRemoteProps) {
|
||||
return <MDXRemote {...props} />;
|
||||
}
|
30
client/app/components/page-wrapper.tsx
Normal file
30
client/app/components/page-wrapper.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
'use client';
|
||||
import Page from "@geist-ui/core/dist/page"
|
||||
import Header from "./header"
|
||||
|
||||
export default function PageWrapper({
|
||||
children,
|
||||
signedIn
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
signedIn?: boolean
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Page.Header>
|
||||
<Header signedIn={signedIn} />
|
||||
</Page.Header>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoadingPageWrapper() {
|
||||
return (
|
||||
<>
|
||||
<Page.Header>
|
||||
<Header signedIn={false} />
|
||||
</Page.Header>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -2,8 +2,8 @@ import { redirect } from "next/navigation"
|
|||
import { getPostsByUser } from "@lib/server/prisma"
|
||||
import PostList from "@components/post-list"
|
||||
import { getCurrentUser } from "@lib/server/session"
|
||||
import Header from "@components/header"
|
||||
import { authOptions } from "@lib/server/auth"
|
||||
import PageWrapper from "@components/page-wrapper"
|
||||
|
||||
export default async function Mine() {
|
||||
const userId = (await getCurrentUser())?.id
|
||||
|
@ -16,9 +16,8 @@ export default async function Mine() {
|
|||
|
||||
const hasMore = false
|
||||
return (
|
||||
<>
|
||||
<Header signedIn />
|
||||
<PageWrapper signedIn>
|
||||
<PostList morePosts={hasMore} initialPosts={posts} />
|
||||
</>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Header from "@components/header"
|
||||
import PageWrapper from "@components/page-wrapper"
|
||||
import { getCurrentUser } from "@lib/server/session"
|
||||
import { getWelcomeContent } from "pages/api/welcome"
|
||||
import Home from "./components/home"
|
||||
|
@ -13,9 +14,8 @@ export default async function Page() {
|
|||
const authed = await getCurrentUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header signedIn={Boolean(authed)}/>
|
||||
<Home rendered={rendered} introContent={content} introTitle={title} />
|
||||
</>
|
||||
<PageWrapper signedIn={Boolean(authed)}>
|
||||
<Home rendered={rendered as string} introContent={content} introTitle={title} />
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,17 +5,17 @@ import Profile from "app/settings/components/sections/profile"
|
|||
import { authOptions } from "@lib/server/auth"
|
||||
import { getCurrentUser } from "@lib/server/session"
|
||||
import { redirect } from "next/navigation"
|
||||
import PageWrapper from "@components/page-wrapper"
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const user = await getCurrentUser()
|
||||
|
||||
if (!user) {
|
||||
redirect(authOptions.pages?.signIn || "/new")
|
||||
return redirect(authOptions.pages?.signIn || "/new")
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header signedIn />
|
||||
<PageWrapper signedIn>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
@ -32,6 +32,6 @@ export default async function SettingsPage() {
|
|||
<Password />
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
</>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
@import "./syntax.css";
|
||||
@import "./markdown.css";
|
||||
@import "./inter.css";
|
||||
|
||||
:root {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import useSWR from "swr"
|
||||
|
||||
// https://2020.paco.me/blog/shared-hook-state-with-swr
|
||||
const useSharedState = <T>(key: string, initial?: T) => {
|
||||
const { data: state, mutate: setState } = useSWR(key, {
|
||||
fallbackData: initial
|
||||
})
|
||||
return [state, setState] as const
|
||||
}
|
||||
|
||||
export default useSharedState
|
|
@ -1,33 +0,0 @@
|
|||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import { getCookie, setCookie } from "cookies-next"
|
||||
import { useEffect } from "react"
|
||||
import useSharedState from "./use-shared-state"
|
||||
|
||||
const useSignedIn = () => {
|
||||
const token = getCookie(TOKEN_COOKIE_NAME)
|
||||
|
||||
const [signedIn, setSignedIn] = useSharedState(
|
||||
"signedIn",
|
||||
typeof window === "undefined" ? false : !!token
|
||||
)
|
||||
|
||||
const signin = (token: string) => {
|
||||
setSignedIn(true)
|
||||
// TODO: investigate SameSite / CORS cookie security
|
||||
setCookie(TOKEN_COOKIE_NAME, token)
|
||||
}
|
||||
|
||||
// useEffect(() => {
|
||||
// if (token) {
|
||||
// setSignedIn(true)
|
||||
// } else {
|
||||
// setSignedIn(false)
|
||||
// }
|
||||
// }, [setSignedIn, token])
|
||||
|
||||
console.log("signed in", signedIn)
|
||||
|
||||
return { signedIn, signin, token }
|
||||
}
|
||||
|
||||
export default useSignedIn
|
|
@ -1,7 +1,6 @@
|
|||
import { marked } from "marked"
|
||||
import Highlight, { defaultProps, Language } from "prism-react-renderer"
|
||||
import Image from "next/image"
|
||||
|
||||
import Highlight, { defaultProps, Language } from "prism-react-renderer"
|
||||
// // image sizes. DDoS Safe?
|
||||
// const imageSizeLink = /^!?\[((?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?)\]\(\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?(?:\s+=(?:[\w%]+)?x(?:[\w%]+)?)?)(?:\s+("(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/;
|
||||
// //@ts-ignore
|
||||
|
@ -31,25 +30,30 @@ const renderer = new marked.Renderer()
|
|||
// )
|
||||
// }
|
||||
|
||||
renderer.link = (href, _, text) => {
|
||||
const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
|
||||
if (isHrefLocal) {
|
||||
return <a href={href || ''}>
|
||||
{text}
|
||||
</a>
|
||||
}
|
||||
// renderer.link = (href, _, text) => {
|
||||
// const isHrefLocal = href?.startsWith("/") || href?.startsWith("#")
|
||||
// if (isHrefLocal) {
|
||||
// return <a href={href || ""}>{text}</a>
|
||||
// }
|
||||
|
||||
// dirty hack
|
||||
// if text contains elements, render as html
|
||||
return <a href={href || ""} target="_blank" rel="noopener noreferrer" dangerouslySetInnerHTML={{ __html: convertHtmlEntities(text) }} ></a>
|
||||
}
|
||||
// // dirty hack
|
||||
// // if text contains elements, render as html
|
||||
// return (
|
||||
// <a
|
||||
// href={href || ""}
|
||||
// target="_blank"
|
||||
// rel="noopener noreferrer"
|
||||
// dangerouslySetInnerHTML={{ __html: convertHtmlEntities(text) }}
|
||||
// ></a>
|
||||
// )
|
||||
// }
|
||||
|
||||
// @ts-ignore
|
||||
renderer.image = function (href, _, text) {
|
||||
return <Image loading="lazy" src="${href}" alt="${text}" layout="fill" />
|
||||
}
|
||||
|
||||
renderer.checkbox = () => ""
|
||||
// renderer.checkbox = () => ""
|
||||
// @ts-ignore
|
||||
renderer.listitem = (text, task, checked) => {
|
||||
if (task) {
|
||||
|
@ -63,24 +67,24 @@ renderer.listitem = (text, task, checked) => {
|
|||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return <li>{text}</li>
|
||||
}
|
||||
|
||||
// //@ts-ignore
|
||||
// renderer.code = (code: string, language: string) => {
|
||||
// return (<pre>
|
||||
// {/* {title && <code>{title} </code>} */}
|
||||
// {/* {language && title && <code style={{}}> {language} </code>} */}
|
||||
// <Code
|
||||
// language={language}
|
||||
// // title={title}
|
||||
// code={code}
|
||||
// // highlight={highlight}
|
||||
// />
|
||||
// </pre>
|
||||
// )
|
||||
// }
|
||||
// @ts-ignore
|
||||
renderer.code = (code: string, language: string) => {
|
||||
const component =
|
||||
<pre>
|
||||
<Code
|
||||
language={language}
|
||||
// title={title}
|
||||
code={code}
|
||||
// highlight={highlight}
|
||||
/>
|
||||
)
|
||||
</pre>
|
||||
|
||||
return component
|
||||
}
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||
import { NextAuthOptions } from "next-auth"
|
||||
import GitHubProvider from "next-auth/providers/github"
|
||||
import prisma from "@lib/server/prisma"
|
||||
import { prisma } from "@lib/server/prisma"
|
||||
import config from "@lib/config"
|
||||
|
||||
const providers: NextAuthOptions["providers"] = [
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import markdown from "../render-markdown"
|
||||
import type { File } from "@lib/server/prisma"
|
||||
import markdown from "@wcj/markdown-to-html"
|
||||
/**
|
||||
* returns rendered HTML from a Drift file
|
||||
*/
|
||||
export function getHtmlFromFile({
|
||||
export async function getHtmlFromFile({
|
||||
content,
|
||||
title
|
||||
}: Pick<File, "content" | "title">) {
|
||||
|
@ -39,6 +39,7 @@ ${content}
|
|||
contentToRender = "\n" + content
|
||||
}
|
||||
|
||||
const html = markdown(contentToRender)
|
||||
const html = markdown(contentToRender, {
|
||||
})
|
||||
return html
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import config from "@lib/config"
|
||||
import { User } from "@prisma/client"
|
||||
import prisma from "@lib/server/prisma"
|
||||
import { prisma } from "@lib/server/prisma"
|
||||
import * as jwt from "jsonwebtoken"
|
||||
import next, { NextApiHandler, NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ declare global {
|
|||
import config from "@lib/config"
|
||||
import { Post, PrismaClient, File, User } from "@prisma/client"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// we want to update iff they exist the createdAt/updated/expired/deleted items
|
||||
// the input could be an array, in which case we'd check each item in the array
|
||||
// if it's an object, we'd check that object
|
||||
|
@ -36,18 +34,16 @@ const updateDates = (input: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
prisma.$use(async (params, next) => {
|
||||
const result = await next(params)
|
||||
return updateDates(result)
|
||||
})
|
||||
|
||||
export default prisma
|
||||
export const prisma =
|
||||
global.prisma ||
|
||||
new PrismaClient({
|
||||
log: ["query"]
|
||||
})
|
||||
|
||||
// https://next-auth.js.org/adapters/prisma
|
||||
const client = globalThis.prisma || prisma
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
||||
if (process.env.NODE_ENV !== "production") global.prisma = prisma
|
||||
|
||||
export type { User, AuthTokens, File, Post } from "@prisma/client"
|
||||
export type { User, File, Post } from "@prisma/client"
|
||||
|
||||
export type PostWithFiles = Post & {
|
||||
files: File[]
|
||||
|
@ -138,7 +134,6 @@ export const createUser = async (
|
|||
throw new Error("Wrong registration password")
|
||||
}
|
||||
|
||||
|
||||
const isUserAdminByDefault =
|
||||
config.enable_admin && (await prisma.user.count()) === 0
|
||||
const userRole = isUserAdminByDefault ? "admin" : "user"
|
||||
|
@ -150,7 +145,6 @@ export const createUser = async (
|
|||
}
|
||||
|
||||
export const getPostById = async (postId: Post["id"], withFiles = false) => {
|
||||
console.log("getPostById", postId)
|
||||
const post = await prisma.post.findUnique({
|
||||
where: {
|
||||
id: postId
|
||||
|
|
|
@ -14,8 +14,12 @@
|
|||
"dependencies": {
|
||||
"@geist-ui/core": "^2.3.8",
|
||||
"@geist-ui/icons": "1.0.2",
|
||||
"@mdx-js/loader": "^2.1.5",
|
||||
"@mdx-js/mdx": "^2.1.5",
|
||||
"@mdx-js/react": "^2.1.5",
|
||||
"@next-auth/prisma-adapter": "^1.0.5",
|
||||
"@prisma/client": "^4.6.1",
|
||||
"@wcj/markdown-to-html": "^2.1.2",
|
||||
"bcrypt": "^5.1.0",
|
||||
"client-zip": "2.2.1",
|
||||
"clsx": "^1.2.1",
|
||||
|
@ -26,6 +30,7 @@
|
|||
"next": "13.0.3-canary.4",
|
||||
"next-auth": "^4.16.4",
|
||||
"next-joi": "^2.2.1",
|
||||
"next-mdx-remote": "^4.2.0",
|
||||
"next-themes": "npm:@wits/next-themes@0.2.7",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"rc-table": "7.24.1",
|
||||
|
@ -35,10 +40,19 @@
|
|||
"react-dropzone": "14.2.3",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-loading-skeleton": "3.1.0",
|
||||
"rehype-parse": "^8.0.4",
|
||||
"rehype-remark": "^9.1.2",
|
||||
"remark": "^14.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-html": "^15.0.1",
|
||||
"remark-mdx": "^2.1.5",
|
||||
"remark-rehype": "^10.1.0",
|
||||
"remark-stringify": "^10.0.2",
|
||||
"server-only": "^0.0.1",
|
||||
"showdown": "^2.1.0",
|
||||
"swr": "1.3.0",
|
||||
"textarea-markdown-editor": "0.1.13",
|
||||
"unified": "^10.1.2",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -54,6 +68,7 @@
|
|||
"cross-env": "7.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.2",
|
||||
"katex": "^0.16.3",
|
||||
"next-unused": "0.0.6",
|
||||
"prettier": "2.6.2",
|
||||
"prisma": "^4.6.1",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
|
||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
import prisma from "@lib/server/prisma"
|
||||
import { prisma } from "@lib/server/prisma"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
export default withMethods(
|
||||
|
@ -10,7 +10,7 @@ export default withMethods(
|
|||
const query = req.query
|
||||
const fileId = parseQueryParam(query.fileId)
|
||||
const content = parseQueryParam(query.content)
|
||||
const title = parseQueryParam(query.title)
|
||||
const title = parseQueryParam(query.title) || "Untitled"
|
||||
|
||||
if (fileId && (content || title)) {
|
||||
return res.status(400).json({ error: "Too many arguments" })
|
||||
|
@ -33,7 +33,7 @@ export default withMethods(
|
|||
return res.status(400).json({ error: "Missing arguments" })
|
||||
}
|
||||
|
||||
const renderedHTML = getHtmlFromFile({
|
||||
const renderedHTML = await getHtmlFromFile({
|
||||
title,
|
||||
content
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import prisma from "@lib/server/prisma"
|
||||
import { prisma } from "@lib/server/prisma"
|
||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
|
||||
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
@ -9,16 +9,14 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
}
|
||||
})
|
||||
|
||||
console.log("file", file, "id", req.query.id)
|
||||
|
||||
if (!file) {
|
||||
return res.status(404).end()
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", "text/plain")
|
||||
res.setHeader("Cache-Control", "public, max-age=4800")
|
||||
console.log(file.html)
|
||||
return res.status(200).write(file.html)
|
||||
}
|
||||
res.status(200).write(file.html)
|
||||
res.end()
|
||||
|
||||
}
|
||||
export default getRawFile
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import prisma from "lib/server/prisma"
|
||||
import {prisma} from "lib/server/prisma"
|
||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
|
||||
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
|
117
client/pages/api/post/[id].ts
Normal file
117
client/pages/api/post/[id].ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
import { getPostById } from "@lib/server/prisma"
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
import { getSession } from "next-auth/react"
|
||||
import { prisma } from "lib/server/prisma"
|
||||
import * as crypto from "crypto"
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === "GET") return handleGet(req, res)
|
||||
else if (req.method === "PUT") return handlePut(req, res)
|
||||
}
|
||||
|
||||
export default withMethods(["GET", "PUT"], handler)
|
||||
|
||||
async function handleGet(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const id = parseQueryParam(req.query.id)
|
||||
const files = req.query.files ? parseQueryParam(req.query.files) : true
|
||||
|
||||
console.log("post id is", id)
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: "Missing id" })
|
||||
}
|
||||
|
||||
const post = await getPostById(id, Boolean(files))
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ message: "Post not found" })
|
||||
}
|
||||
|
||||
if (post.visibility === "public") {
|
||||
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate")
|
||||
return res.json(post)
|
||||
} else if (post.visibility === "unlisted") {
|
||||
res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate")
|
||||
}
|
||||
|
||||
const session = await getSession({ req })
|
||||
|
||||
// the user can always go directly to their own post
|
||||
if (session?.user.id === post.authorId) {
|
||||
return res.json(post)
|
||||
}
|
||||
|
||||
if (post.visibility === "protected") {
|
||||
const password = parseQueryParam(req.query.password)
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(password?.toString() || "")
|
||||
.digest("hex")
|
||||
.toString()
|
||||
|
||||
if (hash === post.password) {
|
||||
return res.json(post)
|
||||
} else {
|
||||
return {
|
||||
isProtected: true,
|
||||
post: {
|
||||
id: post.id,
|
||||
visibility: post.visibility,
|
||||
title: post.title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(404).json({ message: "Post not found" })
|
||||
}
|
||||
|
||||
// PUT is for adjusting visibility and password
|
||||
async function handlePut(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const { password, visibility } = req.body
|
||||
const id = parseQueryParam(req.query.id)
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: "Missing id" })
|
||||
}
|
||||
|
||||
const post = await getPostById(id, false)
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ message: "Post not found" })
|
||||
}
|
||||
|
||||
const session = await getSession({ req })
|
||||
|
||||
const isAuthor = session?.user.id === post.authorId
|
||||
|
||||
if (!isAuthor) {
|
||||
return res.status(403).json({ message: "Unauthorized" })
|
||||
}
|
||||
|
||||
if (visibility === "protected" && !password) {
|
||||
return res.status(400).json({ message: "Missing password" })
|
||||
}
|
||||
|
||||
const hashedPassword = crypto
|
||||
.createHash("sha256")
|
||||
.update(password?.toString() || "")
|
||||
.digest("hex")
|
||||
.toString()
|
||||
|
||||
const updatedPost = await prisma.post.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
visibility,
|
||||
password: visibility === "protected" ? hashedPassword : null
|
||||
}
|
||||
})
|
||||
|
||||
res.json({
|
||||
id: updatedPost.id,
|
||||
visibility: updatedPost.visibility
|
||||
})
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
|
||||
import { authOptions } from "@lib/server/auth"
|
||||
import prisma, { getPostById } from "@lib/server/prisma"
|
||||
import {prisma, getPostById } from "@lib/server/prisma"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
import { File } from "@lib/server/prisma"
|
||||
|
@ -13,18 +13,18 @@ import { getSession } from "next-auth/react"
|
|||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === "POST") {
|
||||
return await handlePost(req, res)
|
||||
} else {
|
||||
return await handleGet(req, res)
|
||||
}
|
||||
return await handlePost(req, res)
|
||||
}
|
||||
|
||||
export default withMethods(["POST", "GET"], handler)
|
||||
export default withMethods(["POST"], handler)
|
||||
|
||||
async function handlePost(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const session = await unstable_getServerSession(req, res, authOptions)
|
||||
if (!session) {
|
||||
console.log("no session")
|
||||
return res.status(401).json({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
const files = req.body.files as File[]
|
||||
const fileTitles = files.map((file) => file.title)
|
||||
|
@ -45,23 +45,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<any>) {
|
|||
.digest("hex")
|
||||
}
|
||||
|
||||
const postFiles = files.map((file) => {
|
||||
const html = getHtmlFromFile(file)
|
||||
|
||||
return {
|
||||
title: file.title,
|
||||
content: file.content,
|
||||
sha: crypto
|
||||
.createHash("sha256")
|
||||
.update(file.content)
|
||||
.digest("hex")
|
||||
.toString(),
|
||||
html: html,
|
||||
userId: session?.user.id
|
||||
// postId: post.id
|
||||
}
|
||||
}) as File[]
|
||||
|
||||
const post = await prisma.post.create({
|
||||
data: {
|
||||
title: req.body.title,
|
||||
|
@ -69,62 +52,51 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<any>) {
|
|||
visibility: req.body.visibility,
|
||||
password: hashedPassword,
|
||||
expiresAt: req.body.expiresAt,
|
||||
parentId: req.body.parentId,
|
||||
// authorId: session?.user.id,
|
||||
author: {
|
||||
connect: {
|
||||
id: session?.user.id
|
||||
}
|
||||
},
|
||||
files: {
|
||||
create: postFiles
|
||||
}
|
||||
// files: {
|
||||
// connectOrCreate: postFiles.map((file) => ({
|
||||
// where: {
|
||||
// sha: file.sha
|
||||
// },
|
||||
// create: file
|
||||
// }))
|
||||
// }
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const html = (await getHtmlFromFile(file)) as string
|
||||
|
||||
return prisma.file.create({
|
||||
data: {
|
||||
title: file.title,
|
||||
content: file.content,
|
||||
sha: crypto
|
||||
.createHash("sha256")
|
||||
.update(file.content)
|
||||
.digest("hex")
|
||||
.toString(),
|
||||
html: html,
|
||||
userId: session.user.id,
|
||||
post: {
|
||||
connect: {
|
||||
id: post.id
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
return res.json(post)
|
||||
} catch (error) {
|
||||
return res.status(500).json(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGet(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const id = parseQueryParam(req.query.id)
|
||||
const files = req.query.files ? parseQueryParam(req.query.files) : true
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({ error: "Missing id" })
|
||||
}
|
||||
|
||||
const post = await getPostById(id, Boolean(files))
|
||||
|
||||
if (!post) {
|
||||
return res.status(404).json({ message: "Post not found" })
|
||||
}
|
||||
|
||||
if (post.visibility === "public") {
|
||||
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate")
|
||||
return res.json(post)
|
||||
} else if (post.visibility === "unlisted") {
|
||||
res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate")
|
||||
}
|
||||
|
||||
const session = await getSession({ req })
|
||||
|
||||
// the user can always go directly to their own post
|
||||
if (session?.user.id === post.authorId) {
|
||||
return res.json(post)
|
||||
}
|
||||
|
||||
if (post.visibility === "protected") {
|
||||
return {
|
||||
isProtected: true,
|
||||
post: {
|
||||
id: post.id,
|
||||
visibility: post.visibility,
|
||||
title: post.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(404).json({ message: "Post not found" })
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// })
|
||||
|
||||
import { USER_COOKIE_NAME } from "@lib/constants"
|
||||
import prisma, { getUserById } from "@lib/server/prisma"
|
||||
import { getUserById } from "@lib/server/prisma"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
|
@ -37,17 +37,20 @@ export default async function handler(
|
|||
message: "Unauthorized"
|
||||
})
|
||||
|
||||
const userId = String(getCookie(USER_COOKIE_NAME, {
|
||||
req, res
|
||||
}))
|
||||
const userId = String(
|
||||
getCookie(USER_COOKIE_NAME, {
|
||||
req,
|
||||
res
|
||||
})
|
||||
)
|
||||
|
||||
if (!userId) {
|
||||
return error()
|
||||
}
|
||||
if (!userId) {
|
||||
return error()
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await getUserById(userId);
|
||||
|
||||
const user = await getUserById(userId)
|
||||
|
||||
if (!user) {
|
||||
return error()
|
||||
}
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
// a nextjs api handerl
|
||||
|
||||
import config from "@lib/config"
|
||||
import renderMarkdown from "@lib/render-markdown"
|
||||
import { getHtmlFromMarkdown } from "@lib/remark-plugins"
|
||||
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
|
||||
import markdown from "@wcj/markdown-to-html"
|
||||
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
export const getWelcomeContent = async () => {
|
||||
const introContent = config.welcome_content
|
||||
const introTitle = config.welcome_title
|
||||
|
||||
|
||||
return {
|
||||
title: introTitle,
|
||||
content: introContent,
|
||||
rendered: renderMarkdown(introContent)
|
||||
rendered: await getHtmlFromFile({
|
||||
title: `intro.md`,
|
||||
content: introContent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
_: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
export default async function handler(_: NextApiRequest, res: NextApiResponse) {
|
||||
const welcomeContent = await getWelcomeContent()
|
||||
if (!welcomeContent) {
|
||||
return res.status(500).json({ error: "Missing welcome content" })
|
||||
}
|
||||
console.log(welcomeContent.title)
|
||||
|
||||
return res.json(welcomeContent)
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
|||
node_modules/
|
||||
__tests__/
|
|
@ -1,4 +0,0 @@
|
|||
JWT_SECRET=secret-jwt
|
||||
MEMORY_DB=true
|
||||
REGISTRATION_PASSWORD=password
|
||||
SECRET_KEY=secret
|
4
server/.gitignore
vendored
4
server/.gitignore
vendored
|
@ -1,4 +0,0 @@
|
|||
.env
|
||||
node_modules/
|
||||
dist/
|
||||
drift.sqlite
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 80,
|
||||
"useTabs": true
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
const path = require("path")
|
||||
|
||||
module.exports = {
|
||||
// config: path.resolve("config", "config.js"),
|
||||
"models-path": path.resolve("src", "lib", "models"),
|
||||
// "seeders-path": path.resolve("src", "seeders"),
|
||||
"migrations-path": path.resolve("src", "migrations")
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
FROM node:17-alpine AS deps
|
||||
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat git
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock tsconfig.json tslint.json ./
|
||||
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
FROM node:17-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ARG NODE_ENV
|
||||
|
||||
ENV NODE_ENV=${NODE_ENV:-production}
|
||||
|
||||
RUN apk add --no-cache git
|
||||
RUN yarn build:docker
|
||||
|
||||
FROM node:17-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG NODE_ENV
|
||||
|
||||
ENV NODE_ENV=${NODE_ENV:-production}
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 drift
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
USER drift
|
||||
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
|
@ -1,11 +0,0 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
setupFiles: ["<rootDir>/test/setup-tests.ts"],
|
||||
moduleNameMapper: {
|
||||
"@lib/(.*)": "<rootDir>/src/lib/$1",
|
||||
"@routes/(.*)": "<rootDir>/src/routes/$1"
|
||||
},
|
||||
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/dist/"]
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
require("dotenv").config()
|
||||
require("./src/database").umzug.runAsCLI()
|
|
@ -1,65 +0,0 @@
|
|||
{
|
||||
"name": "sequelize-typescript-starter",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=production node dist/index.js",
|
||||
"dev": "cross-env NODE_ENV=development nodemon index.ts",
|
||||
"build": "mkdir -p ./dist && cp .env ./dist/.env && tsc -p ./tsconfig.json && tsc-alias -p ./tsconfig.json && yarn post-build",
|
||||
"build:docker": "mkdir -p ./dist && cp .env.test ./dist/.env && tsc -p ./tsconfig.json && tsc-alias -p ./tsconfig.json && yarn post-build",
|
||||
"post-build": "cp package.json ./dist/package.json && cp yarn.lock ./dist/yarn.lock && cd dist && env NODE_ENV=production yarn install",
|
||||
"migrate:up": "ts-node migrate up",
|
||||
"migrate:down": "ts-node migrate down",
|
||||
"migrate": "ts-node migrate",
|
||||
"lint": "prettier --config .prettierrc 'src/**/*.ts' 'index.ts' --write",
|
||||
"test": "jest --silent"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.18.2",
|
||||
"celebrate": "^15.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.16.2",
|
||||
"express-jwt": "^6.1.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"marked": "^4.0.12",
|
||||
"nodemon": "^2.0.15",
|
||||
"prism-react-renderer": "^1.3.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"reflect-metadata": "^0.1.10",
|
||||
"sequelize": "^6.17.0",
|
||||
"sequelize-typescript": "^2.1.3",
|
||||
"sqlite3": "^5.1.2",
|
||||
"strong-error-handler": "^4.0.0",
|
||||
"umzug": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/cors": "2.8.12",
|
||||
"@types/express": "4.17.13",
|
||||
"@types/express-jwt": "6.0.4",
|
||||
"@types/jest": "27.5.0",
|
||||
"@types/jsonwebtoken": "8.5.8",
|
||||
"@types/marked": "4.0.3",
|
||||
"@types/node": "17.0.21",
|
||||
"@types/node-fetch": "2.6.1",
|
||||
"@types/react-dom": "17.0.16",
|
||||
"@types/supertest": "2.0.12",
|
||||
"@types/validator": "^13.7.10",
|
||||
"cross-env": "7.0.3",
|
||||
"jest": "27.5.1",
|
||||
"prettier": "2.6.2",
|
||||
"supertest": "6.2.3",
|
||||
"ts-jest": "27.1.4",
|
||||
"ts-node": "10.7.0",
|
||||
"tsc-alias": "1.6.7",
|
||||
"tsconfig-paths": "3.14.1",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "4.6.4"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,43 +0,0 @@
|
|||
import * as express from "express"
|
||||
import * as bodyParser from "body-parser"
|
||||
import * as errorhandler from "strong-error-handler"
|
||||
import { posts, user, auth, files, admin, health } from "@routes/index"
|
||||
import { errors } from "celebrate"
|
||||
import secretKey from "@lib/middleware/secret-key"
|
||||
import markdown from "@lib/render-markdown"
|
||||
import config from "@lib/config"
|
||||
|
||||
export const app = express()
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
app.use(bodyParser.json({ limit: "5mb" }))
|
||||
|
||||
app.use("/auth", auth)
|
||||
app.use("/posts", posts)
|
||||
app.use("/user", user)
|
||||
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" })
|
||||
}
|
||||
|
||||
return res.json({
|
||||
title: introTitle,
|
||||
content: introContent,
|
||||
rendered: markdown(introContent)
|
||||
})
|
||||
})
|
||||
|
||||
app.use(errors())
|
||||
|
||||
app.use(
|
||||
errorhandler({
|
||||
debug: !config.is_production,
|
||||
log: true
|
||||
})
|
||||
)
|
|
@ -1,48 +0,0 @@
|
|||
import config from "@lib/config"
|
||||
import databasePath from "@lib/get-database-path"
|
||||
import { Sequelize } from "sequelize-typescript"
|
||||
import { SequelizeStorage, Umzug } from "umzug"
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: "sqlite",
|
||||
database: "drift",
|
||||
storage: config.memory_db ? ":memory:" : databasePath,
|
||||
models: [__dirname + "/lib/models"],
|
||||
logging: console.log
|
||||
})
|
||||
|
||||
if (config.memory_db) {
|
||||
console.log("Using in-memory database")
|
||||
} else {
|
||||
console.log(`Database path: ${databasePath}`)
|
||||
}
|
||||
|
||||
export const umzug = new Umzug({
|
||||
migrations: {
|
||||
glob: config.is_production
|
||||
? __dirname + "/migrations/*.js"
|
||||
: __dirname + "/migrations/*.ts"
|
||||
},
|
||||
context: sequelize.getQueryInterface(),
|
||||
storage: new SequelizeStorage({ sequelize }),
|
||||
logger: console
|
||||
})
|
||||
|
||||
export type Migration = typeof umzug._types.migration
|
||||
|
||||
// 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 () => {
|
||||
// Checks migrations and run them if they are not already applied. To keep
|
||||
// track of the executed migrations, a table (and sequelize model) called SequelizeMeta
|
||||
// will be automatically created (if it doesn't exist already) and parsed.
|
||||
console.log("Checking migrations...")
|
||||
const migrations = await umzug.up()
|
||||
if (migrations.length > 0) {
|
||||
console.log("Migrations applied:")
|
||||
console.log(migrations)
|
||||
} else {
|
||||
console.log("No migrations applied.")
|
||||
}
|
||||
})()
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
type Config = {
|
||||
port: number
|
||||
jwt_secret: string
|
||||
drift_home: string
|
||||
is_production: boolean
|
||||
memory_db: boolean
|
||||
enable_admin: boolean
|
||||
secret_key: string
|
||||
registration_password: string
|
||||
welcome_content: string | undefined
|
||||
welcome_title: string | undefined
|
||||
}
|
||||
|
||||
type EnvironmentValue = string | undefined
|
||||
type Environment = { [key: string]: EnvironmentValue }
|
||||
|
||||
export const config = (env: Environment): Config => {
|
||||
const stringToBoolean = (str: EnvironmentValue): boolean => {
|
||||
if (str === "true") {
|
||||
return true
|
||||
} else if (str === "false") {
|
||||
return false
|
||||
} else if (str) {
|
||||
throw new Error(`Invalid boolean value: ${str}`)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const throwIfUndefined = (str: EnvironmentValue, name: string): string => {
|
||||
if (str === undefined) {
|
||||
throw new Error(`Missing environment variable: ${name}`)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
const defaultIfUndefined = (
|
||||
str: EnvironmentValue,
|
||||
defaultValue: string
|
||||
): string => {
|
||||
if (str === undefined) {
|
||||
return defaultValue
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
const validNodeEnvs = (str: EnvironmentValue) => {
|
||||
const valid = ["development", "production", "test"]
|
||||
if (str && !valid.includes(str)) {
|
||||
throw new Error(`Invalid NODE_ENV set: ${str}`)
|
||||
} else if (!str) {
|
||||
console.warn("No NODE_ENV specified, defaulting to development")
|
||||
} else {
|
||||
console.log(`Using NODE_ENV: ${str}`)
|
||||
}
|
||||
}
|
||||
|
||||
const is_production = env.NODE_ENV === "production"
|
||||
|
||||
const developmentDefault = (
|
||||
str: EnvironmentValue,
|
||||
name: string,
|
||||
defaultValue: string
|
||||
): string => {
|
||||
if (is_production) return throwIfUndefined(str, name)
|
||||
return defaultIfUndefined(str, defaultValue)
|
||||
}
|
||||
|
||||
validNodeEnvs(env.NODE_ENV)
|
||||
|
||||
const config: Config = {
|
||||
port: env.PORT ? parseInt(env.PORT) : 3000,
|
||||
jwt_secret: env.JWT_SECRET || "myjwtsecret",
|
||||
drift_home: env.DRIFT_HOME || "~/.drift",
|
||||
is_production,
|
||||
memory_db: stringToBoolean(env.MEMORY_DB),
|
||||
enable_admin: stringToBoolean(env.ENABLE_ADMIN),
|
||||
secret_key: developmentDefault(env.SECRET_KEY, "SECRET_KEY", "secret"),
|
||||
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
||||
welcome_content: env.WELCOME_CONTENT,
|
||||
welcome_title: env.WELCOME_TITLE
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
export default config(process.env)
|
|
@ -1,16 +0,0 @@
|
|||
// https://github.com/thelounge/thelounge/blob/0fb6dae8a68627cd7747ea6164ebe93390fe90f2/src/helper.js#L224
|
||||
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import config from "./config"
|
||||
// Expand ~ into the current user home dir.
|
||||
// This does *not* support `~other_user/tmp` => `/home/other_user/tmp`.
|
||||
function getDatabasePath() {
|
||||
const fileName = "drift.sqlite"
|
||||
const databasePath = `${config.drift_home}/${fileName}`
|
||||
|
||||
const home = os.homedir().replace("$", "$$$$")
|
||||
return path.resolve(databasePath.replace(/^~($|\/|\\)/, home + "$1"))
|
||||
}
|
||||
|
||||
export default getDatabasePath()
|
|
@ -1,40 +0,0 @@
|
|||
import markdown from "./render-markdown"
|
||||
import { File } from "@lib/models/File"
|
||||
|
||||
/**
|
||||
* returns rendered HTML from a Drift file
|
||||
*/
|
||||
function getHtmlFromFile({ content, title }: Pick<File, "content" | "title">) {
|
||||
const renderAsMarkdown = [
|
||||
"markdown",
|
||||
"md",
|
||||
"mdown",
|
||||
"mkdn",
|
||||
"mkd",
|
||||
"mdwn",
|
||||
"mdtxt",
|
||||
"mdtext",
|
||||
"text",
|
||||
""
|
||||
]
|
||||
const fileType = () => {
|
||||
const pathParts = title.split(".")
|
||||
const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""
|
||||
return language
|
||||
}
|
||||
const type = fileType()
|
||||
let contentToRender: string = content || ""
|
||||
|
||||
if (!renderAsMarkdown.includes(type)) {
|
||||
contentToRender = `~~~${type}
|
||||
${content}
|
||||
~~~`
|
||||
} else {
|
||||
contentToRender = "\n" + content
|
||||
}
|
||||
|
||||
const html = markdown(contentToRender)
|
||||
return html
|
||||
}
|
||||
|
||||
export default getHtmlFromFile
|
|
@ -1,50 +0,0 @@
|
|||
// import * as request from 'supertest'
|
||||
// import { app } from '../../../app'
|
||||
import { NextFunction, Response } from "express"
|
||||
import isAdmin from "@lib/middleware/is-admin"
|
||||
import { UserJwtRequest } from "@lib/middleware/jwt"
|
||||
|
||||
describe("is-admin middlware", () => {
|
||||
let mockRequest: Partial<UserJwtRequest>
|
||||
let mockResponse: Partial<Response>
|
||||
let nextFunction: NextFunction = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {}
|
||||
mockResponse = {
|
||||
sendStatus: jest.fn()
|
||||
}
|
||||
})
|
||||
|
||||
it("should return 401 if no authorization header", async () => {
|
||||
const res = mockResponse as Response
|
||||
isAdmin(mockRequest as UserJwtRequest, res, nextFunction)
|
||||
expect(res.sendStatus).toHaveBeenCalledWith(401)
|
||||
})
|
||||
|
||||
it("should return 401 if no token is supplied", async () => {
|
||||
const req = mockRequest as UserJwtRequest
|
||||
req.headers = {
|
||||
authorization: "Bearer"
|
||||
}
|
||||
isAdmin(req, mockResponse as Response, nextFunction)
|
||||
expect(mockResponse.sendStatus).toBeCalledWith(401)
|
||||
})
|
||||
|
||||
it("should return 404 if config.enable_admin is false", async () => {
|
||||
jest.mock("../../config", () => ({
|
||||
enable_admin: false
|
||||
}))
|
||||
|
||||
const req = mockRequest as UserJwtRequest
|
||||
req.headers = {
|
||||
authorization: "Bearer 123"
|
||||
}
|
||||
isAdmin(req, mockResponse as Response, nextFunction)
|
||||
expect(mockResponse.sendStatus).toBeCalledWith(404)
|
||||
})
|
||||
|
||||
// TODO: 403 if !isAdmin
|
||||
// Verify it calls next() if admin
|
||||
// Requires mocking config.enable_admin
|
||||
})
|
|
@ -1,48 +0,0 @@
|
|||
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
||||
import { NextFunction, Response } from "express"
|
||||
|
||||
describe("jwt middlware", () => {
|
||||
let mockRequest: Partial<UserJwtRequest>
|
||||
let mockResponse: Partial<Response>
|
||||
let nextFunction: NextFunction = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {}
|
||||
mockResponse = {
|
||||
sendStatus: jest.fn().mockReturnThis()
|
||||
}
|
||||
})
|
||||
|
||||
it("should return 401 if no authorization header", () => {
|
||||
const res = mockResponse as Response
|
||||
jwt(mockRequest as UserJwtRequest, res, nextFunction)
|
||||
expect(res.sendStatus).toHaveBeenCalledWith(401)
|
||||
})
|
||||
|
||||
it("should return 401 if no token is supplied", () => {
|
||||
const req = mockRequest as UserJwtRequest
|
||||
req.headers = {
|
||||
authorization: "Bearer"
|
||||
}
|
||||
jwt(req, mockResponse as Response, nextFunction)
|
||||
expect(mockResponse.sendStatus).toBeCalledWith(401)
|
||||
})
|
||||
|
||||
// it("should return 401 if token is deleted", async () => {
|
||||
// try {
|
||||
// const tokenString = "123"
|
||||
|
||||
// const req = mockRequest as UserJwtRequest
|
||||
// req.headers = {
|
||||
// authorization: `Bearer ${tokenString}`
|
||||
// }
|
||||
// jwt(req, mockResponse as Response, nextFunction)
|
||||
// expect(mockResponse.sendStatus).toBeCalledWith(401)
|
||||
// expect(mockResponse.json).toBeCalledWith({
|
||||
// message: "Token is no longer valid"
|
||||
// })
|
||||
// } catch (e) {
|
||||
// console.log(e)
|
||||
// }
|
||||
// })
|
||||
})
|
|
@ -1,46 +0,0 @@
|
|||
// import * as request from 'supertest'
|
||||
// import { app } from '../../../app'
|
||||
import { NextFunction, Response } from "express"
|
||||
import { UserJwtRequest } from "@lib/middleware/jwt"
|
||||
import secretKey from "@lib/middleware/secret-key"
|
||||
import config from "@lib/config"
|
||||
|
||||
describe("secret-key middlware", () => {
|
||||
let mockRequest: Partial<UserJwtRequest>
|
||||
let mockResponse: Partial<Response>
|
||||
let nextFunction: NextFunction = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {}
|
||||
mockResponse = {
|
||||
sendStatus: jest.fn()
|
||||
}
|
||||
})
|
||||
|
||||
it("should return 401 if no x-secret-key header", async () => {
|
||||
const res = mockResponse as Response
|
||||
secretKey(mockRequest as UserJwtRequest, res, nextFunction)
|
||||
expect(res.sendStatus).toHaveBeenCalledWith(401)
|
||||
})
|
||||
|
||||
it("should return 401 if x-secret-key does not match server", async () => {
|
||||
const defaultSecretKey = config.secret_key
|
||||
const req = mockRequest as UserJwtRequest
|
||||
req.headers = {
|
||||
authorization: "Bearer",
|
||||
"x-secret-key": defaultSecretKey + "1"
|
||||
}
|
||||
secretKey(req, mockResponse as Response, nextFunction)
|
||||
expect(mockResponse.sendStatus).toBeCalledWith(401)
|
||||
})
|
||||
|
||||
it("should call next() if x-secret-key matches server", async () => {
|
||||
const req = mockRequest as UserJwtRequest
|
||||
req.headers = {
|
||||
authorization: "Bearer",
|
||||
"x-secret-key": config.secret_key
|
||||
}
|
||||
secretKey(req, mockResponse as Response, nextFunction)
|
||||
expect(nextFunction).toBeCalled()
|
||||
})
|
||||
})
|
|
@ -1,43 +0,0 @@
|
|||
import { NextFunction, Request, Response } from "express"
|
||||
import * as jwt from "jsonwebtoken"
|
||||
import config from "../config"
|
||||
import { User as UserModel } from "../models/User"
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface UserJwtRequest extends Request {
|
||||
user?: User
|
||||
}
|
||||
|
||||
export default function isAdmin(
|
||||
req: UserJwtRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (!req.headers?.authorization) {
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
const authHeader = req.headers["authorization"]
|
||||
const token = authHeader && authHeader.split(" ")[1]
|
||||
if (!token) return res.sendStatus(401)
|
||||
if (!config.enable_admin) return res.sendStatus(404)
|
||||
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
|
||||
if (err) return res.sendStatus(403)
|
||||
const userObj = await UserModel.findByPk(user.id, {
|
||||
attributes: {
|
||||
exclude: ["password"]
|
||||
}
|
||||
})
|
||||
|
||||
if (!userObj || userObj.role !== "admin") {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
req.user = userObj
|
||||
|
||||
next()
|
||||
})
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import { AuthToken } from "@lib/models/AuthToken"
|
||||
import { NextFunction, Request, Response } from "express"
|
||||
import * as jwt from "jsonwebtoken"
|
||||
import config from "../config"
|
||||
import { User as UserModel } from "../models/User"
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface UserJwtRequest extends Request {
|
||||
user?: User
|
||||
}
|
||||
|
||||
export default async function authenticateToken(
|
||||
req: UserJwtRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const authHeader = req.headers ? req.headers["authorization"] : undefined
|
||||
const token = authHeader && authHeader.split(" ")[1]
|
||||
|
||||
if (token == null) return res.sendStatus(401)
|
||||
|
||||
const authToken = await AuthToken.findOne({ where: { token: token } })
|
||||
if (authToken == null) {
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
if (authToken.deletedAt) {
|
||||
return res.sendStatus(401).json({
|
||||
message: "Token is no longer valid"
|
||||
})
|
||||
}
|
||||
|
||||
jwt.verify(token, config.jwt_secret, async (err: any, user: any) => {
|
||||
if (err) return res.sendStatus(403)
|
||||
const userObj = await UserModel.findByPk(user.id, {
|
||||
attributes: {
|
||||
exclude: ["password"]
|
||||
}
|
||||
})
|
||||
if (!userObj) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
req.user = user
|
||||
|
||||
next()
|
||||
})
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import config from "@lib/config"
|
||||
import { NextFunction, Request, Response } from "express"
|
||||
|
||||
export default function authenticateToken(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (!(req.headers && req.headers["x-secret-key"])) {
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
const requestKey = req.headers["x-secret-key"]
|
||||
if (requestKey !== config.secret_key) {
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
next()
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
import {
|
||||
Model,
|
||||
Column,
|
||||
Table,
|
||||
IsUUID,
|
||||
PrimaryKey,
|
||||
DataType,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
DeletedAt,
|
||||
Unique,
|
||||
BelongsTo,
|
||||
ForeignKey
|
||||
} from "sequelize-typescript"
|
||||
import { User } from "./User"
|
||||
|
||||
@Table
|
||||
export class AuthToken extends Model {
|
||||
@IsUUID(4)
|
||||
@PrimaryKey
|
||||
@Unique
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
defaultValue: DataType.UUIDV4
|
||||
})
|
||||
id!: string
|
||||
|
||||
@Column
|
||||
token!: string
|
||||
|
||||
@BelongsTo(() => User, "userId")
|
||||
user!: User
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId!: number
|
||||
|
||||
@Column
|
||||
expiredReason?: string
|
||||
|
||||
@CreatedAt
|
||||
@Column
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column
|
||||
updatedAt!: Date
|
||||
|
||||
@DeletedAt
|
||||
@Column
|
||||
deletedAt?: Date
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
IsUUID,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Scopes,
|
||||
Table,
|
||||
Unique
|
||||
} from "sequelize-typescript"
|
||||
import { Post } from "./Post"
|
||||
import { User } from "./User"
|
||||
|
||||
@Scopes(() => ({
|
||||
full: {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
through: { attributes: [] }
|
||||
},
|
||||
{
|
||||
model: Post,
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
@Table({
|
||||
tableName: "files"
|
||||
})
|
||||
export class File extends Model {
|
||||
@IsUUID(4)
|
||||
@PrimaryKey
|
||||
@Unique
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
defaultValue: DataType.UUIDV4
|
||||
})
|
||||
id!: string
|
||||
|
||||
@Column
|
||||
title!: string
|
||||
|
||||
@Column
|
||||
content!: string
|
||||
|
||||
@Column
|
||||
sha!: string
|
||||
|
||||
@Column
|
||||
html!: string
|
||||
|
||||
@BelongsTo(() => User, "userId")
|
||||
user!: User
|
||||
|
||||
@BelongsTo(() => Post, "postId")
|
||||
post!: Post
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId!: number
|
||||
|
||||
@ForeignKey(() => Post)
|
||||
@Column
|
||||
postId!: number
|
||||
|
||||
@CreatedAt
|
||||
@Column
|
||||
createdAt!: Date
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
import {
|
||||
BelongsToMany,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
HasMany,
|
||||
HasOne,
|
||||
IsUUID,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Scopes,
|
||||
Table,
|
||||
Unique,
|
||||
UpdatedAt
|
||||
} from "sequelize-typescript"
|
||||
import { PostAuthor } from "./PostAuthor"
|
||||
import { User } from "./User"
|
||||
import { File } from "./File"
|
||||
|
||||
@Scopes(() => ({
|
||||
user: {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
},
|
||||
full: {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
through: { attributes: [] }
|
||||
},
|
||||
{
|
||||
model: File,
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
@Table({
|
||||
tableName: "posts"
|
||||
})
|
||||
export class Post extends Model {
|
||||
@IsUUID(4)
|
||||
@PrimaryKey
|
||||
@Unique
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
defaultValue: DataType.UUIDV4
|
||||
})
|
||||
id!: string
|
||||
|
||||
@Column
|
||||
title!: string
|
||||
|
||||
@Column
|
||||
description?: string
|
||||
|
||||
@BelongsToMany(() => User, () => PostAuthor)
|
||||
users?: User[]
|
||||
|
||||
@HasMany(() => File, { constraints: false })
|
||||
files?: File[]
|
||||
|
||||
@CreatedAt
|
||||
@Column
|
||||
createdAt!: Date
|
||||
|
||||
@Column
|
||||
visibility!: string
|
||||
|
||||
@Column
|
||||
password?: string
|
||||
|
||||
@UpdatedAt
|
||||
@Column
|
||||
updatedAt!: Date
|
||||
|
||||
@Column
|
||||
deletedAt?: Date
|
||||
|
||||
@Column
|
||||
expiresAt?: Date
|
||||
|
||||
@HasOne(() => Post, { foreignKey: "parentId", constraints: false })
|
||||
parent?: Post
|
||||
|
||||
// TODO: deletedBy
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import {
|
||||
Model,
|
||||
Column,
|
||||
Table,
|
||||
ForeignKey,
|
||||
IsUUID,
|
||||
PrimaryKey,
|
||||
DataType,
|
||||
Unique
|
||||
} from "sequelize-typescript"
|
||||
import { Post } from "./Post"
|
||||
import { User } from "./User"
|
||||
|
||||
@Table({
|
||||
tableName: "post_authors"
|
||||
})
|
||||
export class PostAuthor extends Model {
|
||||
@IsUUID(4)
|
||||
@PrimaryKey
|
||||
@Unique
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
defaultValue: DataType.UUIDV4
|
||||
})
|
||||
id!: string
|
||||
|
||||
@ForeignKey(() => Post)
|
||||
@Column
|
||||
postId!: number
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId!: number
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import {
|
||||
Model,
|
||||
Column,
|
||||
Table,
|
||||
BelongsToMany,
|
||||
Scopes,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
IsUUID,
|
||||
PrimaryKey,
|
||||
DataType,
|
||||
Unique
|
||||
} from "sequelize-typescript"
|
||||
import { Post } from "./Post"
|
||||
import { PostAuthor } from "./PostAuthor"
|
||||
|
||||
@Scopes(() => ({
|
||||
posts: {
|
||||
include: [
|
||||
{
|
||||
model: Post,
|
||||
through: { attributes: [] }
|
||||
}
|
||||
]
|
||||
},
|
||||
withoutPassword: {
|
||||
attributes: {
|
||||
exclude: ["password"]
|
||||
}
|
||||
}
|
||||
}))
|
||||
@Table({
|
||||
tableName: "users"
|
||||
})
|
||||
export class User extends Model {
|
||||
@IsUUID(4)
|
||||
@PrimaryKey
|
||||
@Unique
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
defaultValue: DataType.UUIDV4
|
||||
})
|
||||
id!: string
|
||||
|
||||
@Column
|
||||
username!: string
|
||||
|
||||
@Column
|
||||
password!: string
|
||||
|
||||
@BelongsToMany(() => Post, () => PostAuthor)
|
||||
posts?: Post[]
|
||||
|
||||
@CreatedAt
|
||||
@Column
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column
|
||||
updatedAt!: Date
|
||||
|
||||
@Column
|
||||
role!: string
|
||||
|
||||
@Column
|
||||
email?: string
|
||||
|
||||
@Column
|
||||
displayName?: string
|
||||
|
||||
@Column
|
||||
bio?: string
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
import { marked } from 'marked'
|
||||
import Highlight, { defaultProps, Language, } from 'prism-react-renderer'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
|
||||
// // image sizes. DDoS Safe?
|
||||
// const imageSizeLink = /^!?\[((?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?)\]\(\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?(?:\s+=(?:[\w%]+)?x(?:[\w%]+)?)?)(?:\s+("(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/;
|
||||
// //@ts-ignore
|
||||
// Lexer.rules.inline.normal.link = imageSizeLink;
|
||||
// //@ts-ignore
|
||||
// Lexer.rules.inline.gfm.link = imageSizeLink;
|
||||
// //@ts-ignore
|
||||
// Lexer.rules.inline.breaks.link = imageSizeLink;
|
||||
|
||||
//@ts-ignore
|
||||
delete defaultProps.theme
|
||||
// import linkStyles from '../components/link/link.module.css'
|
||||
|
||||
const renderer = new marked.Renderer()
|
||||
|
||||
renderer.heading = (text, level, _, slugger) => {
|
||||
const id = slugger.slug(text)
|
||||
const Component = `h${level}`
|
||||
|
||||
return renderToStaticMarkup(
|
||||
//@ts-ignore
|
||||
<Component>
|
||||
<a href={`#${id}`} id={id} style={{ color: "inherit" }} dangerouslySetInnerHTML={{ __html: (text) }} >
|
||||
</a>
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
// renderer.link = (href, _, text) => {
|
||||
// const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
|
||||
// if (isHrefLocal) {
|
||||
// return renderToStaticMarkup(
|
||||
// <a href={href || ''}>
|
||||
// {text}
|
||||
// </a>
|
||||
// )
|
||||
// }
|
||||
|
||||
// // dirty hack
|
||||
// // if text contains elements, render as html
|
||||
// return <a href={href || ""} target="_blank" rel="noopener noreferrer" dangerouslySetInnerHTML={{ __html: convertHtmlEntities(text) }} ></a>
|
||||
// }
|
||||
|
||||
|
||||
renderer.image = function (href, _, text) {
|
||||
return `<Image loading="lazy" src="${href}" alt="${text}" layout="fill" />`
|
||||
}
|
||||
|
||||
renderer.checkbox = () => ''
|
||||
renderer.listitem = (text, task, checked) => {
|
||||
if (task) {
|
||||
return `<li class="reset"><span class="check">​<input type="checkbox" disabled ${checked ? 'checked' : ''
|
||||
} /></span><span>${text}</span></li>`
|
||||
}
|
||||
|
||||
return `<li>${text}</li>`
|
||||
}
|
||||
|
||||
renderer.code = (code: string, language: string) => {
|
||||
return renderToStaticMarkup(
|
||||
<pre>
|
||||
{/* {title && <code>{title} </code>} */}
|
||||
{/* {language && title && <code style={{}}> {language} </code>} */}
|
||||
<Code
|
||||
language={language}
|
||||
// title={title}
|
||||
code={code}
|
||||
// highlight={highlight}
|
||||
/>
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
headerIds: true,
|
||||
renderer,
|
||||
})
|
||||
|
||||
const markdown = (markdown: string) => marked(markdown)
|
||||
|
||||
export default markdown
|
||||
|
||||
const Code = ({ code, language, highlight, title, ...props }: {
|
||||
code: string,
|
||||
language: string,
|
||||
highlight?: string,
|
||||
title?: string,
|
||||
}) => {
|
||||
if (!language)
|
||||
return (
|
||||
<>
|
||||
<code {...props} dangerouslySetInnerHTML={{ __html: code }} />
|
||||
</>
|
||||
)
|
||||
|
||||
const highlightedLines = highlight
|
||||
//@ts-ignore
|
||||
? highlight.split(',').reduce((lines, h) => {
|
||||
if (h.includes('-')) {
|
||||
// Expand ranges like 3-5 into [3,4,5]
|
||||
const [start, end] = h.split('-').map(Number)
|
||||
const x = Array(end - start + 1)
|
||||
.fill(undefined)
|
||||
.map((_, i) => i + start)
|
||||
return [...lines, ...x]
|
||||
}
|
||||
|
||||
return [...lines, Number(h)]
|
||||
}, [])
|
||||
: ''
|
||||
|
||||
// https://mdxjs.com/guides/syntax-harkedighlighting#all-together
|
||||
return (
|
||||
<>
|
||||
<Highlight {...defaultProps} code={code.trim()} language={language as Language} >
|
||||
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
||||
<code className={className} style={{ ...style }}>
|
||||
{
|
||||
tokens.map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
{...getLineProps({ line, key: i })}
|
||||
style={
|
||||
//@ts-ignore
|
||||
highlightedLines.includes((i + 1).toString())
|
||||
? {
|
||||
background: 'var(--highlight)',
|
||||
margin: '0 -1rem',
|
||||
padding: '0 1rem',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{
|
||||
line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({ token, key })} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</code>
|
||||
)}
|
||||
</Highlight>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.createTable("users", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
deletedAt: {
|
||||
type: DataTypes.DATE
|
||||
}
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.dropTable("users")
|
|
@ -1,12 +0,0 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.addColumn("users", "role", {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: "user"
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.removeColumn("users", "role")
|
|
@ -1,34 +0,0 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.createTable("posts", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
visibility: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
deletedAt: {
|
||||
type: DataTypes.DATE
|
||||
}
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
await queryInterface.dropTable("posts")
|
|
@ -1,58 +0,0 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.createTable("files", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
sha: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
html: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
deletedAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: "users",
|
||||
key: "id"
|
||||
},
|
||||
onDelete: "SET NULL",
|
||||
onUpdate: "CASCADE"
|
||||
},
|
||||
postId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: "posts",
|
||||
key: "id"
|
||||
},
|
||||
onDelete: "SET NULL",
|
||||
onUpdate: "CASCADE"
|
||||
}
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
await queryInterface.dropTable("files")
|
|
@ -1,41 +0,0 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.createTable("post_authors", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
postId: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
references: {
|
||||
model: "posts",
|
||||
key: "id"
|
||||
},
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "CASCADE"
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
references: {
|
||||
model: "users",
|
||||
key: "id"
|
||||
},
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "CASCADE"
|
||||
}
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
await queryInterface.dropTable("post_authors")
|
|
@ -1,12 +0,0 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.addColumn("posts", "expiresAt", {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
await queryInterface.removeColumn("posts", "expiresAt")
|
|
@ -1,12 +0,0 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.addColumn("posts", "parentId", {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
await queryInterface.removeColumn("posts", "parentId")
|
|
@ -1,43 +0,0 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.createTable("AuthTokens", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
token: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
expiredReason: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
deletedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: "users",
|
||||
key: "id"
|
||||
},
|
||||
onUpdate: "CASCADE",
|
||||
onDelete: "CASCADE"
|
||||
}
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.dropTable("AuthTokens")
|
|
@ -1,12 +0,0 @@
|
|||
"use strict"
|
||||
import { DataTypes } from "sequelize"
|
||||
import type { Migration } from "../database"
|
||||
|
||||
export const up: Migration = async ({ context: queryInterface }) =>
|
||||
queryInterface.addColumn("posts", "description", {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
})
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
await queryInterface.removeColumn("posts", "description")
|
|
@ -1,26 +0,0 @@
|
|||
"use strict"
|
||||
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
|
||||
})
|
||||
])
|
||||
|
||||
export const down: Migration = async ({ context: queryInterface }) =>
|
||||
Promise.all([
|
||||
queryInterface.removeColumn("users", "email"),
|
||||
queryInterface.removeColumn("users", "displayName"),
|
||||
queryInterface.removeColumn("users", "bio")
|
||||
])
|
|
@ -1,10 +0,0 @@
|
|||
import { createServer } from "http"
|
||||
import { app } from "./app"
|
||||
import config from "./lib/config"
|
||||
import "./database"
|
||||
;(async () => {
|
||||
// await sequelize.sync()
|
||||
createServer(app).listen(config.port, () =>
|
||||
console.info(`Server running on port ${config.port}`)
|
||||
)
|
||||
})()
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react-jsx",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noUnusedLocals": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"pretty": true,
|
||||
"strictNullChecks": true,
|
||||
"skipLibCheck": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@routes/*": ["./src/routes/*"],
|
||||
"@lib/*": ["./src/lib/*"]
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"require": ["tsconfig-paths/register"]
|
||||
},
|
||||
"include": ["index.ts", "src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
{
|
||||
"extends": "tslint:latest",
|
||||
"rules": {
|
||||
"arrow-parens": false,
|
||||
"class-name": false,
|
||||
"no-empty": false,
|
||||
"unified-signatures": false,
|
||||
"no-trailing-whitespace": false,
|
||||
"no-submodule-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"interface-name": false,
|
||||
"no-consecutive-blank-lines": false,
|
||||
"no-object-literal-type-assertion": false,
|
||||
"no-unused-expression": false,
|
||||
"trailing-comma": false,
|
||||
"ordered-imports": false,
|
||||
"no-unused-imports": [true, {
|
||||
"ignoreExports": true,
|
||||
"ignoreDeclarations": true,
|
||||
"ignoreTypeReferences": true
|
||||
}],
|
||||
"max-line-length": [
|
||||
true,
|
||||
140
|
||||
],
|
||||
"member-access": false,
|
||||
"no-string-literal": false,
|
||||
"curly": false,
|
||||
"only-arrow-functions": false,
|
||||
"typedef": [
|
||||
true
|
||||
],
|
||||
"no-var-requires": false,
|
||||
"quotemark": [
|
||||
"single"
|
||||
],
|
||||
"triple-equals": true,
|
||||
"member-ordering": [
|
||||
true,
|
||||
"public-before-private",
|
||||
"variables-before-functions"
|
||||
],
|
||||
"variable-name": [
|
||||
true,
|
||||
"ban-keywords",
|
||||
"check-format",
|
||||
"allow-leading-underscore",
|
||||
"allow-pascal-case"
|
||||
]
|
||||
}
|
||||
}
|
5178
server/yarn.lock
5178
server/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue