fix admin page, expiring view, displayName setting/field
This commit is contained in:
parent
0627ab7396
commit
2b783145d4
25 changed files with 2079 additions and 442 deletions
|
@ -15,7 +15,9 @@ const NewFromExisting = async ({
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = await getPostById(id, true)
|
const post = await getPostById(id, {
|
||||||
|
withFiles: true,
|
||||||
|
})
|
||||||
|
|
||||||
return <NewPost initialPost={post} newPostParent={id} />
|
return <NewPost initialPost={post} newPostParent={id} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import VisibilityBadge from "@components/badges/visibility-badge"
|
||||||
import DocumentComponent from "./view-document"
|
import DocumentComponent from "./view-document"
|
||||||
import styles from "./post-page.module.css"
|
import styles from "./post-page.module.css"
|
||||||
|
|
||||||
import { Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core/dist"
|
import { Button, ButtonGroup, useMediaQuery } from "@geist-ui/core/dist"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import Archive from "@geist-ui/icons/archive"
|
import Archive from "@geist-ui/icons/archive"
|
||||||
import Edit from "@geist-ui/icons/edit"
|
import Edit from "@geist-ui/icons/edit"
|
||||||
|
@ -16,47 +16,54 @@ import ExpirationBadge from "@components/badges/expiration-badge"
|
||||||
import CreatedAgoBadge from "@components/badges/created-ago-badge"
|
import CreatedAgoBadge from "@components/badges/created-ago-badge"
|
||||||
import PasswordModalPage from "./password-modal-wrapper"
|
import PasswordModalPage from "./password-modal-wrapper"
|
||||||
import VisibilityControl from "@components/badges/visibility-control"
|
import VisibilityControl from "@components/badges/visibility-control"
|
||||||
import { File, PostWithFiles } from "@lib/server/prisma"
|
import { File, PostWithFilesAndAuthor } from "@lib/server/prisma"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
post: string | PostWithFiles
|
post: string | PostWithFilesAndAuthor
|
||||||
isProtected?: boolean
|
isProtected?: boolean
|
||||||
isAuthor?: boolean
|
isAuthor?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
|
const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
|
||||||
const [post, setPost] = useState<PostWithFiles>(typeof initialPost === "string" ? JSON.parse(initialPost) : initialPost)
|
const [post, setPost] = useState<PostWithFilesAndAuthor>(
|
||||||
const [visibility, setVisibility] = useState<string>(post.visibility)
|
typeof initialPost === "string" ? JSON.parse(initialPost) : initialPost
|
||||||
const [isExpired, setIsExpired] = useState(
|
|
||||||
post.expiresAt ? new Date(post.expiresAt) < new Date() : null
|
|
||||||
)
|
)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [visibility, setVisibility] = useState<string>(post.visibility)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isMobile = useMediaQuery("mobile")
|
const isMobile = useMediaQuery("mobile")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAuthor && isExpired) {
|
|
||||||
router.push("/expired")
|
|
||||||
}
|
|
||||||
|
|
||||||
const expirationDate = new Date(post.expiresAt ? post.expiresAt : "")
|
|
||||||
if (!isAuthor && expirationDate < new Date()) {
|
|
||||||
router.push("/expired")
|
|
||||||
} else {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
let interval: NodeJS.Timer | null = null
|
|
||||||
if (post.expiresAt) {
|
if (post.expiresAt) {
|
||||||
interval = setInterval(() => {
|
if (new Date(post.expiresAt) < new Date()) {
|
||||||
|
if (!isAuthor) {
|
||||||
|
router.push("/expired")
|
||||||
|
}
|
||||||
|
|
||||||
const expirationDate = new Date(post.expiresAt ? post.expiresAt : "")
|
const expirationDate = new Date(post.expiresAt ? post.expiresAt : "")
|
||||||
if (expirationDate < new Date()) setIsExpired(true)
|
if (!isAuthor && expirationDate < new Date()) {
|
||||||
}, 4000)
|
router.push("/expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
let interval: NodeJS.Timer | null = null
|
||||||
|
if (post.expiresAt) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
const expirationDate = new Date(
|
||||||
|
post.expiresAt ? post.expiresAt : ""
|
||||||
|
)
|
||||||
|
if (expirationDate < new Date()) {
|
||||||
|
if (!isAuthor) {
|
||||||
|
router.push("/expired")
|
||||||
|
}
|
||||||
|
clearInterval(interval!)
|
||||||
|
}
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (interval) clearInterval(interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return () => {
|
}, [isAuthor, post.expiresAt, router])
|
||||||
if (interval) clearInterval(interval)
|
|
||||||
}
|
|
||||||
}, [isExpired, isAuthor, post.expiresAt, router])
|
|
||||||
|
|
||||||
const download = async () => {
|
const download = async () => {
|
||||||
if (!post.files) return
|
if (!post.files) return
|
||||||
|
@ -85,11 +92,7 @@ const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
|
||||||
router.push(`/post/${post.parentId}`)
|
router.push(`/post/${post.parentId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
const isAvailable = !isProtected && post.title
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAvailable = !isExpired && !isProtected && post.title
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -128,7 +131,7 @@ const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.title}>
|
<span className={styles.title}>
|
||||||
<h3>{post.title}</h3>
|
<h3>{post.title} <span style={{color: 'var(--gray)'}}>by {post.author?.displayName}</span></h3>
|
||||||
<span className={styles.badges}>
|
<span className={styles.badges}>
|
||||||
<VisibilityBadge visibility={visibility} />
|
<VisibilityBadge visibility={visibility} />
|
||||||
<CreatedAgoBadge createdAt={post.createdAt} />
|
<CreatedAgoBadge createdAt={post.createdAt} />
|
||||||
|
|
|
@ -22,7 +22,10 @@ export type PostProps = {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const getPost = async (id: string) => {
|
const getPost = async (id: string) => {
|
||||||
const post = await getPostById(id, true)
|
const post = await getPostById(id, {
|
||||||
|
withFiles: true,
|
||||||
|
withAuthor: true,
|
||||||
|
})
|
||||||
const user = await getCurrentUser()
|
const user = await getCurrentUser()
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
|
@ -49,12 +52,21 @@ const getPost = async (id: string) => {
|
||||||
|
|
||||||
if (post.visibility === "protected" && !isAuthorOrAdmin) {
|
if (post.visibility === "protected" && !isAuthorOrAdmin) {
|
||||||
return {
|
return {
|
||||||
post,
|
// post,
|
||||||
isProtected: true,
|
isProtected: true,
|
||||||
isAuthor: isAuthorOrAdmin
|
isAuthor: isAuthorOrAdmin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// if expired
|
||||||
|
if (post.expiresAt && !isAuthorOrAdmin) {
|
||||||
|
const expirationDate = new Date(post.expiresAt)
|
||||||
|
if (expirationDate < new Date()) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { post, isAuthor: isAuthorOrAdmin }
|
return { post, isAuthor: isAuthorOrAdmin }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,23 @@
|
||||||
"use client"
|
import { getAllPosts, getAllUsers } from "@lib/server/prisma"
|
||||||
|
import { getCurrentUser } from "@lib/server/session"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
import styles from "./admin.module.css"
|
import styles from "./admin.module.css"
|
||||||
import PostTable from "./post-table"
|
import PostTable from "./post-table"
|
||||||
import UserTable from "./user-table"
|
import UserTable from "./user-table"
|
||||||
|
|
||||||
export const adminFetcher = async (
|
const Admin = async () => {
|
||||||
url: string,
|
const user = await getCurrentUser()
|
||||||
options?: {
|
if (!user) {
|
||||||
method?: string
|
return notFound()
|
||||||
body?: any
|
|
||||||
}
|
}
|
||||||
) =>
|
|
||||||
fetch("/api/admin" + url, {
|
|
||||||
method: options?.method || "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: options?.body && JSON.stringify(options.body)
|
|
||||||
})
|
|
||||||
|
|
||||||
const Admin = () => {
|
if (user.role !== "admin") {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = await getAllPosts()
|
||||||
|
const users = await getAllUsers()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.adminWrapper}>
|
<div className={styles.adminWrapper}>
|
||||||
<h2>Administration</h2>
|
<h2>Administration</h2>
|
||||||
|
@ -31,8 +29,8 @@ const Admin = () => {
|
||||||
gap: 4
|
gap: 4
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UserTable />
|
<UserTable users={users} />
|
||||||
<PostTable />
|
<PostTable posts={posts} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,25 +1,17 @@
|
||||||
|
'use client';
|
||||||
import SettingsGroup from "@components/settings-group"
|
import SettingsGroup from "@components/settings-group"
|
||||||
import { Fieldset, useToasts } from "@geist-ui/core/dist"
|
import { Fieldset, useToasts } from "@geist-ui/core/dist"
|
||||||
import byteToMB from "@lib/byte-to-mb"
|
import byteToMB from "@lib/byte-to-mb"
|
||||||
import { Post } from "@lib/types"
|
import { Post } from "@lib/server/prisma";
|
||||||
import Table from "rc-table"
|
import Table from "rc-table"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { adminFetcher } from "./admin"
|
|
||||||
import ActionDropdown from "./action-dropdown"
|
import ActionDropdown from "./action-dropdown"
|
||||||
|
|
||||||
const PostTable = () => {
|
const PostTable = ({
|
||||||
const [posts, setPosts] = useState<Post[]>()
|
posts,
|
||||||
const { setToast } = useToasts()
|
}: {
|
||||||
|
posts: Post[]
|
||||||
useEffect(() => {
|
}) => {
|
||||||
const fetchPosts = async () => {
|
|
||||||
const res = await adminFetcher("/posts")
|
|
||||||
const data = await res.json()
|
|
||||||
setPosts(data)
|
|
||||||
}
|
|
||||||
fetchPosts()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const tablePosts = useMemo(
|
const tablePosts = useMemo(
|
||||||
() =>
|
() =>
|
||||||
posts?.map((post) => {
|
posts?.map((post) => {
|
||||||
|
|
|
@ -1,28 +1,23 @@
|
||||||
|
"use client"
|
||||||
import { Fieldset, useToasts } from "@geist-ui/core/dist"
|
import { Fieldset, useToasts } from "@geist-ui/core/dist"
|
||||||
import { User } from "@lib/types"
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
|
||||||
import { adminFetcher } from "./admin"
|
|
||||||
import Table from "rc-table"
|
import Table from "rc-table"
|
||||||
import ActionDropdown from "./action-dropdown"
|
import ActionDropdown from "./action-dropdown"
|
||||||
import SettingsGroup from "@components/settings-group"
|
import SettingsGroup from "@components/settings-group"
|
||||||
|
import type { User, UserWithPosts } from "@lib/server/prisma"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
const UserTable = () => {
|
const UserTable = ({ users: initial }: { users: UserWithPosts[] }) => {
|
||||||
const [users, setUsers] = useState<User[]>()
|
const [users, setUsers] = useState(initial)
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
|
|
||||||
useEffect(() => {
|
console.log(initial)
|
||||||
const fetchUsers = async () => {
|
|
||||||
const res = await adminFetcher("/users")
|
|
||||||
const data = await res.json()
|
|
||||||
setUsers(data)
|
|
||||||
}
|
|
||||||
fetchUsers()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const toggleRole = async (id: string, role: "admin" | "user") => {
|
const toggleRole = async (id: string, role: "admin" | "user") => {
|
||||||
const res = await adminFetcher("/users/toggle-role", {
|
const res = await fetch("/api/admin?action=toggle-role", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: { id, role }
|
body: JSON.stringify({
|
||||||
|
userId: id,
|
||||||
|
role
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
@ -47,7 +42,7 @@ const UserTable = () => {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setToast({
|
setToast({
|
||||||
text: "Something went wrong",
|
text: "Something went wrong",
|
||||||
type: "error"
|
type: "error"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -56,11 +51,20 @@ const UserTable = () => {
|
||||||
const deleteUser = async (id: string) => {
|
const deleteUser = async (id: string) => {
|
||||||
const confirm = window.confirm("Are you sure you want to delete this user?")
|
const confirm = window.confirm("Are you sure you want to delete this user?")
|
||||||
if (!confirm) return
|
if (!confirm) return
|
||||||
const res = await adminFetcher(`/users/${id}`, {
|
// const res = await adminFetcher(`/users/${id}`, {
|
||||||
method: "DELETE"
|
// method: "DELETE"
|
||||||
|
// })
|
||||||
|
const res = await fetch("/api/admin?action=delete-user", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: id
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const json = await res.json()
|
setUsers((users) => {
|
||||||
|
const newUsers = users?.filter((user) => user.id !== id)
|
||||||
|
return newUsers
|
||||||
|
})
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setToast({
|
setToast({
|
||||||
|
@ -75,30 +79,24 @@ const UserTable = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableUsers = useMemo(
|
const tableUsers = users?.map((user) => {
|
||||||
() =>
|
return {
|
||||||
users?.map((user) => {
|
id: user.id,
|
||||||
return {
|
displayName: user.displayName,
|
||||||
id: user.id,
|
posts: user.posts?.length || 0,
|
||||||
username: user.username,
|
createdAt: `${new Date(user.createdAt)} ${new Date(
|
||||||
posts: user.posts?.length || 0,
|
user.createdAt
|
||||||
createdAt: `${new Date(
|
).toLocaleTimeString()}`,
|
||||||
user.createdAt
|
role: user.role,
|
||||||
).toLocaleDateString()} ${new Date(
|
actions: ""
|
||||||
user.createdAt
|
}
|
||||||
).toLocaleTimeString()}`,
|
})
|
||||||
role: user.role,
|
|
||||||
actions: ""
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[users]
|
|
||||||
)
|
|
||||||
|
|
||||||
const usernameColumns = [
|
const usernameColumns = [
|
||||||
{
|
{
|
||||||
title: "Username",
|
title: "Name",
|
||||||
dataIndex: "username",
|
dataIndex: "displayName",
|
||||||
key: "username",
|
key: "displayName",
|
||||||
width: 50
|
width: 50
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { getCurrentUser } from "@lib/server/session"
|
|
||||||
import Admin from "./components/admin"
|
|
||||||
import { notFound } from "next/navigation"
|
|
||||||
|
|
||||||
const AdminPage = async () => {
|
|
||||||
const user = await getCurrentUser();
|
|
||||||
if (!user) {
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.role !== "admin") {
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Admin />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AdminPage
|
|
|
@ -37,14 +37,13 @@ type Tab = {
|
||||||
href?: string
|
href?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = ({ signedIn = false }) => {
|
const Header = ({ signedIn = false, isAdmin = false }) => {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const [expanded, setExpanded] = useState<boolean>(false)
|
const [expanded, setExpanded] = useState<boolean>(false)
|
||||||
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
|
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
|
||||||
const isMobile = useMediaQuery("xs", { match: "down" })
|
const isMobile = useMediaQuery("xs", { match: "down" })
|
||||||
// const { status } = useSession()
|
// const { status } = useSession()
|
||||||
// const signedIn = status === "authenticated"
|
// const signedIn = status === "authenticated"
|
||||||
const [pages, setPages] = useState<Tab[]>([])
|
|
||||||
const { setTheme, resolvedTheme } = useTheme()
|
const { setTheme, resolvedTheme } = useTheme()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -57,7 +56,7 @@ const Header = ({ signedIn = false }) => {
|
||||||
}
|
}
|
||||||
}, [isMobile])
|
}, [isMobile])
|
||||||
|
|
||||||
useEffect(() => {
|
const getPages = () => {
|
||||||
const defaultPages: Tab[] = [
|
const defaultPages: Tab[] = [
|
||||||
{
|
{
|
||||||
name: isMobile ? "GitHub" : "",
|
name: isMobile ? "GitHub" : "",
|
||||||
|
@ -76,8 +75,17 @@ const Header = ({ signedIn = false }) => {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
defaultPages.push({
|
||||||
|
name: "admin",
|
||||||
|
icon: <SettingsIcon />,
|
||||||
|
value: "admin",
|
||||||
|
href: "/admin"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (signedIn)
|
if (signedIn)
|
||||||
setPages([
|
return [
|
||||||
{
|
{
|
||||||
name: "new",
|
name: "new",
|
||||||
icon: <NewIcon />,
|
icon: <NewIcon />,
|
||||||
|
@ -103,9 +111,9 @@ const Header = ({ signedIn = false }) => {
|
||||||
onClick: () => signOut()
|
onClick: () => signOut()
|
||||||
},
|
},
|
||||||
...defaultPages
|
...defaultPages
|
||||||
])
|
]
|
||||||
else
|
else
|
||||||
setPages([
|
return [
|
||||||
{
|
{
|
||||||
name: "home",
|
name: "home",
|
||||||
icon: <HomeIcon />,
|
icon: <HomeIcon />,
|
||||||
|
@ -125,67 +133,46 @@ const Header = ({ signedIn = false }) => {
|
||||||
href: "/signup"
|
href: "/signup"
|
||||||
},
|
},
|
||||||
...defaultPages
|
...defaultPages
|
||||||
])
|
]
|
||||||
// if (userData?.role === "admin") {
|
}
|
||||||
// setPages((pages) => [
|
|
||||||
// ...pages,
|
|
||||||
// {
|
|
||||||
// name: "admin",
|
|
||||||
// icon: <SettingsIcon />,
|
|
||||||
// value: "admin",
|
|
||||||
// href: "/admin"
|
|
||||||
// }
|
|
||||||
// ])
|
|
||||||
// }
|
|
||||||
// TODO: investigate deps causing infinite loop
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isMobile, resolvedTheme])
|
|
||||||
|
|
||||||
const onTabChange = useCallback(
|
const pages = getPages()
|
||||||
(tab: string) => {
|
|
||||||
if (typeof window === "undefined") return
|
|
||||||
const match = pages.find((page) => page.value === tab)
|
|
||||||
if (match?.onClick) {
|
|
||||||
match.onClick()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[pages]
|
|
||||||
)
|
|
||||||
|
|
||||||
const getButton = useCallback(
|
const onTabChange = (tab: string) => {
|
||||||
(tab: Tab) => {
|
if (typeof window === "undefined") return
|
||||||
const activeStyle = pathname === tab.href ? styles.active : ""
|
const match = pages.find((page) => page.value === tab)
|
||||||
if (tab.onClick) {
|
if (match?.onClick) {
|
||||||
return (
|
match.onClick()
|
||||||
<Button
|
}
|
||||||
auto={isMobile ? false : true}
|
}
|
||||||
key={tab.value}
|
|
||||||
icon={tab.icon}
|
const getButton = (tab: Tab) => {
|
||||||
onClick={() => onTabChange(tab.value)}
|
const activeStyle = pathname === tab.href ? styles.active : ""
|
||||||
className={`${styles.tab} ${activeStyle}`}
|
if (tab.onClick) {
|
||||||
shadow={false}
|
return (
|
||||||
>
|
<Button
|
||||||
|
auto={isMobile ? false : true}
|
||||||
|
key={tab.value}
|
||||||
|
icon={tab.icon}
|
||||||
|
onClick={() => onTabChange(tab.value)}
|
||||||
|
className={`${styles.tab} ${activeStyle}`}
|
||||||
|
shadow={false}
|
||||||
|
>
|
||||||
|
{tab.name ? tab.name : undefined}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
} else if (tab.href) {
|
||||||
|
return (
|
||||||
|
<Link key={tab.value} href={tab.href} className={styles.tab}>
|
||||||
|
<Button auto={isMobile ? false : true} icon={tab.icon} shadow={false}>
|
||||||
{tab.name ? tab.name : undefined}
|
{tab.name ? tab.name : undefined}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
</Link>
|
||||||
} else if (tab.href) {
|
)
|
||||||
return (
|
}
|
||||||
<Link key={tab.value} href={tab.href} className={styles.tab}>
|
}
|
||||||
<Button
|
|
||||||
auto={isMobile ? false : true}
|
|
||||||
icon={tab.icon}
|
|
||||||
shadow={false}
|
|
||||||
>
|
|
||||||
{tab.name ? tab.name : undefined}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isMobile, onTabChange, pathname]
|
|
||||||
)
|
|
||||||
|
|
||||||
const buttons = useMemo(() => pages.map(getButton), [pages, getButton])
|
const buttons = pages.map(getButton)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page.Header>
|
<Page.Header>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { ServerThemeProvider } from "next-themes"
|
||||||
import { LayoutWrapper } from "./root-layout-wrapper"
|
import { LayoutWrapper } from "./root-layout-wrapper"
|
||||||
import styles from '@styles/Home.module.css';
|
import styles from '@styles/Home.module.css';
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { getSession } from "@lib/server/session";
|
||||||
|
|
||||||
interface RootLayoutProps {
|
interface RootLayoutProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
@ -10,8 +11,7 @@ interface RootLayoutProps {
|
||||||
|
|
||||||
export default async function RootLayout({ children }: RootLayoutProps) {
|
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||||
// TODO: this opts out of SSG
|
// TODO: this opts out of SSG
|
||||||
const cookiesList = cookies();
|
const session = await getSession()
|
||||||
const hasNextAuth = cookiesList.get("next-auth.session-token") !== undefined;
|
|
||||||
return (
|
return (
|
||||||
<ServerThemeProvider
|
<ServerThemeProvider
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
|
@ -23,7 +23,7 @@ export default async function RootLayout({ children }: RootLayoutProps) {
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body className={styles.main}>
|
<body className={styles.main}>
|
||||||
<LayoutWrapper signedIn={hasNextAuth}>{children}</LayoutWrapper>
|
<LayoutWrapper signedIn={Boolean(session?.user)} isAdmin={session?.user.role === "admin"}>{children}</LayoutWrapper>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</ServerThemeProvider>
|
</ServerThemeProvider>
|
||||||
|
|
|
@ -7,14 +7,13 @@ import * as RadixTooltip from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
export function LayoutWrapper({
|
export function LayoutWrapper({
|
||||||
children,
|
children,
|
||||||
signedIn
|
signedIn,
|
||||||
|
isAdmin,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
signedIn?: boolean
|
signedIn?: boolean
|
||||||
|
isAdmin?: boolean
|
||||||
}) {
|
}) {
|
||||||
const skeletonBaseColor = "var(--light-gray)"
|
|
||||||
const skeletonHighlightColor = "var(--lighter-gray)"
|
|
||||||
|
|
||||||
const customTheme = Themes.createFromLight({
|
const customTheme = Themes.createFromLight({
|
||||||
type: "custom",
|
type: "custom",
|
||||||
palette: {
|
palette: {
|
||||||
|
@ -63,7 +62,7 @@ export function LayoutWrapper({
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Page width={"100%"}>
|
<Page width={"100%"}>
|
||||||
<Page.Header>
|
<Page.Header>
|
||||||
<Header signedIn={signedIn} />
|
<Header isAdmin={isAdmin} signedIn={signedIn} />
|
||||||
</Page.Header>
|
</Page.Header>
|
||||||
{children}
|
{children}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -34,7 +34,7 @@ const Profile = ({ user }: { user: User }) => {
|
||||||
bio
|
bio
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch("/server-api/user/profile", {
|
const res = await fetch(`/api/user/${user.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import Header from "@components/header"
|
|
||||||
import SettingsGroup from "../components/settings-group"
|
import SettingsGroup from "../components/settings-group"
|
||||||
import Password from "app/settings/components/sections/password"
|
|
||||||
import Profile from "app/settings/components/sections/profile"
|
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"
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
@import "./inter.css";
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Spacing */
|
/* Spacing */
|
||||||
--gap-quarter: 0.25rem;
|
--gap-quarter: 0.25rem;
|
||||||
|
@ -163,7 +161,7 @@ code {
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: this should not be necessary. */
|
/* TODO: these should not be necessary. */
|
||||||
main {
|
main {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: block;
|
|
||||||
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
|
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 200;
|
|
||||||
font-display: block;
|
|
||||||
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
|
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: block;
|
|
||||||
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
|
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: block;
|
|
||||||
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
|
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: block;
|
|
||||||
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
|
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
font-display: block;
|
|
||||||
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
|
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: block;
|
|
||||||
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
|
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 800;
|
|
||||||
font-display: block;
|
|
||||||
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
|
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Inter";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: block;
|
|
||||||
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
|
|
||||||
format("woff2");
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
|
11
client/jest.config.js
Normal file
11
client/jest.config.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/** @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/"]
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ declare global {
|
||||||
|
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import { Post, PrismaClient, File, User, Prisma } from "@prisma/client"
|
import { Post, PrismaClient, File, User, Prisma } from "@prisma/client"
|
||||||
|
export type { User, File, Post } from "@prisma/client"
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -34,12 +35,28 @@ const updateDates = (input: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const prisma =
|
export const prisma =
|
||||||
global.prisma ||
|
global.prisma ||
|
||||||
new PrismaClient({
|
new PrismaClient({
|
||||||
log: ["query"]
|
log: ["query"]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// a prisma middleware for capturing the first user and making them an admin
|
||||||
|
prisma.$use(async (params, next) => {
|
||||||
|
const result = await next(params)
|
||||||
|
if (params.model === "User" && params.action === "create") {
|
||||||
|
const users = await prisma.user.findMany()
|
||||||
|
if (users.length === 1) {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: users[0].id },
|
||||||
|
data: { role: "admin" }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
// prisma.$use(async (params, next) => {
|
// prisma.$use(async (params, next) => {
|
||||||
// const result = await next(params)
|
// const result = await next(params)
|
||||||
// return updateDates(result)
|
// return updateDates(result)
|
||||||
|
@ -47,12 +64,14 @@ export const prisma =
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") global.prisma = prisma
|
if (process.env.NODE_ENV !== "production") global.prisma = prisma
|
||||||
|
|
||||||
export type { User, File, Post } from "@prisma/client"
|
|
||||||
|
|
||||||
export type PostWithFiles = Post & {
|
export type PostWithFiles = Post & {
|
||||||
files: File[]
|
files: File[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PostWithFilesAndAuthor = PostWithFiles & {
|
||||||
|
author: User
|
||||||
|
}
|
||||||
|
|
||||||
export const getFilesForPost = async (postId: string) => {
|
export const getFilesForPost = async (postId: string) => {
|
||||||
const files = await prisma.file.findMany({
|
const files = await prisma.file.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
@ -104,7 +123,7 @@ export const getUserById = async (userId: User["id"]) => {
|
||||||
email: true,
|
email: true,
|
||||||
// displayName: true,
|
// displayName: true,
|
||||||
role: true,
|
role: true,
|
||||||
username: true
|
displayName: true,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -151,13 +170,29 @@ export const createUser = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPostById = async (postId: Post["id"], withFiles = false) => {
|
type GetPostByIdOptions = {
|
||||||
|
withFiles: boolean
|
||||||
|
withAuthor: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPostById = async (
|
||||||
|
postId: Post["id"],
|
||||||
|
options?: GetPostByIdOptions
|
||||||
|
) => {
|
||||||
const post = await prisma.post.findUnique({
|
const post = await prisma.post.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: postId
|
id: postId
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
files: withFiles
|
files: options?.withFiles,
|
||||||
|
author: options?.withAuthor
|
||||||
|
? {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
displayName: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -183,11 +218,29 @@ export const getAllPosts = async ({
|
||||||
return posts as PostWithFiles[]
|
return posts as PostWithFiles[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserWithPosts = User & {
|
||||||
|
posts: Post[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllUsers = async () => {
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
// displayName: true,
|
||||||
|
role: true,
|
||||||
|
posts: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return users as UserWithPosts[]
|
||||||
|
}
|
||||||
|
|
||||||
export const searchPosts = async (
|
export const searchPosts = async (
|
||||||
query: string,
|
query: string,
|
||||||
{
|
{
|
||||||
withFiles = false,
|
withFiles = false,
|
||||||
userId,
|
userId
|
||||||
}: {
|
}: {
|
||||||
withFiles?: boolean
|
withFiles?: boolean
|
||||||
userId?: User["id"]
|
userId?: User["id"]
|
||||||
|
|
40
client/lib/types.d.ts
vendored
40
client/lib/types.d.ts
vendored
|
@ -1,40 +0,0 @@
|
||||||
export type PostVisibility = "unlisted" | "private" | "public" | "protected"
|
|
||||||
|
|
||||||
export type Document = {
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type File = {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
html: string
|
|
||||||
createdAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Files = File[]
|
|
||||||
|
|
||||||
export type Post = {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
visibility: PostVisibility
|
|
||||||
files?: Files
|
|
||||||
createdAt: string
|
|
||||||
users?: User[]
|
|
||||||
parent?: Pick<Post, "id" | "title" | "visibility" | "createdAt">
|
|
||||||
expiresAt: Date | string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type User = {
|
|
||||||
id: string
|
|
||||||
username: string
|
|
||||||
posts?: Post[]
|
|
||||||
role: "admin" | "user" | ""
|
|
||||||
createdAt: string
|
|
||||||
displayName?: string
|
|
||||||
bio?: string
|
|
||||||
email?: string
|
|
||||||
}
|
|
|
@ -9,7 +9,8 @@
|
||||||
"lint": "next lint && prettier --list-different --config .prettierrc '{components,lib,app}/**/*.{ts,tsx}' --write",
|
"lint": "next lint && prettier --list-different --config .prettierrc '{components,lib,app}/**/*.{ts,tsx}' --write",
|
||||||
"analyze": "cross-env ANALYZE=true next build",
|
"analyze": "cross-env ANALYZE=true next build",
|
||||||
"find:unused": "next-unused",
|
"find:unused": "next-unused",
|
||||||
"prisma": "prisma"
|
"prisma": "prisma",
|
||||||
|
"jest": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@geist-ui/core": "^2.3.8",
|
"@geist-ui/core": "^2.3.8",
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
"client-zip": "2.2.1",
|
"client-zip": "2.2.1",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cookies-next": "^2.1.1",
|
"cookies-next": "^2.1.1",
|
||||||
|
"jest": "^29.3.1",
|
||||||
"next": "13.0.3-canary.4",
|
"next": "13.0.3-canary.4",
|
||||||
"next-auth": "^4.16.4",
|
"next-auth": "^4.16.4",
|
||||||
"next-themes": "npm:@wits/next-themes@0.2.7",
|
"next-themes": "npm:@wits/next-themes@0.2.7",
|
||||||
|
@ -34,7 +36,8 @@
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"swr": "1.3.0",
|
"swr": "1.3.0",
|
||||||
"textarea-markdown-editor": "0.1.13"
|
"textarea-markdown-editor": "0.1.13",
|
||||||
|
"ts-jest": "^29.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "12.1.6",
|
"@next/bundle-analyzer": "12.1.6",
|
||||||
|
|
|
@ -27,7 +27,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
|
||||||
const session = await getSession({ req })
|
const session = await getSession({ req })
|
||||||
const id = session?.user?.id
|
const id = session?.user?.id
|
||||||
// get admin from db
|
|
||||||
|
|
||||||
const isAdmin = await prisma.user
|
const isAdmin = await prisma.user
|
||||||
.findUnique({
|
.findUnique({
|
||||||
|
|
|
@ -22,7 +22,10 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
return res.status(400).json({ error: "Missing id" })
|
return res.status(400).json({ error: "Missing id" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = await getPostById(id, Boolean(files))
|
const post = await getPostById(id, {
|
||||||
|
withFiles: Boolean(files),
|
||||||
|
withAuthor: true
|
||||||
|
})
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return res.status(404).json({ message: "Post not found" })
|
return res.status(404).json({ message: "Post not found" })
|
||||||
|
|
|
@ -21,8 +21,7 @@ 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) {
|
if (!session || !session.user.id) {
|
||||||
console.log("no session")
|
|
||||||
return res.status(401).json({ error: "Unauthorized" })
|
return res.status(401).json({ error: "Unauthorized" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,12 +52,8 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
expiresAt: req.body.expiresAt,
|
expiresAt: req.body.expiresAt,
|
||||||
parentId: req.body.parentId,
|
parentId: req.body.parentId,
|
||||||
// authorId: session?.user.id,
|
authorId: session.user.id,
|
||||||
author: {
|
|
||||||
connect: {
|
|
||||||
id: session?.user.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// files: {
|
// files: {
|
||||||
// connectOrCreate: postFiles.map((file) => ({
|
// connectOrCreate: postFiles.map((file) => ({
|
||||||
// where: {
|
// where: {
|
||||||
|
|
52
client/pages/api/user/[id].ts
Normal file
52
client/pages/api/user/[id].ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// api/user/[id].ts
|
||||||
|
|
||||||
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
|
import { getUserById } from "@lib/server/prisma"
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
import { prisma } from "lib/server/prisma"
|
||||||
|
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||||
|
import { getSession } from "next-auth/react"
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
const id = parseQueryParam(req.query.id)
|
||||||
|
if (!id) {
|
||||||
|
return res.status(400).json({ error: "Missing id" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getUserById(id)
|
||||||
|
const currUser = (await getSession({ req }))?.user
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: "User not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.id !== currUser?.id) {
|
||||||
|
return res.status(403).json({ message: "Unauthorized" })
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (req.method) {
|
||||||
|
case "PUT":
|
||||||
|
const { displayName } = req.body
|
||||||
|
const updatedUser = await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
displayName
|
||||||
|
// bio
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
id: updatedUser.id,
|
||||||
|
name: updatedUser.displayName
|
||||||
|
// bio: updatedUser.bio
|
||||||
|
})
|
||||||
|
case "GET":
|
||||||
|
return res.json(currUser)
|
||||||
|
default:
|
||||||
|
return res.status(405).json({ message: "Method not allowed" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withMethods(["GET", "PUT"], handler)
|
|
@ -1,24 +0,0 @@
|
||||||
import { getCurrentUser } from "@lib/server/session"
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
_: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
): Promise<any> {
|
|
||||||
const error = () =>
|
|
||||||
res.status(401).json({
|
|
||||||
message: "Unauthorized"
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
const user = await getCurrentUser()
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return error()
|
|
||||||
}
|
|
||||||
return res.json(user)
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`/api/user/self:`, e)
|
|
||||||
return error()
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -88,15 +88,16 @@ model User {
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
|
||||||
// custom fields
|
// custom fields
|
||||||
posts Post[]
|
posts Post[]
|
||||||
username String? @unique
|
|
||||||
role String? @default("user")
|
role String? @default("user")
|
||||||
password String? @db.Text
|
displayName String?
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue