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 Auth from "../components"
|
||||||
import Header from "@components/header"
|
import PageWrapper from "@components/page-wrapper"
|
||||||
|
|
||||||
export default function SignInPage() {
|
export default function SignInPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper>
|
||||||
<Header />
|
|
||||||
<Auth page="signin" />
|
<Auth page="signin" />
|
||||||
</>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Auth from "../components"
|
import Auth from "../components"
|
||||||
import Header from "@components/header"
|
import Header from "@components/header"
|
||||||
import { getRequiresPasscode } from "pages/api/auth/requires-passcode"
|
import { getRequiresPasscode } from "pages/api/auth/requires-passcode"
|
||||||
|
import PageWrapper from "@components/page-wrapper"
|
||||||
|
|
||||||
const getPasscode = async () => {
|
const getPasscode = async () => {
|
||||||
return await getRequiresPasscode()
|
return await getRequiresPasscode()
|
||||||
|
@ -9,9 +10,8 @@ const getPasscode = async () => {
|
||||||
export default async function SignUpPage() {
|
export default async function SignUpPage() {
|
||||||
const requiresPasscode = await getPasscode()
|
const requiresPasscode = await getPasscode()
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper signedIn={false}>
|
||||||
<Header />
|
|
||||||
<Auth page="signup" requiresServerPassword={requiresPasscode} />
|
<Auth page="signup" requiresServerPassword={requiresPasscode} />
|
||||||
</>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { memo, useEffect, useState } from "react"
|
import { memo, useEffect, useState } from "react"
|
||||||
import styles from "./preview.module.css"
|
import styles from "./preview.module.css"
|
||||||
|
import "@styles/markdown.css"
|
||||||
|
import "./marked.css"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
height?: number | string
|
height?: number | string
|
||||||
|
@ -8,8 +10,13 @@ type Props = {
|
||||||
title?: string
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
|
const MarkdownPreview = ({
|
||||||
const [preview, setPreview] = useState<string>(content || "")
|
height = 500,
|
||||||
|
fileId,
|
||||||
|
content: initial = "",
|
||||||
|
title
|
||||||
|
}: Props) => {
|
||||||
|
const [content, setPreview] = useState<string>(initial)
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchPost() {
|
async function fetchPost() {
|
||||||
|
@ -47,16 +54,28 @@ const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<article
|
<StaticPreview content={content} height={height} />
|
||||||
className={styles.markdownPreview}
|
|
||||||
dangerouslySetInnerHTML={{ __html: preview }}
|
|
||||||
style={{
|
|
||||||
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 NewPost from "../../components/new"
|
||||||
import { useRouter } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import Header from "@components/header"
|
|
||||||
import { getPostById } from "@lib/server/prisma"
|
import { getPostById } from "@lib/server/prisma"
|
||||||
|
import PageWrapper from "@components/page-wrapper"
|
||||||
|
|
||||||
const NewFromExisting = async ({
|
const NewFromExisting = async ({
|
||||||
params
|
params
|
||||||
|
@ -11,20 +11,17 @@ const NewFromExisting = async ({
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
const { id } = params
|
const { id } = params
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
router.push("/new")
|
return notFound()
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = await getPostById(id, true)
|
const post = await getPostById(id, true)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper signedIn>
|
||||||
<Header signedIn />
|
|
||||||
<NewPost initialPost={post} newPostParent={id} />
|
<NewPost initialPost={post} newPostParent={id} />
|
||||||
</>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import Header from "@components/header"
|
import Header from "@components/header"
|
||||||
import NewPost from "app/(posts)/new/components/new"
|
import NewPost from "app/(posts)/new/components/new"
|
||||||
import "@styles/react-datepicker.css"
|
import "@styles/react-datepicker.css"
|
||||||
|
import PageWrapper from "@components/page-wrapper"
|
||||||
|
|
||||||
const New = () => <>
|
const New = () => (
|
||||||
<Header signedIn />
|
<PageWrapper signedIn>
|
||||||
<NewPost />
|
<NewPost />
|
||||||
</>
|
</PageWrapper>
|
||||||
|
)
|
||||||
|
|
||||||
export default New
|
export default New
|
||||||
|
|
|
@ -142,13 +142,14 @@ const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* {post.files.length > 1 && <FileTree files={post.files} />} */}
|
{/* {post.files.length > 1 && <FileTree files={post.files} />} */}
|
||||||
{post.files?.map(({ id, content, title }: File) => (
|
{post.files?.map(({ id, content, title, html }: File) => (
|
||||||
<DocumentComponent
|
<DocumentComponent
|
||||||
key={id}
|
key={id}
|
||||||
title={title}
|
title={title}
|
||||||
initialTab={"preview"}
|
initialTab={"preview"}
|
||||||
id={id}
|
id={id}
|
||||||
content={content}
|
content={content}
|
||||||
|
preview={html}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isAuthor && (
|
{isAuthor && (
|
||||||
|
|
|
@ -15,15 +15,12 @@ const PasswordModalPage = ({ setPost, postId }: Props) => {
|
||||||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true)
|
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true)
|
||||||
|
|
||||||
const onSubmit = async (password: string) => {
|
const onSubmit = async (password: string) => {
|
||||||
const res = await fetch(
|
const res = await fetch(`/api/post/${postId}?password=${password}`, {
|
||||||
`/api/posts/authenticate?id=${postId}&password=${password}`,
|
method: "GET",
|
||||||
{
|
headers: {
|
||||||
method: "GET",
|
"Content-Type": "application/json"
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setToast({
|
setToast({
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Tag
|
Tag
|
||||||
} from "@geist-ui/core/dist"
|
} 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 FadeIn from "@components/fade-in"
|
||||||
|
|
||||||
// import Link from "next/link"
|
// import Link from "next/link"
|
||||||
|
@ -24,6 +24,7 @@ type Props = {
|
||||||
skeleton?: boolean
|
skeleton?: boolean
|
||||||
id: string
|
id: string
|
||||||
content: string
|
content: string
|
||||||
|
preview: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
||||||
|
@ -63,6 +64,7 @@ const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
||||||
|
|
||||||
const Document = ({
|
const Document = ({
|
||||||
content,
|
content,
|
||||||
|
preview,
|
||||||
title,
|
title,
|
||||||
initialTab = "edit",
|
initialTab = "edit",
|
||||||
skeleton,
|
skeleton,
|
||||||
|
@ -150,11 +152,11 @@ const Document = ({
|
||||||
</Tabs.Item>
|
</Tabs.Item>
|
||||||
<Tabs.Item label="Preview" value="preview">
|
<Tabs.Item label="Preview" value="preview">
|
||||||
<div style={{ marginTop: "var(--gap-half)" }}>
|
<div style={{ marginTop: "var(--gap-half)" }}>
|
||||||
<HtmlPreview
|
<StaticPreview
|
||||||
height={height}
|
height={height}
|
||||||
fileId={id}
|
// fileId={id}
|
||||||
content={content}
|
content={preview}
|
||||||
title={title}
|
// title={title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Item>
|
</Tabs.Item>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { notFound } from "next/navigation"
|
||||||
import { getPostById } from "@lib/server/prisma"
|
import { getPostById } from "@lib/server/prisma"
|
||||||
import { getCurrentUser, getSession } from "@lib/server/session"
|
import { getCurrentUser, getSession } from "@lib/server/session"
|
||||||
import Header from "@components/header"
|
import Header from "@components/header"
|
||||||
|
import PageWrapper from "@components/page-wrapper"
|
||||||
|
|
||||||
export type PostProps = {
|
export type PostProps = {
|
||||||
post: Post
|
post: Post
|
||||||
|
@ -16,8 +17,8 @@ export type PostProps = {
|
||||||
const getPost = async (id: string) => {
|
const getPost = async (id: string) => {
|
||||||
const post = await getPostById(id, true)
|
const post = await getPostById(id, true)
|
||||||
const user = await getCurrentUser()
|
const user = await getCurrentUser()
|
||||||
|
|
||||||
console.log("my post", post)
|
console.log("post is", post)
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
@ -40,9 +41,7 @@ const getPost = async (id: string) => {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("HERE", post.visibility, isAuthor)
|
|
||||||
if (post.visibility === "protected" && !isAuthor) {
|
if (post.visibility === "protected" && !isAuthor) {
|
||||||
console.log("HERE2")
|
|
||||||
return {
|
return {
|
||||||
post,
|
post,
|
||||||
isProtected: true,
|
isProtected: true,
|
||||||
|
@ -63,10 +62,9 @@ const PostView = async ({
|
||||||
}) => {
|
}) => {
|
||||||
const { post, isProtected, isAuthor, signedIn } = await getPost(params.id)
|
const { post, isProtected, isAuthor, signedIn } = await getPost(params.id)
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper signedIn={signedIn}>
|
||||||
<Header signedIn={signedIn} />
|
|
||||||
<PostPage isAuthor={isAuthor} isProtected={isProtected} post={post} />
|
<PostPage isAuthor={isAuthor} isProtected={isProtected} post={post} />
|
||||||
</>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
|
||||||
|
|
||||||
const sendRequest = useCallback(
|
const sendRequest = useCallback(
|
||||||
async (visibility: string, password?: string) => {
|
async (visibility: string, password?: string) => {
|
||||||
const res = await fetch(`/server-api/posts/${postId}`, {
|
const res = await fetch(`/api/post/${postId}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
|
@ -30,7 +30,6 @@ const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
setVisibility(json.visibility)
|
setVisibility(json.visibility)
|
||||||
} else {
|
} else {
|
||||||
const json = await res.json()
|
|
||||||
setToast({
|
setToast({
|
||||||
text: "An error occurred",
|
text: "An error occurred",
|
||||||
type: "error"
|
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 { getPostsByUser } from "@lib/server/prisma"
|
||||||
import PostList from "@components/post-list"
|
import PostList from "@components/post-list"
|
||||||
import { getCurrentUser } from "@lib/server/session"
|
import { getCurrentUser } from "@lib/server/session"
|
||||||
import Header from "@components/header"
|
|
||||||
import { authOptions } from "@lib/server/auth"
|
import { authOptions } from "@lib/server/auth"
|
||||||
|
import PageWrapper from "@components/page-wrapper"
|
||||||
|
|
||||||
export default async function Mine() {
|
export default async function Mine() {
|
||||||
const userId = (await getCurrentUser())?.id
|
const userId = (await getCurrentUser())?.id
|
||||||
|
@ -16,9 +16,8 @@ export default async function Mine() {
|
||||||
|
|
||||||
const hasMore = false
|
const hasMore = false
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper signedIn>
|
||||||
<Header signedIn />
|
|
||||||
<PostList morePosts={hasMore} initialPosts={posts} />
|
<PostList morePosts={hasMore} initialPosts={posts} />
|
||||||
</>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Header from "@components/header"
|
import Header from "@components/header"
|
||||||
|
import PageWrapper from "@components/page-wrapper"
|
||||||
import { getCurrentUser } from "@lib/server/session"
|
import { getCurrentUser } from "@lib/server/session"
|
||||||
import { getWelcomeContent } from "pages/api/welcome"
|
import { getWelcomeContent } from "pages/api/welcome"
|
||||||
import Home from "./components/home"
|
import Home from "./components/home"
|
||||||
|
@ -13,9 +14,8 @@ export default async function Page() {
|
||||||
const authed = await getCurrentUser();
|
const authed = await getCurrentUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper signedIn={Boolean(authed)}>
|
||||||
<Header signedIn={Boolean(authed)}/>
|
<Home rendered={rendered as string} introContent={content} introTitle={title} />
|
||||||
<Home rendered={rendered} introContent={content} introTitle={title} />
|
</PageWrapper>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,17 +5,17 @@ import Profile from "app/settings/components/sections/profile"
|
||||||
import { authOptions } from "@lib/server/auth"
|
import { authOptions } from "@lib/server/auth"
|
||||||
import { getCurrentUser } from "@lib/server/session"
|
import { getCurrentUser } from "@lib/server/session"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
|
import PageWrapper from "@components/page-wrapper"
|
||||||
|
|
||||||
export default async function SettingsPage() {
|
export default async function SettingsPage() {
|
||||||
const user = await getCurrentUser()
|
const user = await getCurrentUser()
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(authOptions.pages?.signIn || "/new")
|
return redirect(authOptions.pages?.signIn || "/new")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper signedIn>
|
||||||
<Header signedIn />
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -32,6 +32,6 @@ export default async function SettingsPage() {
|
||||||
<Password />
|
<Password />
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
@import "./syntax.css";
|
@import "./syntax.css";
|
||||||
@import "./markdown.css";
|
|
||||||
@import "./inter.css";
|
@import "./inter.css";
|
||||||
|
|
||||||
:root {
|
: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 { marked } from "marked"
|
||||||
import Highlight, { defaultProps, Language } from "prism-react-renderer"
|
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
import Highlight, { defaultProps, Language } from "prism-react-renderer"
|
||||||
// // image sizes. DDoS Safe?
|
// // image sizes. DDoS Safe?
|
||||||
// const imageSizeLink = /^!?\[((?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?)\]\(\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?(?:\s+=(?:[\w%]+)?x(?:[\w%]+)?)?)(?:\s+("(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/;
|
// const imageSizeLink = /^!?\[((?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?)\]\(\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?(?:\s+=(?:[\w%]+)?x(?:[\w%]+)?)?)(?:\s+("(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/;
|
||||||
// //@ts-ignore
|
// //@ts-ignore
|
||||||
|
@ -31,25 +30,30 @@ const renderer = new marked.Renderer()
|
||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
|
||||||
renderer.link = (href, _, text) => {
|
// renderer.link = (href, _, text) => {
|
||||||
const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
|
// const isHrefLocal = href?.startsWith("/") || href?.startsWith("#")
|
||||||
if (isHrefLocal) {
|
// if (isHrefLocal) {
|
||||||
return <a href={href || ''}>
|
// return <a href={href || ""}>{text}</a>
|
||||||
{text}
|
// }
|
||||||
</a>
|
|
||||||
}
|
|
||||||
|
|
||||||
// dirty hack
|
// // dirty hack
|
||||||
// if text contains elements, render as html
|
// // if text contains elements, render as html
|
||||||
return <a href={href || ""} target="_blank" rel="noopener noreferrer" dangerouslySetInnerHTML={{ __html: convertHtmlEntities(text) }} ></a>
|
// return (
|
||||||
}
|
// <a
|
||||||
|
// href={href || ""}
|
||||||
|
// target="_blank"
|
||||||
|
// rel="noopener noreferrer"
|
||||||
|
// dangerouslySetInnerHTML={{ __html: convertHtmlEntities(text) }}
|
||||||
|
// ></a>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
renderer.image = function (href, _, text) {
|
renderer.image = function (href, _, text) {
|
||||||
return <Image loading="lazy" src="${href}" alt="${text}" layout="fill" />
|
return <Image loading="lazy" src="${href}" alt="${text}" layout="fill" />
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.checkbox = () => ""
|
// renderer.checkbox = () => ""
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
renderer.listitem = (text, task, checked) => {
|
renderer.listitem = (text, task, checked) => {
|
||||||
if (task) {
|
if (task) {
|
||||||
|
@ -63,24 +67,24 @@ renderer.listitem = (text, task, checked) => {
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <li>{text}</li>
|
return <li>{text}</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
// //@ts-ignore
|
// @ts-ignore
|
||||||
// renderer.code = (code: string, language: string) => {
|
renderer.code = (code: string, language: string) => {
|
||||||
// return (<pre>
|
const component =
|
||||||
// {/* {title && <code>{title} </code>} */}
|
<pre>
|
||||||
// {/* {language && title && <code style={{}}> {language} </code>} */}
|
<Code
|
||||||
// <Code
|
language={language}
|
||||||
// language={language}
|
// title={title}
|
||||||
// // title={title}
|
code={code}
|
||||||
// code={code}
|
// highlight={highlight}
|
||||||
// // highlight={highlight}
|
/>
|
||||||
// />
|
)
|
||||||
// </pre>
|
</pre>
|
||||||
// )
|
|
||||||
// }
|
return component
|
||||||
|
}
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
gfm: true,
|
gfm: true,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||||
import { NextAuthOptions } from "next-auth"
|
import { NextAuthOptions } from "next-auth"
|
||||||
import GitHubProvider from "next-auth/providers/github"
|
import GitHubProvider from "next-auth/providers/github"
|
||||||
import prisma from "@lib/server/prisma"
|
import { prisma } from "@lib/server/prisma"
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
|
|
||||||
const providers: NextAuthOptions["providers"] = [
|
const providers: NextAuthOptions["providers"] = [
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import markdown from "../render-markdown"
|
|
||||||
import type { File } from "@lib/server/prisma"
|
import type { File } from "@lib/server/prisma"
|
||||||
|
import markdown from "@wcj/markdown-to-html"
|
||||||
/**
|
/**
|
||||||
* returns rendered HTML from a Drift file
|
* returns rendered HTML from a Drift file
|
||||||
*/
|
*/
|
||||||
export function getHtmlFromFile({
|
export async function getHtmlFromFile({
|
||||||
content,
|
content,
|
||||||
title
|
title
|
||||||
}: Pick<File, "content" | "title">) {
|
}: Pick<File, "content" | "title">) {
|
||||||
|
@ -39,6 +39,7 @@ ${content}
|
||||||
contentToRender = "\n" + content
|
contentToRender = "\n" + content
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = markdown(contentToRender)
|
const html = markdown(contentToRender, {
|
||||||
|
})
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import { User } from "@prisma/client"
|
import { User } from "@prisma/client"
|
||||||
import prisma from "@lib/server/prisma"
|
import { prisma } from "@lib/server/prisma"
|
||||||
import * as jwt from "jsonwebtoken"
|
import * as jwt from "jsonwebtoken"
|
||||||
import next, { NextApiHandler, NextApiRequest, NextApiResponse } from "next"
|
import next, { NextApiHandler, NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,6 @@ declare global {
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import { Post, PrismaClient, File, User } from "@prisma/client"
|
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
|
// 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
|
// 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
|
// 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
|
if (process.env.NODE_ENV !== "production") global.prisma = prisma
|
||||||
const client = globalThis.prisma || prisma
|
|
||||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
|
||||||
|
|
||||||
export type { User, AuthTokens, File, Post } from "@prisma/client"
|
export type { User, File, Post } from "@prisma/client"
|
||||||
|
|
||||||
export type PostWithFiles = Post & {
|
export type PostWithFiles = Post & {
|
||||||
files: File[]
|
files: File[]
|
||||||
|
@ -138,7 +134,6 @@ export const createUser = async (
|
||||||
throw new Error("Wrong registration password")
|
throw new Error("Wrong registration password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const isUserAdminByDefault =
|
const isUserAdminByDefault =
|
||||||
config.enable_admin && (await prisma.user.count()) === 0
|
config.enable_admin && (await prisma.user.count()) === 0
|
||||||
const userRole = isUserAdminByDefault ? "admin" : "user"
|
const userRole = isUserAdminByDefault ? "admin" : "user"
|
||||||
|
@ -150,7 +145,6 @@ export const createUser = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPostById = async (postId: Post["id"], withFiles = false) => {
|
export const getPostById = async (postId: Post["id"], withFiles = false) => {
|
||||||
console.log("getPostById", postId)
|
|
||||||
const post = await prisma.post.findUnique({
|
const post = await prisma.post.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: postId
|
id: postId
|
||||||
|
|
|
@ -14,8 +14,12 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@geist-ui/core": "^2.3.8",
|
"@geist-ui/core": "^2.3.8",
|
||||||
"@geist-ui/icons": "1.0.2",
|
"@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",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@prisma/client": "^4.6.1",
|
"@prisma/client": "^4.6.1",
|
||||||
|
"@wcj/markdown-to-html": "^2.1.2",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"client-zip": "2.2.1",
|
"client-zip": "2.2.1",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
@ -26,6 +30,7 @@
|
||||||
"next": "13.0.3-canary.4",
|
"next": "13.0.3-canary.4",
|
||||||
"next-auth": "^4.16.4",
|
"next-auth": "^4.16.4",
|
||||||
"next-joi": "^2.2.1",
|
"next-joi": "^2.2.1",
|
||||||
|
"next-mdx-remote": "^4.2.0",
|
||||||
"next-themes": "npm:@wits/next-themes@0.2.7",
|
"next-themes": "npm:@wits/next-themes@0.2.7",
|
||||||
"prism-react-renderer": "^1.3.5",
|
"prism-react-renderer": "^1.3.5",
|
||||||
"rc-table": "7.24.1",
|
"rc-table": "7.24.1",
|
||||||
|
@ -35,10 +40,19 @@
|
||||||
"react-dropzone": "14.2.3",
|
"react-dropzone": "14.2.3",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-loading-skeleton": "3.1.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",
|
"server-only": "^0.0.1",
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"swr": "1.3.0",
|
"swr": "1.3.0",
|
||||||
"textarea-markdown-editor": "0.1.13",
|
"textarea-markdown-editor": "0.1.13",
|
||||||
|
"unified": "^10.1.2",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -54,6 +68,7 @@
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.27.0",
|
"eslint": "8.27.0",
|
||||||
"eslint-config-next": "13.0.2",
|
"eslint-config-next": "13.0.2",
|
||||||
|
"katex": "^0.16.3",
|
||||||
"next-unused": "0.0.6",
|
"next-unused": "0.0.6",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"prisma": "^4.6.1",
|
"prisma": "^4.6.1",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { withMethods } from "@lib/api-middleware/with-methods"
|
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||||
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
|
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
|
||||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
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"
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
export default withMethods(
|
export default withMethods(
|
||||||
|
@ -10,7 +10,7 @@ export default withMethods(
|
||||||
const query = req.query
|
const query = req.query
|
||||||
const fileId = parseQueryParam(query.fileId)
|
const fileId = parseQueryParam(query.fileId)
|
||||||
const content = parseQueryParam(query.content)
|
const content = parseQueryParam(query.content)
|
||||||
const title = parseQueryParam(query.title)
|
const title = parseQueryParam(query.title) || "Untitled"
|
||||||
|
|
||||||
if (fileId && (content || title)) {
|
if (fileId && (content || title)) {
|
||||||
return res.status(400).json({ error: "Too many arguments" })
|
return res.status(400).json({ error: "Too many arguments" })
|
||||||
|
@ -33,7 +33,7 @@ export default withMethods(
|
||||||
return res.status(400).json({ error: "Missing arguments" })
|
return res.status(400).json({ error: "Missing arguments" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderedHTML = getHtmlFromFile({
|
const renderedHTML = await getHtmlFromFile({
|
||||||
title,
|
title,
|
||||||
content
|
content
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
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"
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
|
|
||||||
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
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) {
|
if (!file) {
|
||||||
return res.status(404).end()
|
return res.status(404).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader("Content-Type", "text/plain")
|
res.setHeader("Content-Type", "text/plain")
|
||||||
res.setHeader("Cache-Control", "public, max-age=4800")
|
res.setHeader("Cache-Control", "public, max-age=4800")
|
||||||
console.log(file.html)
|
res.status(200).write(file.html)
|
||||||
return res.status(200).write(file.html)
|
res.end()
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
export default getRawFile
|
export default getRawFile
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
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"
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
|
|
||||||
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
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 { withMethods } from "@lib/api-middleware/with-methods"
|
||||||
|
|
||||||
import { authOptions } from "@lib/server/auth"
|
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 { NextApiRequest, NextApiResponse } from "next"
|
||||||
import { unstable_getServerSession } from "next-auth/next"
|
import { unstable_getServerSession } from "next-auth/next"
|
||||||
import { File } from "@lib/server/prisma"
|
import { File } from "@lib/server/prisma"
|
||||||
|
@ -13,18 +13,18 @@ import { getSession } from "next-auth/react"
|
||||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (req.method === "POST") {
|
return await handlePost(req, res)
|
||||||
return await handlePost(req, res)
|
|
||||||
} else {
|
|
||||||
return await handleGet(req, res)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withMethods(["POST", "GET"], handler)
|
export default withMethods(["POST"], handler)
|
||||||
|
|
||||||
async function handlePost(req: NextApiRequest, res: NextApiResponse<any>) {
|
async function handlePost(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
try {
|
try {
|
||||||
const session = await unstable_getServerSession(req, res, authOptions)
|
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 files = req.body.files as File[]
|
||||||
const fileTitles = files.map((file) => file.title)
|
const fileTitles = files.map((file) => file.title)
|
||||||
|
@ -45,23 +45,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
.digest("hex")
|
.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({
|
const post = await prisma.post.create({
|
||||||
data: {
|
data: {
|
||||||
title: req.body.title,
|
title: req.body.title,
|
||||||
|
@ -69,62 +52,51 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
visibility: req.body.visibility,
|
visibility: req.body.visibility,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
expiresAt: req.body.expiresAt,
|
expiresAt: req.body.expiresAt,
|
||||||
|
parentId: req.body.parentId,
|
||||||
// authorId: session?.user.id,
|
// authorId: session?.user.id,
|
||||||
author: {
|
author: {
|
||||||
connect: {
|
connect: {
|
||||||
id: session?.user.id
|
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)
|
return res.json(post)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(500).json(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 { 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 { getCookie } from "cookies-next"
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
@ -37,17 +37,20 @@ export default async function handler(
|
||||||
message: "Unauthorized"
|
message: "Unauthorized"
|
||||||
})
|
})
|
||||||
|
|
||||||
const userId = String(getCookie(USER_COOKIE_NAME, {
|
const userId = String(
|
||||||
req, res
|
getCookie(USER_COOKIE_NAME, {
|
||||||
}))
|
req,
|
||||||
|
res
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return error()
|
return error()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await getUserById(userId);
|
const user = await getUserById(userId)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return error()
|
return error()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,31 @@
|
||||||
// a nextjs api handerl
|
// a nextjs api handerl
|
||||||
|
|
||||||
import config from "@lib/config"
|
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"
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
export const getWelcomeContent = async () => {
|
export const getWelcomeContent = async () => {
|
||||||
const introContent = config.welcome_content
|
const introContent = config.welcome_content
|
||||||
const introTitle = config.welcome_title
|
const introTitle = config.welcome_title
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: introTitle,
|
title: introTitle,
|
||||||
content: introContent,
|
content: introContent,
|
||||||
rendered: renderMarkdown(introContent)
|
rendered: await getHtmlFromFile({
|
||||||
|
title: `intro.md`,
|
||||||
|
content: introContent
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(_: NextApiRequest, res: NextApiResponse) {
|
||||||
_: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
const welcomeContent = await getWelcomeContent()
|
const welcomeContent = await getWelcomeContent()
|
||||||
if (!welcomeContent) {
|
if (!welcomeContent) {
|
||||||
return res.status(500).json({ error: "Missing welcome content" })
|
return res.status(500).json({ error: "Missing welcome content" })
|
||||||
}
|
}
|
||||||
console.log(welcomeContent.title)
|
|
||||||
|
|
||||||
return res.json(welcomeContent)
|
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