client/server: admin page improvements; add deleting users and changing roles
This commit is contained in:
parent
7d5afbc682
commit
7d08570915
13 changed files with 511 additions and 187 deletions
|
@ -1,130 +1,35 @@
|
||||||
import { Text, Fieldset, Spacer, Link } from "@geist-ui/core"
|
import { Text, Fieldset, Spacer } from "@geist-ui/core"
|
||||||
import { Post, User } from "@lib/types"
|
import { Post, User } from "@lib/types"
|
||||||
import Cookies from "js-cookie"
|
import Cookies from "js-cookie"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import useSWR from "swr"
|
|
||||||
import styles from "./admin.module.css"
|
import styles from "./admin.module.css"
|
||||||
import PostModal from "./post-modal-link"
|
import PostModal from "./post-modal-link"
|
||||||
|
import PostTable from "./post-table"
|
||||||
|
import UserTable from "./user-table"
|
||||||
|
|
||||||
export const adminFetcher = (url: string) =>
|
export const adminFetcher = async (
|
||||||
fetch(url, {
|
url: string,
|
||||||
method: "GET",
|
options?: {
|
||||||
|
method?: string
|
||||||
|
body?: any
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
fetch("/server-api/admin" + url, {
|
||||||
|
method: options?.method || "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${Cookies.get("drift-token")}`
|
Authorization: `Bearer ${Cookies.get("drift-token")}`
|
||||||
}
|
},
|
||||||
}).then((res) => res.json())
|
body: options?.body && JSON.stringify(options.body)
|
||||||
|
})
|
||||||
|
|
||||||
const Admin = () => {
|
const Admin = () => {
|
||||||
const { data: posts, error: postsError } = useSWR<Post[]>(
|
|
||||||
"/server-api/admin/posts",
|
|
||||||
adminFetcher
|
|
||||||
)
|
|
||||||
const { data: users, error: usersError } = useSWR<User[]>(
|
|
||||||
"/server-api/admin/users",
|
|
||||||
adminFetcher
|
|
||||||
)
|
|
||||||
const [postSizes, setPostSizes] = useState<{ [key: string]: number }>({})
|
|
||||||
const byteToMB = (bytes: number) =>
|
|
||||||
Math.round((bytes / 1024 / 1024) * 100) / 100
|
|
||||||
useEffect(() => {
|
|
||||||
if (posts) {
|
|
||||||
// sum the sizes of each file per post
|
|
||||||
const sizes = posts.reduce((acc, post) => {
|
|
||||||
const size =
|
|
||||||
post.files?.reduce((acc, file) => acc + file.html.length, 0) || 0
|
|
||||||
return { ...acc, [post.id]: byteToMB(size) }
|
|
||||||
}, {})
|
|
||||||
setPostSizes(sizes)
|
|
||||||
}
|
|
||||||
}, [posts])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.adminWrapper}>
|
<div className={styles.adminWrapper}>
|
||||||
<Text h2>Administration</Text>
|
<Text h2>Administration</Text>
|
||||||
<Fieldset>
|
<UserTable />
|
||||||
<Fieldset.Title>Users</Fieldset.Title>
|
|
||||||
{users && <Fieldset.Subtitle>{users.length} users</Fieldset.Subtitle>}
|
|
||||||
{!users && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
|
|
||||||
{usersError && <Fieldset.Subtitle>An error occured</Fieldset.Subtitle>}
|
|
||||||
{users && (
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Posts</th>
|
|
||||||
<th>Created</th>
|
|
||||||
<th>Role</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{users?.map((user) => (
|
|
||||||
<tr key={user.id}>
|
|
||||||
<td>{user.username}</td>
|
|
||||||
<td>{user.posts?.length}</td>
|
|
||||||
<td>
|
|
||||||
{new Date(user.createdAt).toLocaleDateString()}{" "}
|
|
||||||
{new Date(user.createdAt).toLocaleTimeString()}
|
|
||||||
</td>
|
|
||||||
<td>{user.role}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</Fieldset>
|
|
||||||
<Spacer height={1} />
|
<Spacer height={1} />
|
||||||
<Fieldset>
|
<PostTable />
|
||||||
<Fieldset.Title>Posts</Fieldset.Title>
|
|
||||||
{posts && <Fieldset.Subtitle>{posts.length} posts</Fieldset.Subtitle>}
|
|
||||||
{!posts && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
|
|
||||||
{postsError && <Fieldset.Subtitle>An error occured</Fieldset.Subtitle>}
|
|
||||||
{posts && (
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Visibility</th>
|
|
||||||
<th>Created</th>
|
|
||||||
<th>Author</th>
|
|
||||||
<th>Size</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{posts?.map((post) => (
|
|
||||||
<tr key={post.id}>
|
|
||||||
<td>
|
|
||||||
<PostModal id={post.id} />
|
|
||||||
</td>
|
|
||||||
<td>{post.visibility}</td>
|
|
||||||
<td>
|
|
||||||
{new Date(post.createdAt).toLocaleDateString()}{" "}
|
|
||||||
{new Date(post.createdAt).toLocaleTimeString()}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{post.users?.length ? (
|
|
||||||
post.users[0].username
|
|
||||||
) : (
|
|
||||||
<i>Deleted</i>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{postSizes[post.id] ? `${postSizes[post.id]} MB` : ""}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
{Object.keys(postSizes).length && (
|
|
||||||
<div style={{ float: "right" }}>
|
|
||||||
<Text>
|
|
||||||
Total size:{" "}
|
|
||||||
{Object.values(postSizes).reduce((prev, curr) => prev + curr)} MB
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Fieldset>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
139
client/components/admin/post-table.tsx
Normal file
139
client/components/admin/post-table.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import { Button, Fieldset, Link, Popover, useToasts } from "@geist-ui/core"
|
||||||
|
import MoreVertical from "@geist-ui/icons/moreVertical"
|
||||||
|
import { Post } from "@lib/types"
|
||||||
|
import Cookies from "js-cookie"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { adminFetcher } from "."
|
||||||
|
import Table from "rc-table"
|
||||||
|
import byteToMB from "@lib/byte-to-mb"
|
||||||
|
|
||||||
|
const PostTable = () => {
|
||||||
|
const [posts, setPosts] = useState<Post[]>()
|
||||||
|
const { setToast } = useToasts()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPosts = async () => {
|
||||||
|
const res = await adminFetcher("/posts")
|
||||||
|
const data = await res.json()
|
||||||
|
setPosts(data)
|
||||||
|
}
|
||||||
|
fetchPosts()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const tablePosts = useMemo(
|
||||||
|
() =>
|
||||||
|
posts?.map((post) => {
|
||||||
|
return {
|
||||||
|
id: post.id,
|
||||||
|
title: post.title,
|
||||||
|
files: post.files?.length || 0,
|
||||||
|
createdAt: `${new Date(
|
||||||
|
post.createdAt
|
||||||
|
).toLocaleDateString()} ${new Date(
|
||||||
|
post.createdAt
|
||||||
|
).toLocaleTimeString()}`,
|
||||||
|
visibility: post.visibility,
|
||||||
|
size: post.files
|
||||||
|
? byteToMB(
|
||||||
|
post.files.reduce((acc, file) => acc + file.html.length, 0)
|
||||||
|
)
|
||||||
|
: 0,
|
||||||
|
actions: ""
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[posts]
|
||||||
|
)
|
||||||
|
|
||||||
|
// const deletePost = async (id: number) => {
|
||||||
|
// const confirm = window.confirm("Are you sure you want to delete this post?")
|
||||||
|
// if (!confirm) return
|
||||||
|
// const res = await adminFetcher(`/posts/${id}`, {
|
||||||
|
// method: "DELETE",
|
||||||
|
// })
|
||||||
|
|
||||||
|
// const json = await res.json()
|
||||||
|
|
||||||
|
// if (res.status === 200) {
|
||||||
|
// setToast({
|
||||||
|
// text: "Post deleted",
|
||||||
|
// type: "success"
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// setToast({
|
||||||
|
// text: json.error || "Something went wrong",
|
||||||
|
// type: "error"
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const tableColumns = [
|
||||||
|
{
|
||||||
|
title: "Title",
|
||||||
|
dataIndex: "title",
|
||||||
|
key: "title",
|
||||||
|
width: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Files",
|
||||||
|
dataIndex: "files",
|
||||||
|
key: "files",
|
||||||
|
width: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Created",
|
||||||
|
dataIndex: "createdAt",
|
||||||
|
key: "createdAt",
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Visibility",
|
||||||
|
dataIndex: "visibility",
|
||||||
|
key: "visibility",
|
||||||
|
width: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Size (MB)",
|
||||||
|
dataIndex: "size",
|
||||||
|
key: "size",
|
||||||
|
width: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Actions",
|
||||||
|
dataIndex: "",
|
||||||
|
key: "actions",
|
||||||
|
width: 50,
|
||||||
|
render(post: any) {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 100,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* <Link href="#" onClick={() => deletePost(post.id)}>Delete post</Link> */}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
hideArrow
|
||||||
|
>
|
||||||
|
<Button iconRight={<MoreVertical />} auto></Button>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fieldset>
|
||||||
|
<Fieldset.Title>Posts</Fieldset.Title>
|
||||||
|
{posts && <Fieldset.Subtitle>{posts.length} posts</Fieldset.Subtitle>}
|
||||||
|
{!posts && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
|
||||||
|
{posts && <Table columns={tableColumns} data={tablePosts} />}
|
||||||
|
</Fieldset>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostTable
|
165
client/components/admin/user-table.tsx
Normal file
165
client/components/admin/user-table.tsx
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import { Button, Fieldset, Link, Popover, useToasts } from "@geist-ui/core"
|
||||||
|
import MoreVertical from "@geist-ui/icons/moreVertical"
|
||||||
|
import { User } from "@lib/types"
|
||||||
|
import Cookies from "js-cookie"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { adminFetcher } from "."
|
||||||
|
import Table from "rc-table"
|
||||||
|
|
||||||
|
const UserTable = () => {
|
||||||
|
const [users, setUsers] = useState<User[]>()
|
||||||
|
const { setToast } = useToasts()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
const res = await adminFetcher("/users")
|
||||||
|
const data = await res.json()
|
||||||
|
setUsers(data)
|
||||||
|
}
|
||||||
|
fetchUsers()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleRole = async (id: number, role: "admin" | "user") => {
|
||||||
|
const res = await adminFetcher("/users/toggle-role", {
|
||||||
|
method: "POST",
|
||||||
|
body: { id, role }
|
||||||
|
})
|
||||||
|
|
||||||
|
const json = await res.json()
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
setToast({
|
||||||
|
text: "Role updated",
|
||||||
|
type: "success"
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: swr should handle updating this
|
||||||
|
const newUsers = users?.map((user) => {
|
||||||
|
if (user.id === id.toString()) {
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
role: role === "admin" ? "user" : ("admin" as User["role"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
setUsers(newUsers)
|
||||||
|
} else {
|
||||||
|
setToast({
|
||||||
|
text: json.error || "Something went wrong",
|
||||||
|
type: "error"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteUser = async (id: number) => {
|
||||||
|
const confirm = window.confirm("Are you sure you want to delete this user?")
|
||||||
|
if (!confirm) return
|
||||||
|
const res = await adminFetcher(`/users/${id}`, {
|
||||||
|
method: "DELETE"
|
||||||
|
})
|
||||||
|
|
||||||
|
const json = await res.json()
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
setToast({
|
||||||
|
text: "User deleted",
|
||||||
|
type: "success"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setToast({
|
||||||
|
text: json.error || "Something went wrong",
|
||||||
|
type: "error"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableUsers = useMemo(
|
||||||
|
() =>
|
||||||
|
users?.map((user) => {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
posts: user.posts?.length || 0,
|
||||||
|
createdAt: `${new Date(
|
||||||
|
user.createdAt
|
||||||
|
).toLocaleDateString()} ${new Date(
|
||||||
|
user.createdAt
|
||||||
|
).toLocaleTimeString()}`,
|
||||||
|
role: user.role,
|
||||||
|
actions: ""
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[users]
|
||||||
|
)
|
||||||
|
|
||||||
|
const usernameColumns = [
|
||||||
|
{
|
||||||
|
title: "Username",
|
||||||
|
dataIndex: "username",
|
||||||
|
key: "username",
|
||||||
|
width: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Posts",
|
||||||
|
dataIndex: "posts",
|
||||||
|
key: "posts",
|
||||||
|
width: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Created",
|
||||||
|
dataIndex: "createdAt",
|
||||||
|
key: "createdAt",
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Role",
|
||||||
|
dataIndex: "role",
|
||||||
|
key: "role",
|
||||||
|
width: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Actions",
|
||||||
|
dataIndex: "",
|
||||||
|
key: "actions",
|
||||||
|
width: 50,
|
||||||
|
render(user: any) {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 100,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link href="#" onClick={() => toggleRole(user.id, user.role)}>
|
||||||
|
{user.role === "admin" ? "Change role" : "Make admin"}
|
||||||
|
</Link>
|
||||||
|
<Link href="#" onClick={() => deleteUser(user.id)}>
|
||||||
|
Delete user
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
hideArrow
|
||||||
|
>
|
||||||
|
<Button iconRight={<MoreVertical />} auto></Button>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fieldset>
|
||||||
|
<Fieldset.Title>Users</Fieldset.Title>
|
||||||
|
{users && <Fieldset.Subtitle>{users.length} users</Fieldset.Subtitle>}
|
||||||
|
{!users && <Fieldset.Subtitle>Loading...</Fieldset.Subtitle>}
|
||||||
|
{users && <Table columns={usernameColumns} data={tableUsers} />}
|
||||||
|
</Fieldset>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserTable
|
|
@ -9,6 +9,7 @@ import {
|
||||||
allowedFileNames,
|
allowedFileNames,
|
||||||
allowedFileExtensions
|
allowedFileExtensions
|
||||||
} from "@lib/constants"
|
} from "@lib/constants"
|
||||||
|
import byteToMB from "@lib/byte-to-mb"
|
||||||
|
|
||||||
function FileDropzone({ setDocs }: { setDocs: (docs: Document[]) => void }) {
|
function FileDropzone({ setDocs }: { setDocs: (docs: Document[]) => void }) {
|
||||||
const { palette } = useTheme()
|
const { palette } = useTheme()
|
||||||
|
@ -40,9 +41,6 @@ function FileDropzone({ setDocs }: { setDocs: (docs: Document[]) => void }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const validator = (file: File) => {
|
const validator = (file: File) => {
|
||||||
const byteToMB = (bytes: number) =>
|
|
||||||
Math.round((bytes / 1024 / 1024) * 100) / 100
|
|
||||||
|
|
||||||
// TODO: make this configurable
|
// TODO: make this configurable
|
||||||
const maxFileSize = 50000000
|
const maxFileSize = 50000000
|
||||||
if (file.size > maxFileSize) {
|
if (file.size > maxFileSize) {
|
||||||
|
|
|
@ -5,60 +5,60 @@ import { useRouter } from "next/router"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setPost: (post: Post) => void
|
setPost: (post: Post) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PasswordModalPage = ({ setPost }: Props) => {
|
const PasswordModalPage = ({ setPost }: Props) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true)
|
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true)
|
||||||
|
|
||||||
const onSubmit = async (password: string) => {
|
const onSubmit = async (password: string) => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/server-api/posts/authenticate?id=${router.query.id}&password=${password}`,
|
`/server-api/posts/authenticate?id=${router.query.id}&password=${password}`,
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
setToast({
|
setToast({
|
||||||
type: "error",
|
type: "error",
|
||||||
text: "Wrong password"
|
text: "Wrong password"
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data) {
|
if (data) {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
setToast({
|
setToast({
|
||||||
text: data.error,
|
text: data.error,
|
||||||
type: "error"
|
type: "error"
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
setIsPasswordModalOpen(false)
|
setIsPasswordModalOpen(false)
|
||||||
setPost(data)
|
setPost(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setIsPasswordModalOpen(false)
|
setIsPasswordModalOpen(false)
|
||||||
router.push("/")
|
router.push("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PasswordModal
|
<PasswordModal
|
||||||
creating={false}
|
creating={false}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
isOpen={isPasswordModalOpen}
|
isOpen={isPasswordModalOpen}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PasswordModalPage
|
export default PasswordModalPage
|
||||||
|
|
4
client/lib/byte-to-mb.ts
Normal file
4
client/lib/byte-to-mb.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
const byteToMB = (bytes: number) =>
|
||||||
|
Math.round((bytes / 1024 / 1024) * 100) / 100
|
||||||
|
|
||||||
|
export default byteToMB
|
|
@ -21,6 +21,7 @@
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"next": "12.1.5",
|
"next": "12.1.5",
|
||||||
"next-themes": "0.1.1",
|
"next-themes": "0.1.1",
|
||||||
|
"rc-table": "^7.24.1",
|
||||||
"react": "18.0.0",
|
"react": "18.0.0",
|
||||||
"react-datepicker": "4.7.0",
|
"react-datepicker": "4.7.0",
|
||||||
"react-dom": "18.0.0",
|
"react-dom": "18.0.0",
|
||||||
|
|
|
@ -19,7 +19,8 @@ class MyDocument extends Document {
|
||||||
{initialProps.styles}
|
{initialProps.styles}
|
||||||
{styles}
|
{styles}
|
||||||
</>
|
</>
|
||||||
) as any
|
) as // TODO: Investigate typescript
|
||||||
|
any
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,10 @@ export function middleware(req: NextRequest, event: NextFetchEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.includes("/protected/") || pathname.includes("/private/")) {
|
if (pathname.includes("/protected/") || pathname.includes("/private/")) {
|
||||||
const urlWithoutVisibility = pathname.replace("/protected/", "/").replace("/private/", "/").substring(1)
|
const urlWithoutVisibility = pathname
|
||||||
|
.replace("/protected/", "/")
|
||||||
|
.replace("/private/", "/")
|
||||||
|
.substring(1)
|
||||||
return NextResponse.redirect(getURL(urlWithoutVisibility))
|
return NextResponse.redirect(getURL(urlWithoutVisibility))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,8 +45,10 @@ export const getServerSideProps: GetServerSideProps = async ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await post.json() as Post
|
const json = (await post.json()) as Post
|
||||||
const isAuthor = json.users?.find(user => user.id === req.cookies["drift-userid"])
|
const isAuthor = json.users?.find(
|
||||||
|
(user) => user.id === req.cookies["drift-userid"]
|
||||||
|
)
|
||||||
|
|
||||||
if (json.visibility === "public" || json.visibility === "unlisted") {
|
if (json.visibility === "public" || json.visibility === "unlisted") {
|
||||||
const sMaxAge = 60 * 60 * 12 // half a day
|
const sMaxAge = 60 * 60 * 12 // half a day
|
||||||
|
@ -60,7 +62,7 @@ export const getServerSideProps: GetServerSideProps = async ({
|
||||||
post: {
|
post: {
|
||||||
id: json.id,
|
id: json.id,
|
||||||
visibility: json.visibility,
|
visibility: json.visibility,
|
||||||
expiresAt: json.expiresAt,
|
expiresAt: json.expiresAt
|
||||||
},
|
},
|
||||||
isProtected: true
|
isProtected: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
core-js-pure "^3.20.2"
|
core-js-pure "^3.20.2"
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7":
|
"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7":
|
||||||
version "7.17.9"
|
version "7.17.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
||||||
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
|
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
|
||||||
|
@ -552,7 +552,7 @@ chalk@^4.0.0, chalk@^4.1.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
classnames@^2.2.6:
|
classnames@^2.2.1, classnames@^2.2.5, classnames@^2.2.6:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||||
|
@ -2446,6 +2446,36 @@ queue-microtask@^1.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||||
|
|
||||||
|
rc-resize-observer@^1.1.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.2.0.tgz#9f46052f81cdf03498be35144cb7c53fd282c4c7"
|
||||||
|
integrity sha512-6W+UzT3PyDM0wVCEHfoW3qTHPTvbdSgiA43buiy8PzmeMnfgnDeb9NjdimMXMl3/TcrvvWl5RRVdp+NqcR47pQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.10.1"
|
||||||
|
classnames "^2.2.1"
|
||||||
|
rc-util "^5.15.0"
|
||||||
|
resize-observer-polyfill "^1.5.1"
|
||||||
|
|
||||||
|
rc-table@^7.24.1:
|
||||||
|
version "7.24.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.24.1.tgz#15ecabc9d69f8300b988caa52986e3b215150f2b"
|
||||||
|
integrity sha512-DRWpv5z5pmOaTmy5GqWoskeV1thaOu5HuD+2f61b/CkbBqlgJR3cygc5R/Qvd2uVW6pHU0lYulhmz0VLVFm+rw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.10.1"
|
||||||
|
classnames "^2.2.5"
|
||||||
|
rc-resize-observer "^1.1.0"
|
||||||
|
rc-util "^5.14.0"
|
||||||
|
shallowequal "^1.1.0"
|
||||||
|
|
||||||
|
rc-util@^5.14.0, rc-util@^5.15.0:
|
||||||
|
version "5.20.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.20.1.tgz#323590df56175f60b1a67d2ba76f04c3c2cb84cd"
|
||||||
|
integrity sha512-2IEyErPAYl0Up5gBu71e8IkOs+/SL9XRUvnGhtsr7IHlXLx2OsbQKTDpWacJbzLCmNcgJylDGj1kiklx+zagRA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.12.5"
|
||||||
|
react-is "^16.12.0"
|
||||||
|
shallowequal "^1.1.0"
|
||||||
|
|
||||||
rc@^1.2.7:
|
rc@^1.2.7:
|
||||||
version "1.2.8"
|
version "1.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||||
|
@ -2490,7 +2520,7 @@ react-fast-compare@^3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||||
|
|
||||||
react-is@^16.13.1:
|
react-is@^16.12.0, react-is@^16.13.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
@ -2590,6 +2620,11 @@ reserved-words@^0.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"
|
resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"
|
||||||
integrity sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=
|
integrity sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=
|
||||||
|
|
||||||
|
resize-observer-polyfill@^1.5.1:
|
||||||
|
version "1.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||||
|
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||||
|
|
||||||
resolve-dependency-path@^2.0.0:
|
resolve-dependency-path@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-2.0.0.tgz#11700e340717b865d216c66cabeb4a2a3c696736"
|
resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-2.0.0.tgz#11700e340717b865d216c66cabeb4a2a3c696736"
|
||||||
|
@ -2709,6 +2744,11 @@ semver@^7.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^6.0.0"
|
lru-cache "^6.0.0"
|
||||||
|
|
||||||
|
shallowequal@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
|
||||||
|
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
|
||||||
|
|
||||||
shebang-command@^2.0.0:
|
shebang-command@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import isAdmin from "@lib/middleware/is-admin"
|
import isAdmin, { UserJwtRequest } from "@lib/middleware/is-admin"
|
||||||
import { Post } from "@lib/models/Post"
|
import { Post } from "@lib/models/Post"
|
||||||
import { User } from "@lib/models/User"
|
import { User } from "@lib/models/User"
|
||||||
import { File } from "@lib/models/File"
|
import { File } from "@lib/models/File"
|
||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
|
import { celebrate, Joi } from "celebrate"
|
||||||
|
|
||||||
export const admin = Router()
|
export const admin = Router()
|
||||||
|
|
||||||
|
@ -35,6 +36,81 @@ admin.get("/users", async (req, res, next) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
admin.post(
|
||||||
|
"/users/toggle-role",
|
||||||
|
celebrate({
|
||||||
|
body: {
|
||||||
|
id: Joi.string().required(),
|
||||||
|
role: Joi.string().required().allow("user", "admin")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
async (req: UserJwtRequest, res, next) => {
|
||||||
|
try {
|
||||||
|
const { id, role } = req.body
|
||||||
|
if (req.user?.id === id) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "You can't change your own role"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findByPk(id)
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "User not found"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.update({
|
||||||
|
role
|
||||||
|
})
|
||||||
|
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
next(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
admin.delete("/users/:id", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.params.id)
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: "User not found"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await user.destroy()
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
next(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// admin.delete("/posts/:id", async (req, res, next) => {
|
||||||
|
// try {
|
||||||
|
// const post = await Post.findByPk(req.params.id)
|
||||||
|
// if (!post) {
|
||||||
|
// return res.status(404).json({
|
||||||
|
// error: "Post not found"
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// await post.destroy()
|
||||||
|
|
||||||
|
// res.json({
|
||||||
|
// success: true
|
||||||
|
// })
|
||||||
|
// } catch (e) {
|
||||||
|
// next(e)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
admin.get("/posts", async (req, res, next) => {
|
admin.get("/posts", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const posts = await Post.findAll({
|
const posts = await Post.findAll({
|
||||||
|
|
|
@ -243,14 +243,7 @@ const fullPostSequelizeOptions = {
|
||||||
{
|
{
|
||||||
model: File,
|
model: File,
|
||||||
as: "files",
|
as: "files",
|
||||||
attributes: [
|
attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt"]
|
||||||
"id",
|
|
||||||
"title",
|
|
||||||
"content",
|
|
||||||
"sha",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: User,
|
model: User,
|
||||||
|
@ -270,11 +263,12 @@ const fullPostSequelizeOptions = {
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
"deletedAt",
|
"deletedAt",
|
||||||
"expiresAt",
|
"expiresAt"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
posts.get("/authenticate",
|
posts.get(
|
||||||
|
"/authenticate",
|
||||||
celebrate({
|
celebrate({
|
||||||
query: {
|
query: {
|
||||||
id: Joi.string().required(),
|
id: Joi.string().required(),
|
||||||
|
@ -286,10 +280,7 @@ posts.get("/authenticate",
|
||||||
|
|
||||||
const post = await Post.findByPk(id?.toString(), {
|
const post = await Post.findByPk(id?.toString(), {
|
||||||
...fullPostSequelizeOptions,
|
...fullPostSequelizeOptions,
|
||||||
attributes: [
|
attributes: [...fullPostSequelizeOptions.attributes, "password"]
|
||||||
...fullPostSequelizeOptions.attributes,
|
|
||||||
"password"
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const hash = crypto
|
const hash = crypto
|
||||||
|
@ -306,7 +297,6 @@ posts.get("/authenticate",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
posts.get(
|
posts.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
secretKey,
|
secretKey,
|
||||||
|
|
Loading…
Reference in a new issue