convert admin, run lint

This commit is contained in:
Max Leiter 2022-11-09 23:11:36 -08:00
parent cf7d89eb20
commit 8b0b172f7d
64 changed files with 410 additions and 581 deletions

View file

@ -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" />
}

View file

@ -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"

View file

@ -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} />
}

View 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&apos;re trying to view has expired.</Text>
</Note>
)
}

View file

@ -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
}: {

View file

@ -1,4 +1,4 @@
export default function NewLayout({ children }: { children: React.ReactNode }) {
// useRedirectIfNotAuthed()
return <>{children}</>;
// useRedirectIfNotAuthed()
return <>{children}</>
}

View file

@ -1,5 +1,5 @@
import NewPost from "@components/new-post"
import '@styles/react-datepicker.css'
import "@styles/react-datepicker.css"
const New = () => <NewPost />

View file

@ -0,0 +1,5 @@
import PageSeo from "@components/page-seo"
export default function Head() {
return <PageSeo title="Drift - Your profile" isPrivate />
}

View 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} />
}

View file

@ -0,0 +1,5 @@
import PageSeo from "@components/page-seo"
export default function Head() {
return <PageSeo title="Drift - Settings" isPrivate />
}

View file

@ -0,0 +1,5 @@
import SettingsPage from "@components/settings"
const Settings = () => <SettingsPage />
export default Settings

22
client/app/admin/page.tsx Normal file
View 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

View file

@ -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>
)

View file

@ -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} />
}

View file

@ -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>
)
}

View file

@ -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"
}

View file

@ -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>

View file

@ -1,3 +1,5 @@
"use client"
import { TOKEN_COOKIE_NAME } from "@lib/constants"
import { getCookie } from "cookies-next"
import styles from "./admin.module.css"

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -16,6 +16,7 @@ const PageSeo = ({
<>
<title>Drift - {title}</title>
{!isPrivate && <meta name="description" content={description} />}
{isPrivate && <meta name="robots" content="noindex" />}
</>
)
}

View file

@ -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">

View file

@ -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

View file

@ -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"

View file

@ -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) {

View file

@ -1,3 +1,5 @@
"use client"
import Password from "./sections/password"
import Profile from "./sections/profile"
import SettingsGroup from "../settings-group"

View file

@ -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 = () => {

View file

@ -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 = () => {

View file

@ -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,

View file

@ -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)

View file

@ -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)
});
})
}

View file

@ -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 }
}

View file

@ -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()

View file

@ -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('#')

View file

@ -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,

View file

@ -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

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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) {

View file

@ -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)) {

View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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" })
}
}

View file

@ -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() })

View file

@ -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" })

View file

@ -1,11 +0,0 @@
import prisma from "app/prisma"
export const getPostsByUser = async (userId: number) => {
const posts = await prisma.post.findMany({
where: {
}
})
return posts
}

View file

@ -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" })
}
}

View 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()
}
}

View 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()
}
}

View file

@ -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)
}

View file

@ -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&apos;re trying to view has expired.
</Text>
</Note>
</Page.Content>
</Page>
)
}
export default Expired

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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;

View file

@ -68,8 +68,7 @@ model Post {
expiresAt DateTime?
parentId String?
description String?
// files File[]
// postToAuthors PostToAuthors[]
authorId String
@@map("Posts")
}