convert admin, run lint
This commit is contained in:
parent
cf7d89eb20
commit
8b0b172f7d
64 changed files with 410 additions and 581 deletions
|
@ -1,5 +1,5 @@
|
|||
import PageSeo from "@components/head"
|
||||
import PageSeo from "@components/page-seo"
|
||||
|
||||
export default function AuthHead() {
|
||||
return <PageSeo title="Sign In" />
|
||||
return <PageSeo title="Sign In" />
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
"use client"
|
||||
import ShiftBy from "@components/shift-by"
|
||||
import { Spacer, Tabs, Card, Textarea, Text } from "@geist-ui/core/dist"
|
||||
import Image from "next/image"
|
|
@ -1,4 +1,5 @@
|
|||
import { getWelcomeContent } from "pages/api/welcome"
|
||||
import Home from "./home"
|
||||
|
||||
const getWelcomeData = async () => {
|
||||
const welcomeContent = await getWelcomeContent()
|
||||
|
@ -6,7 +7,7 @@ const getWelcomeData = async () => {
|
|||
}
|
||||
|
||||
export default async function Page() {
|
||||
const welcomeData = await getWelcomeData()
|
||||
|
||||
return <h1>{JSON.stringify(welcomeData)}</h1>
|
||||
const { content, rendered, title } = await getWelcomeData()
|
||||
|
||||
return <Home rendered={rendered} introContent={content} introTitle={title} />
|
||||
}
|
11
client/app/(posts)/expired/page.tsx
Normal file
11
client/app/(posts)/expired/page.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
"use client"
|
||||
|
||||
import { Note, Text } from "@geist-ui/core/dist"
|
||||
|
||||
export default function ExpiredPage() {
|
||||
return (
|
||||
<Note type="error" label={false}>
|
||||
<Text h4>Error: The Drift you're trying to view has expired.</Text>
|
||||
</Note>
|
||||
)
|
||||
}
|
|
@ -4,6 +4,7 @@ import { cookies } from "next/headers"
|
|||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import { getPostWithFiles } from "app/prisma"
|
||||
import { useRedirectIfNotAuthed } from "@lib/server/hooks/use-redirect-if-not-authed"
|
||||
|
||||
const NewFromExisting = async ({
|
||||
params
|
||||
}: {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default function NewLayout({ children }: { children: React.ReactNode }) {
|
||||
// useRedirectIfNotAuthed()
|
||||
return <>{children}</>;
|
||||
// useRedirectIfNotAuthed()
|
||||
return <>{children}</>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import NewPost from "@components/new-post"
|
||||
import '@styles/react-datepicker.css'
|
||||
import "@styles/react-datepicker.css"
|
||||
|
||||
const New = () => <NewPost />
|
||||
|
||||
|
|
5
client/app/(profiles)/mine/head.tsx
Normal file
5
client/app/(profiles)/mine/head.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import PageSeo from "@components/page-seo"
|
||||
|
||||
export default function Head() {
|
||||
return <PageSeo title="Drift - Your profile" isPrivate />
|
||||
}
|
18
client/app/(profiles)/mine/page.tsx
Normal file
18
client/app/(profiles)/mine/page.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { USER_COOKIE_NAME } from "@lib/constants"
|
||||
import { notFound, useRouter } from "next/navigation"
|
||||
import { cookies } from "next/headers"
|
||||
import { getPostsByUser } from "app/prisma"
|
||||
import PostList from "@components/post-list"
|
||||
export default async function Mine() {
|
||||
// TODO: fix router usage
|
||||
// const router = useRouter()
|
||||
const userId = cookies().get(USER_COOKIE_NAME)?.value
|
||||
if (!userId) {
|
||||
// return router.push("/signin")
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const posts = await getPostsByUser(userId, true)
|
||||
const hasMore = false
|
||||
return <PostList morePosts={hasMore} initialPosts={posts} />
|
||||
}
|
5
client/app/(profiles)/settings/head.tsx
Normal file
5
client/app/(profiles)/settings/head.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import PageSeo from "@components/page-seo"
|
||||
|
||||
export default function Head() {
|
||||
return <PageSeo title="Drift - Settings" isPrivate />
|
||||
}
|
5
client/app/(profiles)/settings/page.tsx
Normal file
5
client/app/(profiles)/settings/page.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import SettingsPage from "@components/settings"
|
||||
|
||||
const Settings = () => <SettingsPage />
|
||||
|
||||
export default Settings
|
22
client/app/admin/page.tsx
Normal file
22
client/app/admin/page.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Admin from "@components/admin"
|
||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import { isUserAdmin } from "app/prisma"
|
||||
import { cookies } from "next/headers"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
const AdminPage = async () => {
|
||||
const driftToken = cookies().get(TOKEN_COOKIE_NAME)?.value
|
||||
if (!driftToken) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const isAdmin = await isUserAdmin(driftToken)
|
||||
|
||||
if (!isAdmin) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return <Admin />
|
||||
}
|
||||
|
||||
export default AdminPage
|
|
@ -49,7 +49,9 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
|||
<meta name="theme-color" content="#ffffff" />
|
||||
<title>Drift</title>
|
||||
</head>
|
||||
<body><LayoutWrapper>{children}</LayoutWrapper></body>
|
||||
<body>
|
||||
<LayoutWrapper>{children}</LayoutWrapper>
|
||||
</body>
|
||||
</html>
|
||||
</ServerThemeProvider>
|
||||
)
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import styles from "@styles/Home.module.css"
|
||||
|
||||
import MyPosts from "@components/my-posts"
|
||||
import type { GetServerSideProps } from "next"
|
||||
import { Post } from "@lib/types"
|
||||
import { Page } from "@geist-ui/core/dist"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { cookies } from "next/headers"
|
||||
export default function Mine() {
|
||||
const router = useRouter()
|
||||
const driftToken = cookies().get(TOKEN_COOKIE_NAME)
|
||||
if (!driftToken) {
|
||||
return router.push("/signin")
|
||||
}
|
||||
|
||||
// const posts = await fetch(process.env.API_URL + `/posts/mine`, {
|
||||
// method: "GET",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// Authorization: `Bearer ${driftToken}`,
|
||||
// "x-secret-key": process.env.SECRET_KEY || ""
|
||||
// }
|
||||
// })
|
||||
|
||||
if (!posts.ok) {
|
||||
return router.push("/signin")
|
||||
}
|
||||
|
||||
const { posts, error, hasMore } = await posts.json()
|
||||
|
||||
return <MyPosts morePosts={hasMore} error={error} posts={posts} />
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import Header from "@components/header"
|
||||
import { Page } from "@geist-ui/core/dist"
|
||||
import styles from "@styles/Home.module.css"
|
||||
|
||||
export default function PageWrapper({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Page width={"100%"}>
|
||||
<Page.Header>
|
||||
<Header />
|
||||
</Page.Header>
|
||||
|
||||
<Page.Content className={styles.main}>{children}</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
}
|
|
@ -1,16 +1,10 @@
|
|||
import { Post, PrismaClient, File } from "@prisma/client"
|
||||
import { Post, PrismaClient, File, User } from "@prisma/client"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export default prisma
|
||||
|
||||
export type {
|
||||
User,
|
||||
AuthTokens,
|
||||
File,
|
||||
Post,
|
||||
PostToAuthors
|
||||
} from "@prisma/client"
|
||||
export type { User, AuthTokens, File, Post } from "@prisma/client"
|
||||
|
||||
export type PostWithFiles = Post & {
|
||||
files: File[]
|
||||
|
@ -50,3 +44,74 @@ export const getPostWithFiles = async (
|
|||
files
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPostsByUser(userId: string): Promise<Post[]>
|
||||
export async function getPostsByUser(
|
||||
userId: string,
|
||||
includeFiles: true
|
||||
): Promise<PostWithFiles[]>
|
||||
export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
|
||||
const sharedOptions = {
|
||||
take: 20,
|
||||
orderBy: {
|
||||
createdAt: "desc" as const
|
||||
}
|
||||
}
|
||||
|
||||
const posts = await prisma.post.findMany({
|
||||
where: {
|
||||
authorId: userId
|
||||
},
|
||||
...sharedOptions
|
||||
})
|
||||
|
||||
if (withFiles) {
|
||||
return Promise.all(
|
||||
posts.map(async (post) => {
|
||||
const files = await prisma.file.findMany({
|
||||
where: {
|
||||
postId: post.id
|
||||
},
|
||||
...sharedOptions
|
||||
})
|
||||
|
||||
return {
|
||||
...post,
|
||||
files
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
export const getUserById = async (userId: User["id"]) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
displayName: true,
|
||||
role: true,
|
||||
username: true
|
||||
}
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
export const isUserAdmin = async (userId: User["id"]) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId
|
||||
},
|
||||
select: {
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
return user?.role?.toLowerCase() === "admin"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
"use client"
|
||||
|
||||
import { CssBaseline, GeistProvider, Themes } from "@geist-ui/core/dist"
|
||||
import Header from "@components/header"
|
||||
import { CssBaseline, GeistProvider, Page, Themes } from "@geist-ui/core/dist"
|
||||
import { ThemeProvider } from "next-themes"
|
||||
import { SkeletonTheme } from "react-loading-skeleton"
|
||||
import PageWrapper from "./page-wrapper"
|
||||
import styles from "@styles/Home.module.css"
|
||||
|
||||
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||
const skeletonBaseColor = "var(--light-gray)"
|
||||
|
@ -58,7 +59,13 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
|||
attribute="data-theme"
|
||||
>
|
||||
<CssBaseline />
|
||||
<PageWrapper>{children}</PageWrapper>
|
||||
<Page width={"100%"}>
|
||||
<Page.Header>
|
||||
<Header />
|
||||
</Page.Header>
|
||||
|
||||
<Page.Content className={styles.main}>{children}</Page.Content>
|
||||
</Page>
|
||||
</ThemeProvider>
|
||||
</SkeletonTheme>
|
||||
</GeistProvider>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client"
|
||||
|
||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import { getCookie } from "cookies-next"
|
||||
import styles from "./admin.module.css"
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
"use client"
|
||||
|
||||
import { FormEvent, useEffect, useState } from "react"
|
||||
import { FormEvent, useState } from "react"
|
||||
import styles from "./auth.module.css"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "../link"
|
||||
import Cookies from "js-cookie"
|
||||
import useSignedIn from "@lib/hooks/use-signed-in"
|
||||
import Input from "@components/input"
|
||||
import Button from "@components/button"
|
||||
import Note from "@components/note"
|
||||
import { USER_COOKIE_NAME } from "@lib/constants"
|
||||
import { setCookie } from "cookies-next"
|
||||
import { Button, Input, Note } from "@geist-ui/core/dist"
|
||||
|
||||
const NO_EMPTY_SPACE_REGEX = /^\S*$/
|
||||
const ERROR_MESSAGE =
|
||||
|
@ -83,24 +80,26 @@ const Auth = ({
|
|||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.formGroup}>
|
||||
<Input
|
||||
type="text"
|
||||
htmlType="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.currentTarget.value)}
|
||||
placeholder="Username"
|
||||
required
|
||||
minLength={3}
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
htmlType="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.currentTarget.value)}
|
||||
placeholder="Password"
|
||||
required
|
||||
minLength={6}
|
||||
/>
|
||||
{requiresServerPassword && (
|
||||
<Input
|
||||
type="password"
|
||||
htmlType="password"
|
||||
id="server-password"
|
||||
value={serverPassword}
|
||||
onChange={(event) =>
|
||||
|
@ -108,10 +107,11 @@ const Auth = ({
|
|||
}
|
||||
placeholder="Server Password"
|
||||
required
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button buttonType="primary" type="submit">
|
||||
<Button width={"100%"} htmlType="submit">
|
||||
{signingIn ? "Sign In" : "Sign Up"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import Head from "next/head"
|
||||
import React from "react"
|
||||
|
||||
type PageSeoProps = {
|
||||
title?: string
|
||||
description?: string
|
||||
isLoading?: boolean
|
||||
isPrivate?: boolean
|
||||
}
|
||||
|
||||
const PageSeo = ({
|
||||
title = "Drift",
|
||||
description = "A self-hostable clone of GitHub Gist",
|
||||
isPrivate = false
|
||||
}: PageSeoProps) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
{!isPrivate && <meta name="description" content={description} />}
|
||||
</Head>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageSeo
|
|
@ -3,7 +3,7 @@ import NextLink from "next/link"
|
|||
import styles from "./link.module.css"
|
||||
|
||||
type LinkProps = {
|
||||
colored?: boolean,
|
||||
colored?: boolean
|
||||
children: React.ReactNode
|
||||
} & React.ComponentProps<typeof NextLink>
|
||||
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import type { Post } from "@lib/types"
|
||||
import PostList from "../post-list"
|
||||
|
||||
const MyPosts = ({
|
||||
posts,
|
||||
error,
|
||||
morePosts
|
||||
}: {
|
||||
posts: Post[]
|
||||
error: boolean
|
||||
morePosts: boolean
|
||||
}) => {
|
||||
return <PostList morePosts={morePosts} initialPosts={posts} error={error} />
|
||||
}
|
||||
|
||||
export default MyPosts
|
|
@ -16,6 +16,7 @@ const PageSeo = ({
|
|||
<>
|
||||
<title>Drift - {title}</title>
|
||||
{!isPrivate && <meta name="description" content={description} />}
|
||||
{isPrivate && <meta name="robots" content="noindex" />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
"use client"
|
||||
|
||||
import { Button, Input, Text } from "@geist-ui/core/dist"
|
||||
|
||||
import styles from "./post-list.module.css"
|
||||
import ListItemSkeleton from "./list-item-skeleton"
|
||||
import ListItem from "./list-item"
|
||||
import { Post } from "@lib/types"
|
||||
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from "react"
|
||||
import useDebounce from "@lib/hooks/use-debounce"
|
||||
import Link from "@components/link"
|
||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import { getCookie } from "cookies-next"
|
||||
import type { PostWithFiles } from "app/prisma"
|
||||
|
||||
type Props = {
|
||||
initialPosts: Post[]
|
||||
error: boolean
|
||||
initialPosts: PostWithFiles[]
|
||||
morePosts: boolean
|
||||
}
|
||||
|
||||
const PostList = ({ morePosts, initialPosts, error }: Props) => {
|
||||
const PostList = ({ morePosts, initialPosts }: Props) => {
|
||||
const [search, setSearchValue] = useState("")
|
||||
const [posts, setPosts] = useState<Post[]>(initialPosts)
|
||||
const [posts, setPosts] = useState(initialPosts)
|
||||
const [searching, setSearching] = useState(false)
|
||||
const [hasMorePosts, setHasMorePosts] = useState(morePosts)
|
||||
|
||||
|
@ -122,7 +123,7 @@ const PostList = ({ morePosts, initialPosts, error }: Props) => {
|
|||
onChange={handleSearchChange}
|
||||
/>
|
||||
</div>
|
||||
{error && <Text type="error">Failed to load.</Text>}
|
||||
{!posts && <Text type="error">Failed to load.</Text>}
|
||||
{!posts.length && searching && (
|
||||
<ul>
|
||||
<li>
|
||||
|
@ -133,7 +134,7 @@ const PostList = ({ morePosts, initialPosts, error }: Props) => {
|
|||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{posts?.length === 0 && !error && (
|
||||
{posts?.length === 0 && posts && (
|
||||
<Text type="secondary">
|
||||
No posts found. Create one{" "}
|
||||
<Link colored href="/new">
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import NextLink from "next/link"
|
||||
import VisibilityBadge from "../badges/visibility-badge"
|
||||
import { Text, Card, Tooltip, Divider, Badge, Button } from "@geist-ui/core/dist"
|
||||
import { File, Post } from "@lib/types"
|
||||
import {
|
||||
Text,
|
||||
Card,
|
||||
Tooltip,
|
||||
Divider,
|
||||
Badge,
|
||||
Button
|
||||
} from "@geist-ui/core/dist"
|
||||
import FadeIn from "@components/fade-in"
|
||||
import Trash from "@geist-ui/icons/trash"
|
||||
import ExpirationBadge from "@components/badges/expiration-badge"
|
||||
|
@ -11,6 +17,8 @@ import { useRouter } from "next/router"
|
|||
import Parent from "@geist-ui/icons/arrowUpCircle"
|
||||
import styles from "./list-item.module.css"
|
||||
import Link from "@components/link"
|
||||
import { PostWithFiles, File } from "app/prisma"
|
||||
import { PostVisibility } from "@lib/types"
|
||||
|
||||
// TODO: isOwner should default to false so this can be used generically
|
||||
const ListItem = ({
|
||||
|
@ -18,7 +26,7 @@ const ListItem = ({
|
|||
isOwner = true,
|
||||
deletePost
|
||||
}: {
|
||||
post: Post
|
||||
post: PostWithFiles
|
||||
isOwner?: boolean
|
||||
deletePost: () => void
|
||||
}) => {
|
||||
|
@ -29,7 +37,7 @@ const ListItem = ({
|
|||
}
|
||||
|
||||
const viewParentClick = () => {
|
||||
router.push(`/post/${post.parent?.id}`)
|
||||
router.push(`/post/${post.parentId}`)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -48,7 +56,7 @@ const ListItem = ({
|
|||
</Link>
|
||||
{isOwner && (
|
||||
<span className={styles.buttons}>
|
||||
{post.parent && (
|
||||
{post.parentId && (
|
||||
<Tooltip text={"View parent"} hideArrow>
|
||||
<Button
|
||||
auto
|
||||
|
@ -74,7 +82,7 @@ const ListItem = ({
|
|||
)}
|
||||
|
||||
<div className={styles.badges}>
|
||||
<VisibilityBadge visibility={post.visibility} />
|
||||
<VisibilityBadge visibility={post.visibility as PostVisibility} />
|
||||
<CreatedAgoBadge createdAt={post.createdAt} />
|
||||
<Badge type="secondary">
|
||||
{post.files?.length === 1
|
||||
|
|
|
@ -5,7 +5,13 @@ import styles from "./post-page.module.css"
|
|||
import homeStyles from "@styles/Home.module.css"
|
||||
|
||||
import type { File, Post, PostVisibility } from "@lib/types"
|
||||
import { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core/dist"
|
||||
import {
|
||||
Page,
|
||||
Button,
|
||||
Text,
|
||||
ButtonGroup,
|
||||
useMediaQuery
|
||||
} from "@geist-ui/core/dist"
|
||||
import { useEffect, useState } from "react"
|
||||
import Archive from "@geist-ui/icons/archive"
|
||||
import Edit from "@geist-ui/icons/edit"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import { getCookie } from "cookies-next"
|
||||
import Cookies from "js-cookie"
|
||||
import { memo, useEffect, useState } from "react"
|
||||
import styles from "./preview.module.css"
|
||||
|
||||
|
@ -36,9 +36,8 @@ const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
|
|||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${getCookie("drift-token")}`
|
||||
},
|
||||
|
||||
Authorization: `Bearer ${getCookie(TOKEN_COOKIE_NAME)}`
|
||||
}
|
||||
})
|
||||
|
||||
if (resp.ok) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client"
|
||||
|
||||
import Password from "./sections/password"
|
||||
import Profile from "./sections/profile"
|
||||
import SettingsGroup from "../settings-group"
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
"use client"
|
||||
|
||||
import { Input, Button, useToasts } from "@geist-ui/core/dist"
|
||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import { getCookie } from "cookies-next"
|
||||
import Cookies from "js-cookie"
|
||||
import { useState } from "react"
|
||||
|
||||
const Password = () => {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
"use client"
|
||||
|
||||
import { Note, Input, Textarea, Button, useToasts } from "@geist-ui/core/dist"
|
||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import useUserData from "@lib/hooks/use-user-data"
|
||||
import { getCookie } from "cookies-next"
|
||||
import Cookies from "js-cookie"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
const Profile = () => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import styles from "./document.module.css"
|
|||
import Download from "@geist-ui/icons/download"
|
||||
import ExternalLink from "@geist-ui/icons/externalLink"
|
||||
import Skeleton from "react-loading-skeleton"
|
||||
import Link from 'next/link';
|
||||
import Link from "next/link"
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
|
@ -80,10 +80,9 @@ export const config = (env: Environment): Config => {
|
|||
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
||||
welcome_content: env.WELCOME_CONTENT ?? "",
|
||||
welcome_title: env.WELCOME_TITLE ?? "",
|
||||
url: 'http://localhost:3000'
|
||||
url: "http://localhost:3000"
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
export default config(process.env)
|
||||
|
|
|
@ -16,7 +16,7 @@ export default function generateUUID() {
|
|||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
|
||||
).toString(16)
|
||||
}
|
||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, callback);
|
||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, callback)
|
||||
}
|
||||
}
|
||||
let timestamp = new Date().getTime()
|
||||
|
@ -35,5 +35,5 @@ export default function generateUUID() {
|
|||
perforNow = Math.floor(perforNow / 16)
|
||||
}
|
||||
return (c === "x" ? random : (random & 0x3) | 0x8).toString(16)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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("drift-token")
|
||||
const token = getCookie(TOKEN_COOKIE_NAME)
|
||||
|
||||
const [signedIn, setSignedIn] = useSharedState(
|
||||
"signedIn",
|
||||
|
@ -13,7 +14,7 @@ const useSignedIn = () => {
|
|||
const signin = (token: string) => {
|
||||
setSignedIn(true)
|
||||
// TODO: investigate SameSite / CORS cookie security
|
||||
setCookie("drift-token", token)
|
||||
setCookie(TOKEN_COOKIE_NAME, token)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -24,6 +25,8 @@ const useSignedIn = () => {
|
|||
}
|
||||
}, [setSignedIn, token])
|
||||
|
||||
console.log("signed in", signedIn)
|
||||
|
||||
return { signedIn, signin, token }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||
import { User } from "@lib/types"
|
||||
import { deleteCookie, getCookie } from "cookies-next"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
const useUserData = () => {
|
||||
const cookie = getCookie("drift-token")
|
||||
const cookie = getCookie(TOKEN_COOKIE_NAME)
|
||||
const [authToken, setAuthToken] = useState<string>(
|
||||
cookie ? String(cookie) : ""
|
||||
)
|
||||
const [user, setUser] = useState<User>()
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
const token = getCookie("drift-token")
|
||||
const token = getCookie(TOKEN_COOKIE_NAME)
|
||||
if (token) {
|
||||
setAuthToken(String(token))
|
||||
}
|
||||
|
@ -29,9 +30,10 @@ const useUserData = () => {
|
|||
const user = await response.json()
|
||||
setUser(user)
|
||||
} else {
|
||||
deleteCookie("drift-token")
|
||||
setAuthToken("")
|
||||
router.push("/")
|
||||
// deleteCookie("drift-token")
|
||||
// setAuthToken("")
|
||||
// router.push("/")
|
||||
console.log("not ok")
|
||||
}
|
||||
}
|
||||
fetchUser()
|
||||
|
|
|
@ -20,21 +20,18 @@ import Link from "next/link"
|
|||
const renderer = new marked.Renderer()
|
||||
|
||||
// @ts-ignore
|
||||
renderer.heading = (text, level, _, slugger) => {
|
||||
const id = slugger.slug(text)
|
||||
const Component = `h${level}`
|
||||
// renderer.heading = (text, level, raw, slugger) => {
|
||||
// const id = slugger.slug(text)
|
||||
// const Component = `h${level}`
|
||||
|
||||
return (
|
||||
<h1>
|
||||
<Link
|
||||
href={`#${id}`}
|
||||
id={id}
|
||||
style={{ color: "inherit" }}
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
></Link>
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
// return (
|
||||
// <Component>
|
||||
// <Link href={`#${id}`} id={id}>
|
||||
// {text}
|
||||
// </Link>
|
||||
// </Component>
|
||||
// )
|
||||
// }
|
||||
|
||||
// renderer.link = (href, _, text) => {
|
||||
// const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
|
||||
|
|
|
@ -9,11 +9,11 @@ export async function generateAndExpireAccessToken(userId: User["id"]) {
|
|||
await prisma.authTokens.create({
|
||||
data: {
|
||||
userId: userId,
|
||||
token: token,
|
||||
token: token
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: set expiredReason?
|
||||
// TODO: set expiredReason?
|
||||
prisma.authTokens.deleteMany({
|
||||
where: {
|
||||
userId: userId,
|
||||
|
|
|
@ -3,7 +3,10 @@ import type { File } from "app/prisma"
|
|||
/**
|
||||
* returns rendered HTML from a Drift file
|
||||
*/
|
||||
function getHtmlFromFile({ content, title }: Pick<File, "content" | "title">) {
|
||||
export function getHtmlFromFile({
|
||||
content,
|
||||
title
|
||||
}: Pick<File, "content" | "title">) {
|
||||
const renderAsMarkdown = [
|
||||
"markdown",
|
||||
"md",
|
||||
|
@ -35,5 +38,3 @@ ${content}
|
|||
const html = markdown(contentToRender)
|
||||
return html
|
||||
}
|
||||
|
||||
export default getHtmlFromFile
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { useRouter } from 'next/navigation'
|
||||
import { useRouter } from "next/navigation"
|
||||
import { isSignedIn } from "../is-signed-in"
|
||||
|
||||
export const useRedirectIfNotAuthed = (to = '/signin') => {
|
||||
const router = useRouter();
|
||||
export const useRedirectIfNotAuthed = (to = "/signin") => {
|
||||
const router = useRouter()
|
||||
|
||||
const signedIn = isSignedIn();
|
||||
const signedIn = isSignedIn()
|
||||
|
||||
if (!signedIn) {
|
||||
router.push(to);
|
||||
}
|
||||
if (!signedIn) {
|
||||
router.push(to)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
export const isSignedIn = () => {
|
||||
const cookieList = cookies()
|
||||
return cookieList.has("drift-token") && cookieList.has("drift-userid")
|
||||
return cookieList.has(TOKEN_COOKIE_NAME) && cookieList.has(USER_COOKIE_NAME)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export async function withJwt(
|
|||
if (token == null) return res.status(401).send("Unauthorized")
|
||||
|
||||
const authToken = await prisma.authTokens.findUnique({
|
||||
// @ts-ignore
|
||||
where: { id: token }
|
||||
})
|
||||
if (authToken == null) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Parses a URL query string from string | string[] | ...
|
||||
* to string | undefined. If it's an array, we return the last item.
|
||||
*/
|
||||
export function parseUrlQuery(query: string | string[] | undefined) {
|
||||
export function parseQueryParam(query: string | string[] | undefined) {
|
||||
if (typeof query === "string") {
|
||||
return query
|
||||
} else if (Array.isArray(query)) {
|
|
@ -1,6 +1,7 @@
|
|||
import { NextFetchEvent, NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants"
|
||||
import serverConfig from "@lib/config"
|
||||
|
||||
const PUBLIC_FILE = /\.(.*)$/
|
||||
|
||||
|
@ -21,7 +22,7 @@ export function middleware(req: NextRequest, event: NextFetchEvent) {
|
|||
resp.cookies.delete(TOKEN_COOKIE_NAME)
|
||||
resp.cookies.delete(USER_COOKIE_NAME)
|
||||
const signoutPromise = new Promise((resolve) => {
|
||||
fetch(`${process.env.API_URL}/auth/signout`, {
|
||||
fetch(`${serverConfig.url}/auth/signout`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@ -65,7 +66,7 @@ export function middleware(req: NextRequest, event: NextFetchEvent) {
|
|||
|
||||
export const config = {
|
||||
match: [
|
||||
"/signout",
|
||||
// "/signout",
|
||||
// "/",
|
||||
"/signin",
|
||||
"/signup",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"dev": "next dev --port 3000",
|
||||
"build": "next build",
|
||||
"start": "next start --port 3000",
|
||||
"lint": "next lint && prettier --list-different --config .prettierrc '{components,lib,pages}/**/*.{ts,tsx}' --write",
|
||||
"lint": "next lint && prettier --list-different --config .prettierrc '{components,lib,app}/**/*.{ts,tsx}' --write",
|
||||
"analyze": "cross-env ANALYZE=true next build",
|
||||
"find:unused": "next-unused",
|
||||
"prisma": "prisma"
|
||||
|
@ -32,6 +32,7 @@
|
|||
"react-dropzone": "14.2.3",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-loading-skeleton": "3.1.0",
|
||||
"showdown": "^2.1.0",
|
||||
"swr": "1.3.0",
|
||||
"textarea-markdown-editor": "0.1.13",
|
||||
"zod": "^3.19.1"
|
||||
|
@ -45,6 +46,7 @@
|
|||
"@types/react": "18.0.9",
|
||||
"@types/react-datepicker": "4.4.1",
|
||||
"@types/react-dom": "18.0.3",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.2",
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import styles from "@styles/Home.module.css"
|
||||
|
||||
import { Page } from "@geist-ui/core/dist"
|
||||
import { useEffect } from "react"
|
||||
import Admin from "@components/admin"
|
||||
import useSignedIn from "@lib/hooks/use-signed-in"
|
||||
import { useRouter } from "next/router"
|
||||
import { GetServerSideProps } from "next"
|
||||
import cookie from "cookie"
|
||||
|
||||
const AdminPage = () => {
|
||||
const { signedIn } = useSignedIn()
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
if (!signedIn) {
|
||||
router.push("/")
|
||||
}
|
||||
}, [router, signedIn])
|
||||
return (
|
||||
<Page className={styles.wrapper}>
|
||||
<Page.Content className={styles.main}>
|
||||
<Admin />
|
||||
</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
|
||||
const driftToken = cookie.parse(req.headers.cookie || "")[`drift-token`]
|
||||
const res = await fetch(`${process.env.API_URL}/admin/is-admin`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${driftToken}`,
|
||||
"x-secret-key": process.env.SECRET_KEY || ""
|
||||
}
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
signedIn: true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
permanent: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPage
|
|
@ -1,32 +0,0 @@
|
|||
import config from "@lib/config"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
const handleRequiresPasscode = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) => {
|
||||
const requiresPasscode = Boolean(config.registration_password)
|
||||
return res.json({ requiresPasscode })
|
||||
}
|
||||
|
||||
const PATH_TO_HANDLER = {
|
||||
"requires-passcode": handleRequiresPasscode
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { slug } = req.query
|
||||
|
||||
if (!slug || Array.isArray(slug)) {
|
||||
return res.status(400).json({ error: "Missing param" })
|
||||
}
|
||||
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
if (PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER]) {
|
||||
return PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER](req, res)
|
||||
}
|
||||
default:
|
||||
return res.status(405).json({ error: "Method not allowed" })
|
||||
}
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
import config from "@lib/config"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
|
||||
export const getRequiresPasscode = async () => {
|
||||
const requiresPasscode = Boolean(config.registration_password)
|
||||
return requiresPasscode
|
||||
}
|
||||
|
||||
const handleRequiresPasscode = async (
|
||||
req: NextApiRequest,
|
||||
_: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) => {
|
||||
return res.json({ requiresPasscode: await getRequiresPasscode() })
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import getHtmlFromFile from "@lib/server/get-html-from-drift-file"
|
||||
import { parseUrlQuery } from "@lib/server/parse-url-query"
|
||||
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
|
||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
import prisma from "app/prisma"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
|
@ -10,9 +10,9 @@ export default async function handler(
|
|||
switch (req.method) {
|
||||
case "GET":
|
||||
const query = req.query
|
||||
const fileId = parseUrlQuery(query.fileId)
|
||||
const content = parseUrlQuery(query.content)
|
||||
const title = parseUrlQuery(query.title)
|
||||
const fileId = parseQueryParam(query.fileId)
|
||||
const content = parseQueryParam(query.content)
|
||||
const title = parseQueryParam(query.title)
|
||||
|
||||
if (fileId && (content || title)) {
|
||||
return res.status(400).json({ error: "Too many arguments" })
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import prisma from "app/prisma"
|
||||
|
||||
export const getPostsByUser = async (userId: number) => {
|
||||
const posts = await prisma.post.findMany({
|
||||
where: {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
return posts
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import config from "@lib/config"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
const handleSelf = async (req: NextApiRequest, res: NextApiResponse) => {}
|
||||
|
||||
const PATH_TO_HANDLER = {
|
||||
self: handleRequiresPasscode
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { slug } = req.query
|
||||
|
||||
if (!slug || Array.isArray(slug)) {
|
||||
return res.status(400).json({ error: "Missing param" })
|
||||
}
|
||||
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
if (PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER]) {
|
||||
return PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER](req, res)
|
||||
}
|
||||
default:
|
||||
return res.status(405).json({ error: "Method not allowed" })
|
||||
}
|
||||
}
|
21
client/pages/api/user/posts.ts
Normal file
21
client/pages/api/user/posts.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
import { getPostsByUser } from "app/prisma"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
export default async function handle(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
const userId = parseQueryParam(req.query.userId)
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: "Missing userId" })
|
||||
}
|
||||
|
||||
const posts = await getPostsByUser(userId)
|
||||
return res.json(posts)
|
||||
default:
|
||||
return res.status(405).end()
|
||||
}
|
||||
}
|
59
client/pages/api/user/self.ts
Normal file
59
client/pages/api/user/self.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
// user.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
|
||||
// const error = () =>
|
||||
// res.status(401).json({
|
||||
// message: "Unauthorized"
|
||||
// })
|
||||
|
||||
import { USER_COOKIE_NAME } from "@lib/constants"
|
||||
import prisma, { getUserById } from "app/prisma"
|
||||
import { getCookie } from "cookies-next"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
// try {
|
||||
// if (!req.user) {
|
||||
// return error()
|
||||
// }
|
||||
|
||||
// const user = await User.findByPk(req.user?.id, {
|
||||
// attributes: {
|
||||
// exclude: ["password"]
|
||||
// }
|
||||
// })
|
||||
// if (!user) {
|
||||
// return error()
|
||||
// }
|
||||
// res.json(user)
|
||||
// } catch (error) {
|
||||
// next(error)
|
||||
// }
|
||||
// })
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<any> {
|
||||
const error = () =>
|
||||
res.status(401).json({
|
||||
message: "Unauthorized"
|
||||
})
|
||||
|
||||
const userId = String(getCookie(USER_COOKIE_NAME, {
|
||||
req, res
|
||||
}))
|
||||
|
||||
if (!userId) {
|
||||
return error()
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await getUserById(userId);
|
||||
|
||||
if (!user) {
|
||||
return error()
|
||||
}
|
||||
return res.json(user)
|
||||
} catch (e) {
|
||||
console.warn(`/api/user/self:`, e)
|
||||
return error()
|
||||
}
|
||||
}
|
|
@ -1,33 +1,31 @@
|
|||
// a nextjs api handerl
|
||||
|
||||
import config from "@lib/config"
|
||||
import markdown from "@lib/render-markdown"
|
||||
import renderMarkdown from "@lib/render-markdown"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
export const getWelcomeContent = async () => {
|
||||
const introContent = config.welcome_content
|
||||
const introTitle = config.welcome_title
|
||||
// if (!introContent || !introTitle) {
|
||||
// return {}
|
||||
// }
|
||||
|
||||
console.log(introContent)
|
||||
|
||||
|
||||
console.log(renderMarkdown(introContent))
|
||||
return {
|
||||
title: introTitle,
|
||||
content: introContent,
|
||||
rendered: markdown(introContent)
|
||||
rendered: renderMarkdown(introContent)
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
_: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const welcomeContent = await getWelcomeContent()
|
||||
if (!welcomeContent) {
|
||||
return res.status(500).json({ error: "Missing welcome content" })
|
||||
}
|
||||
console.log(welcomeContent.title)
|
||||
|
||||
return res.json(welcomeContent)
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import { Note, Page, Text } from "@geist-ui/core/dist"
|
||||
import styles from "@styles/Home.module.css"
|
||||
|
||||
const Expired = () => {
|
||||
return (
|
||||
<Page>
|
||||
<Page.Content className={styles.main}>
|
||||
<Note type="error" label={false}>
|
||||
<Text h4>
|
||||
Error: The Drift you're trying to view has expired.
|
||||
</Text>
|
||||
</Note>
|
||||
</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default Expired
|
|
@ -1,74 +0,0 @@
|
|||
import styles from "@styles/Home.module.css"
|
||||
import PageSeo from "@components/page-seo"
|
||||
import HomeComponent from "@components/home"
|
||||
import { Page, Text } from "@geist-ui/core/dist"
|
||||
import type { GetStaticProps } from "next"
|
||||
import { InferGetStaticPropsType } from "next"
|
||||
type Props =
|
||||
| {
|
||||
introContent: string
|
||||
introTitle: string
|
||||
rendered: string
|
||||
}
|
||||
| {
|
||||
error: boolean
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
try {
|
||||
const resp = await fetch(process.env.API_URL + `/welcome`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-secret-key": process.env.SECRET_KEY || ""
|
||||
}
|
||||
})
|
||||
|
||||
const { title, content, rendered } = await resp.json()
|
||||
return {
|
||||
props: {
|
||||
introContent: content || null,
|
||||
rendered: rendered || null,
|
||||
introTitle: title || null
|
||||
},
|
||||
// Next.js will attempt to re-generate the page:
|
||||
// - When a request comes in
|
||||
// - At most every 60 seconds
|
||||
revalidate: 60 // In seconds
|
||||
}
|
||||
} catch (err) {
|
||||
// If there was an error, it's likely due to the server not running, so we attempt to regenerate the page
|
||||
return {
|
||||
props: {
|
||||
error: true
|
||||
},
|
||||
revalidate: 10 // In seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fix props type
|
||||
const Home = ({
|
||||
rendered,
|
||||
introContent,
|
||||
introTitle,
|
||||
error
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) => {
|
||||
return (
|
||||
<Page className={styles.wrapper}>
|
||||
<PageSeo />
|
||||
<Page.Content className={styles.main}>
|
||||
{error && <Text>Something went wrong. Is the server running?</Text>}
|
||||
{!error && (
|
||||
<HomeComponent
|
||||
rendered={rendered}
|
||||
introContent={introContent}
|
||||
introTitle={introTitle}
|
||||
/>
|
||||
)}
|
||||
</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
|
@ -1,78 +0,0 @@
|
|||
import styles from "@styles/Home.module.css"
|
||||
import NewPost from "@components/new-post"
|
||||
import Header from "@components/header"
|
||||
import PageSeo from "@components/page-seo"
|
||||
import { Page } from "@geist-ui/core/dist"
|
||||
import Head from "next/head"
|
||||
import { GetServerSideProps } from "next"
|
||||
import { Post } from "@lib/types"
|
||||
import cookie from "cookie"
|
||||
|
||||
const NewFromExisting = ({
|
||||
post,
|
||||
parentId
|
||||
}: {
|
||||
post: Post
|
||||
parentId: string
|
||||
}) => {
|
||||
return (
|
||||
<Page className={styles.wrapper}>
|
||||
<PageSeo title="Create a new Drift" />
|
||||
<Head>
|
||||
{/* TODO: solve this. */}
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/css/react-datepicker.css" />
|
||||
</Head>
|
||||
<Page.Content className={styles.main}>
|
||||
<NewPost initialPost={post} newPostParent={parentId} />
|
||||
</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({
|
||||
req,
|
||||
params
|
||||
}) => {
|
||||
const id = params?.id
|
||||
const redirect = {
|
||||
redirect: {
|
||||
destination: "/new",
|
||||
permanent: false
|
||||
}
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
return redirect
|
||||
}
|
||||
|
||||
const driftToken = cookie.parse(req.headers.cookie || "")[`drift-token`]
|
||||
|
||||
const post = await fetch(`${process.env.API_URL}/posts/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${driftToken}`,
|
||||
"x-secret-key": process.env.SECRET_KEY || ""
|
||||
}
|
||||
})
|
||||
|
||||
if (!post.ok) {
|
||||
return redirect
|
||||
}
|
||||
|
||||
const data = await post.json()
|
||||
|
||||
if (!data) {
|
||||
return redirect
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
post: data,
|
||||
parentId: id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NewFromExisting
|
|
@ -1,24 +0,0 @@
|
|||
import styles from "@styles/Home.module.css"
|
||||
import NewPost from "@components/new-post"
|
||||
import Header from "@components/header"
|
||||
import PageSeo from "@components/page-seo"
|
||||
import { Page } from "@geist-ui/core/dist"
|
||||
import Head from "next/head"
|
||||
|
||||
const New = () => {
|
||||
return (
|
||||
<Page className={styles.wrapper}>
|
||||
<PageSeo title="Create a new Drift" />
|
||||
<Head>
|
||||
{/* TODO: solve this. */}
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/css/react-datepicker.css" />
|
||||
</Head>
|
||||
<Page.Content className={styles.main}>
|
||||
<NewPost />
|
||||
</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default New
|
|
@ -1,27 +0,0 @@
|
|||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Text,
|
||||
Fieldset,
|
||||
Input,
|
||||
Page,
|
||||
Note,
|
||||
Textarea
|
||||
} from "@geist-ui/core/dist"
|
||||
import PageSeo from "@components/page-seo"
|
||||
import styles from "@styles/Home.module.css"
|
||||
import SettingsPage from "@components/settings"
|
||||
|
||||
const Settings = () => (
|
||||
<Page width={"100%"}>
|
||||
<PageSeo title="Drift - Settings" />
|
||||
<Page.Content
|
||||
className={styles.main}
|
||||
style={{ gap: "var(--gap)", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<SettingsPage />
|
||||
</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
|
||||
export default Settings
|
|
@ -1,14 +0,0 @@
|
|||
import { Page } from "@geist-ui/core/dist"
|
||||
import PageSeo from "@components/page-seo"
|
||||
import Auth from "@components/auth"
|
||||
import styles from "@styles/Home.module.css"
|
||||
const SignIn = () => (
|
||||
<Page width={"100%"}>
|
||||
<PageSeo title="Drift - Sign In" />
|
||||
<Page.Content className={styles.main}>
|
||||
<Auth page="signin" />
|
||||
</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
|
||||
export default SignIn
|
|
@ -1,15 +0,0 @@
|
|||
import { Page } from "@geist-ui/core/dist"
|
||||
import Auth from "@components/auth"
|
||||
import PageSeo from "@components/page-seo"
|
||||
import styles from "@styles/Home.module.css"
|
||||
|
||||
const SignUp = () => (
|
||||
<Page width="100%">
|
||||
<PageSeo title="Drift - Sign Up" />
|
||||
<Page.Content className={styles.main}>
|
||||
<Auth page="signup" />
|
||||
</Page.Content>
|
||||
</Page>
|
||||
)
|
||||
|
||||
export default SignUp
|
|
@ -12,6 +12,7 @@ specifiers:
|
|||
'@types/react': 18.0.9
|
||||
'@types/react-datepicker': 4.4.1
|
||||
'@types/react-dom': 18.0.3
|
||||
'@types/showdown': ^2.0.0
|
||||
bcrypt: ^5.1.0
|
||||
client-zip: 2.2.1
|
||||
clsx: ^1.2.1
|
||||
|
@ -36,6 +37,7 @@ specifiers:
|
|||
react-hot-toast: ^2.4.0
|
||||
react-loading-skeleton: 3.1.0
|
||||
sharp: ^0.31.2
|
||||
showdown: ^2.1.0
|
||||
swr: 1.3.0
|
||||
textarea-markdown-editor: 0.1.13
|
||||
typescript: 4.6.4
|
||||
|
@ -63,6 +65,7 @@ dependencies:
|
|||
react-dropzone: 14.2.3_react@18.2.0
|
||||
react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y
|
||||
react-loading-skeleton: 3.1.0_react@18.2.0
|
||||
showdown: 2.1.0
|
||||
swr: 1.3.0_react@18.2.0
|
||||
textarea-markdown-editor: 0.1.13_biqbaboplfbrettd7655fr4n2y
|
||||
zod: 3.19.1
|
||||
|
@ -79,6 +82,7 @@ devDependencies:
|
|||
'@types/react': 18.0.9
|
||||
'@types/react-datepicker': 4.4.1_biqbaboplfbrettd7655fr4n2y
|
||||
'@types/react-dom': 18.0.3
|
||||
'@types/showdown': 2.0.0
|
||||
cross-env: 7.0.3
|
||||
eslint: 8.27.0
|
||||
eslint-config-next: 13.0.2_hsmo2rtalirsvadpuxki35bq2i
|
||||
|
@ -469,6 +473,10 @@ packages:
|
|||
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
|
||||
dev: true
|
||||
|
||||
/@types/showdown/2.0.0:
|
||||
resolution: {integrity: sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser/5.42.1_hsmo2rtalirsvadpuxki35bq2i:
|
||||
resolution: {integrity: sha512-kAV+NiNBWVQDY9gDJDToTE/NO8BHi4f6b7zTsVAJoTkmB/zlfOpiEVBzHOKtlgTndCKe8vj9F/PuolemZSh50Q==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
@ -944,6 +952,11 @@ packages:
|
|||
engines: {node: '>= 6'}
|
||||
dev: true
|
||||
|
||||
/commander/9.4.1:
|
||||
resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==}
|
||||
engines: {node: ^12.20.0 || >=14}
|
||||
dev: false
|
||||
|
||||
/commondir/1.0.1:
|
||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||
dev: true
|
||||
|
@ -3546,6 +3559,13 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/showdown/2.1.0:
|
||||
resolution: {integrity: sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
commander: 9.4.1
|
||||
dev: false
|
||||
|
||||
/side-channel/1.0.4:
|
||||
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
||||
dependencies:
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `PostAuthors` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `authorId` to the `Posts` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
PRAGMA foreign_keys=off;
|
||||
DROP TABLE "PostAuthors";
|
||||
PRAGMA foreign_keys=on;
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_Posts" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"title" TEXT NOT NULL,
|
||||
"visibility" TEXT NOT NULL,
|
||||
"password" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"deletedAt" DATETIME,
|
||||
"expiresAt" DATETIME,
|
||||
"parentId" TEXT,
|
||||
"description" TEXT,
|
||||
"authorId" TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO "new_Posts" ("createdAt", "deletedAt", "description", "expiresAt", "id", "parentId", "password", "title", "updatedAt", "visibility") SELECT "createdAt", "deletedAt", "description", "expiresAt", "id", "parentId", "password", "title", "updatedAt", "visibility" FROM "Posts";
|
||||
DROP TABLE "Posts";
|
||||
ALTER TABLE "new_Posts" RENAME TO "Posts";
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
|
@ -68,8 +68,7 @@ model Post {
|
|||
expiresAt DateTime?
|
||||
parentId String?
|
||||
description String?
|
||||
// files File[]
|
||||
// postToAuthors PostToAuthors[]
|
||||
authorId String
|
||||
|
||||
@@map("Posts")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue