Add deleting users to admin, refactor auth
This commit is contained in:
parent
65cf59e96b
commit
447974a74a
20 changed files with 534 additions and 280 deletions
|
@ -1,26 +1,80 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { startTransition, useEffect, useRef, useState } from "react"
|
||||
import styles from "./auth.module.css"
|
||||
import Link from "../../components/link"
|
||||
import { signIn } from "next-auth/react"
|
||||
import { getSession, signIn } from "next-auth/react"
|
||||
import Input from "@components/input"
|
||||
import Button from "@components/button"
|
||||
import Note from "@components/note"
|
||||
import { GitHub } from "react-feather"
|
||||
import { useToasts } from "@components/toasts"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import Note from "@components/note"
|
||||
const Auth = ({
|
||||
page,
|
||||
requiresServerPassword
|
||||
requiresServerPassword,
|
||||
isGithubEnabled
|
||||
}: {
|
||||
page: "signup" | "signin"
|
||||
requiresServerPassword?: boolean
|
||||
isGithubEnabled?: boolean
|
||||
}) => {
|
||||
const [serverPassword, setServerPassword] = useState("")
|
||||
const [errorMsg, setErrorMsg] = useState("")
|
||||
const { setToast } = useToasts()
|
||||
const signingIn = page === "signin"
|
||||
const router = useRouter()
|
||||
const signText = signingIn ? "In" : "Up"
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const queryParams = useSearchParams()
|
||||
|
||||
useEffect(() => {
|
||||
if (queryParams.get("error")) {
|
||||
setToast({
|
||||
message: queryParams.get("error") as string,
|
||||
type: "error"
|
||||
})
|
||||
}
|
||||
}, [queryParams, setToast])
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
|
||||
const res = await signIn("credentials", {
|
||||
username,
|
||||
password,
|
||||
registration_password: serverPassword,
|
||||
redirect: false,
|
||||
// callbackUrl: "/signin",
|
||||
signingIn: signingIn
|
||||
})
|
||||
if (res?.error) {
|
||||
setToast({
|
||||
type: "error",
|
||||
message: res.error
|
||||
})
|
||||
} else {
|
||||
console.log("res", res)
|
||||
startTransition(() => {
|
||||
router.push("/new")
|
||||
router.refresh()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleChangeUsername = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUsername(event.target.value)
|
||||
}
|
||||
|
||||
const handleChangePassword = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPassword(event.target.value)
|
||||
}
|
||||
|
||||
const handleChangeServerPassword = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setServerPassword(event.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
|
@ -28,16 +82,37 @@ const Auth = ({
|
|||
<div className={styles.formContentSpace}>
|
||||
<h1>Sign {signText}</h1>
|
||||
</div>
|
||||
{/* <form onSubmit={handleSubmit}> */}
|
||||
<form>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.formGroup}>
|
||||
{requiresServerPassword ? (
|
||||
<>
|
||||
{" "}
|
||||
<Note type="info">
|
||||
The server administrator has set a password for this server.
|
||||
</Note>
|
||||
<Input
|
||||
type="password"
|
||||
id="server-password"
|
||||
value={serverPassword}
|
||||
onChange={(event) =>
|
||||
setServerPassword(event.currentTarget.value)
|
||||
}
|
||||
placeholder="Server Password"
|
||||
required={true}
|
||||
width="100%"
|
||||
aria-label="Server Password"
|
||||
/>
|
||||
<hr style={{ width: "100%" }} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.currentTarget.value)}
|
||||
onChange={handleChangeUsername}
|
||||
placeholder="Username"
|
||||
required
|
||||
required={true}
|
||||
minLength={3}
|
||||
width="100%"
|
||||
aria-label="Username"
|
||||
|
@ -46,59 +121,37 @@ const Auth = ({
|
|||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.currentTarget.value)}
|
||||
onChange={handleChangePassword}
|
||||
placeholder="Password"
|
||||
required
|
||||
required={true}
|
||||
minLength={6}
|
||||
width="100%"
|
||||
aria-label="Password"
|
||||
/>
|
||||
{requiresServerPassword && (
|
||||
<Input
|
||||
type="password"
|
||||
id="server-password"
|
||||
value={serverPassword}
|
||||
onChange={(event) =>
|
||||
setServerPassword(event.currentTarget.value)
|
||||
}
|
||||
placeholder="Server Password"
|
||||
required
|
||||
width="100%"
|
||||
aria-label="Server Password"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
width={"100%"}
|
||||
type="submit"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn("credentials", {
|
||||
username,
|
||||
password,
|
||||
callbackUrl: "/"
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Button width={"100%"} type="submit">
|
||||
Sign {signText}
|
||||
</Button>
|
||||
<hr style={{ width: "100%" }} />
|
||||
<Button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
width="100%"
|
||||
style={{
|
||||
color: "var(--fg)"
|
||||
}}
|
||||
iconLeft={<GitHub />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn("github", {
|
||||
callbackUrl: "/"
|
||||
})
|
||||
}}
|
||||
>
|
||||
Sign {signText.toLowerCase()} with GitHub
|
||||
</Button>
|
||||
{isGithubEnabled ? <hr style={{ width: "100%" }} /> : null}
|
||||
{isGithubEnabled ? (
|
||||
<Button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
width="100%"
|
||||
style={{
|
||||
color: "var(--fg)"
|
||||
}}
|
||||
iconLeft={<GitHub />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn("github", {
|
||||
callbackUrl: "/",
|
||||
registration_password: serverPassword
|
||||
})
|
||||
}}
|
||||
>
|
||||
Sign {signText.toLowerCase()} with GitHub
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.formContentSpace}>
|
||||
{signingIn ? (
|
||||
|
@ -117,7 +170,6 @@ const Auth = ({
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
{errorMsg && <Note type="error">{errorMsg}</Note>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import config from "@lib/config"
|
||||
import Auth from "../components"
|
||||
|
||||
export default function SignInPage() {
|
||||
return <Auth page="signin" />
|
||||
export function isGithubEnabled() {
|
||||
return config.github_client_id.length && config.github_client_secret.length ? true : false
|
||||
}
|
||||
|
||||
export default function SignInPage() {
|
||||
return <Auth page="signin" isGithubEnabled={isGithubEnabled()} />
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Auth from "../components"
|
||||
import { getRequiresPasscode } from "pages/api/auth/requires-passcode"
|
||||
import { isGithubEnabled } from "../signin/page"
|
||||
|
||||
const getPasscode = async () => {
|
||||
return await getRequiresPasscode()
|
||||
|
@ -7,5 +8,5 @@ const getPasscode = async () => {
|
|||
|
||||
export default async function SignUpPage() {
|
||||
const requiresPasscode = await getPasscode()
|
||||
return <Auth page="signup" requiresServerPassword={requiresPasscode} />
|
||||
return <Auth page="signup" requiresServerPassword={requiresPasscode} isGithubEnabled={isGithubEnabled()} />
|
||||
}
|
||||
|
|
|
@ -187,7 +187,7 @@ const Post = ({
|
|||
)
|
||||
|
||||
if (session.status === "unauthenticated") {
|
||||
router.push("/login")
|
||||
router.push("/signin")
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
.table {
|
||||
width: 100%;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table .id {
|
||||
width: 10ch;
|
||||
}
|
17
src/app/admin/components/table.module.css
Normal file
17
src/app/admin/components/table.module.css
Normal file
|
@ -0,0 +1,17 @@
|
|||
.table {
|
||||
width: 100%;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.id {
|
||||
width: 130px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
138
src/app/admin/components/tables.tsx
Normal file
138
src/app/admin/components/tables.tsx
Normal file
|
@ -0,0 +1,138 @@
|
|||
"use client"
|
||||
|
||||
import Button from "@components/button"
|
||||
import ButtonDropdown from "@components/button-dropdown"
|
||||
import { Spinner } from "@components/spinner"
|
||||
import { useToasts } from "@components/toasts"
|
||||
import { Post, User } from "@lib/server/prisma"
|
||||
import { useState } from "react"
|
||||
import styles from "./table.module.css"
|
||||
|
||||
export function UserTable({
|
||||
users: initialUsers
|
||||
}: {
|
||||
users?: {
|
||||
createdAt: string
|
||||
posts?: Post[]
|
||||
id: string
|
||||
email: string | null
|
||||
role: string | null
|
||||
displayName: string | null
|
||||
|
||||
}[]
|
||||
}) {
|
||||
const { setToast } = useToasts()
|
||||
const [users, setUsers] = useState<typeof initialUsers>(initialUsers)
|
||||
|
||||
const deleteUser = async (id: string) => {
|
||||
try {
|
||||
const res = await fetch("/api/admin?action=delete-user", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: id
|
||||
})
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
setToast({
|
||||
message: "User deleted",
|
||||
type: "success"
|
||||
})
|
||||
setUsers(users?.filter((user) => user.id !== id))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setToast({
|
||||
message: "Error deleting user",
|
||||
type: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>User ID</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!users ? (
|
||||
<tr>
|
||||
<td colSpan={4}>
|
||||
<Spinner />
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{users?.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>{user.displayName ? user.displayName : "no name"}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>{user.role}</td>
|
||||
<td className={styles.id} title={user.id}>
|
||||
{user.id}
|
||||
</td>
|
||||
<td>
|
||||
<Button onClick={() => deleteUser(user.id)}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
export function PostTable({
|
||||
posts
|
||||
}: {
|
||||
posts?: {
|
||||
createdAt: string
|
||||
id: string
|
||||
author?: User | null
|
||||
title: string
|
||||
visibility: string
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Author</th>
|
||||
<th>Created</th>
|
||||
<th>Visibility</th>
|
||||
<th className={styles.id}>Post ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!posts ? (
|
||||
<tr>
|
||||
<td colSpan={5}>
|
||||
<Spinner />
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
{posts?.map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td>
|
||||
<a href={`/post/${post.id}`} target="_blank" rel="noreferrer">
|
||||
{post.title}
|
||||
</a>
|
||||
</td>
|
||||
<td>{"author" in post ? post.author?.name : "no author"}</td>
|
||||
<td>{new Date(post.createdAt).toLocaleDateString()}</td>
|
||||
<td>{post.visibility}</td>
|
||||
<td>{post.id}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { PostTable, UserTable } from "./page"
|
||||
import { PostTable, UserTable } from "./components/tables"
|
||||
|
||||
export default function AdminLoading() {
|
||||
return (
|
||||
|
|
|
@ -1,100 +1,55 @@
|
|||
import { getAllPosts, getAllUsers } from "@lib/server/prisma"
|
||||
import { Spinner } from "@components/spinner"
|
||||
import styles from "./admin.module.css"
|
||||
|
||||
export function UserTable({
|
||||
users
|
||||
}: {
|
||||
users?: Awaited<ReturnType<typeof getAllUsers>>
|
||||
}) {
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>User ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users?.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>{user.displayName ? user.displayName : "no name"}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>{user.role}</td>
|
||||
<td className={styles.id}>{user.id}</td>
|
||||
</tr>
|
||||
))}
|
||||
{!users && (
|
||||
<tr>
|
||||
<td colSpan={4}>
|
||||
<Spinner />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
export function PostTable({
|
||||
posts
|
||||
}: {
|
||||
posts?: Awaited<ReturnType<typeof getAllPosts>>
|
||||
}) {
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Author</th>
|
||||
<th>Created</th>
|
||||
<th>Visibility</th>
|
||||
<th className={styles.id}>Post ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{posts?.map((post) => (
|
||||
<tr key={post.id}>
|
||||
<td>
|
||||
<a href={`/post/${post.id}`} target="_blank" rel="noreferrer">
|
||||
{post.title}
|
||||
</a>
|
||||
</td>
|
||||
<td>{"author" in post ? post.author?.name : "no author"}</td>
|
||||
<td>{post.createdAt.toLocaleDateString()}</td>
|
||||
<td>{post.visibility}</td>
|
||||
<td>{post.id}</td>
|
||||
</tr>
|
||||
))}
|
||||
{!posts && (
|
||||
<tr>
|
||||
<td colSpan={5}>
|
||||
<Spinner />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
import { getAllPosts, getAllUsers } from "@lib/server/prisma"
|
||||
import { PostTable, UserTable } from "./components/tables"
|
||||
|
||||
export default async function AdminPage() {
|
||||
const usersPromise = getAllUsers()
|
||||
const usersPromise = getAllUsers({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
createdAt: true
|
||||
}
|
||||
})
|
||||
const postsPromise = getAllPosts({
|
||||
withAuthor: true
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
author: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const [users, posts] = await Promise.all([usersPromise, postsPromise])
|
||||
|
||||
const serializedPosts = posts.map((post) => {
|
||||
return {
|
||||
...post,
|
||||
createdAt: post.createdAt.toISOString(),
|
||||
updatedAt: post.updatedAt.toISOString(),
|
||||
expiresAt: post.expiresAt?.toISOString(),
|
||||
deletedAt: post.deletedAt?.toISOString()
|
||||
}
|
||||
})
|
||||
|
||||
const serializedUsers = users.map((user) => {
|
||||
return {
|
||||
...user,
|
||||
createdAt: user.createdAt.toISOString()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Admin</h1>
|
||||
<h2>Users</h2>
|
||||
<UserTable users={users} />
|
||||
<UserTable users={serializedUsers} />
|
||||
<h2>Posts</h2>
|
||||
<PostTable posts={posts} />
|
||||
<PostTable posts={serializedPosts} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ type Tab = {
|
|||
|
||||
const Header = () => {
|
||||
const session = useSession()
|
||||
console.log("session", session)
|
||||
const isSignedIn = session?.status === "authenticated"
|
||||
const isAdmin = session?.data?.user?.role === "admin"
|
||||
const isLoading = session?.status === "loading"
|
||||
|
|
|
@ -40,7 +40,7 @@ type InputProps = Omit<Props, "onChange" | "value" | "label" | "aria-label"> &
|
|||
)
|
||||
// eslint-disable-next-line react/display-name
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, className, width, height, labelClassName, ...props }, ref) => {
|
||||
({ label, className, required, width, height, labelClassName, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.wrapper}
|
||||
|
|
|
@ -13,9 +13,24 @@ const getWelcomeData = async () => {
|
|||
export default async function Page() {
|
||||
const { content, rendered, title } = await getWelcomeData()
|
||||
const getPostsPromise = getAllPosts({
|
||||
where: { visibility: "public" },
|
||||
include: {
|
||||
files: true
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
author: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
deletedAt: null,
|
||||
expiresAt: {
|
||||
gt: new Date()
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
type Config = {
|
||||
is_production: boolean
|
||||
enable_admin: boolean
|
||||
|
@ -9,6 +8,7 @@ type Config = {
|
|||
github_client_id: string
|
||||
github_client_secret: string
|
||||
nextauth_secret: string
|
||||
credential_auth: boolean
|
||||
}
|
||||
|
||||
type EnvironmentValue = string | undefined
|
||||
|
@ -77,6 +77,9 @@ export const config = (env: Environment): Config => {
|
|||
github_client_id: env.GITHUB_CLIENT_ID ?? "",
|
||||
github_client_secret: env.GITHUB_CLIENT_SECRET ?? "",
|
||||
nextauth_secret: throwIfUndefined("NEXTAUTH_SECRET"),
|
||||
credential_auth: stringToBoolean(
|
||||
developmentDefault("CREDENTIAL_AUTH", "true")
|
||||
)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
|
|
@ -5,83 +5,140 @@ import CredentialsProvider from "next-auth/providers/credentials"
|
|||
import { prisma } from "@lib/server/prisma"
|
||||
import config from "@lib/config"
|
||||
import * as crypto from "crypto"
|
||||
import { Provider } from "next-auth/providers"
|
||||
|
||||
const providers: NextAuthOptions["providers"] = [
|
||||
GitHubProvider({
|
||||
clientId: config.github_client_id,
|
||||
clientSecret: config.github_client_secret
|
||||
}),
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
||||
password: { label: "Password", type: "password" }
|
||||
const credentialsOptions = () => {
|
||||
const options: Record<string, any> = {
|
||||
username: {
|
||||
label: "Username",
|
||||
required: true,
|
||||
type: "text"
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials) {
|
||||
return null
|
||||
}
|
||||
password: {
|
||||
label: "Password",
|
||||
required: true,
|
||||
type: "password"
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: credentials.username
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
displayName: true,
|
||||
role: true,
|
||||
password: true
|
||||
if (config.registration_password) {
|
||||
options["registration_password"] = {
|
||||
label: "Server Password",
|
||||
type: "password",
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
const providers = () => {
|
||||
const providers = []
|
||||
|
||||
if (config.github_client_id && config.github_client_secret) {
|
||||
providers.push(
|
||||
GitHubProvider({
|
||||
clientId: config.github_client_id,
|
||||
clientSecret: config.github_client_secret
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (config.credential_auth) {
|
||||
providers.push(
|
||||
CredentialsProvider({
|
||||
name: "Drift",
|
||||
credentials: credentialsOptions(),
|
||||
async authorize(credentials) {
|
||||
if (!credentials || !credentials.username || !credentials.password) {
|
||||
throw new Error("Missing credentials")
|
||||
}
|
||||
|
||||
if (credentials.username.length < 3) {
|
||||
throw new Error("Username must be at least 3 characters")
|
||||
}
|
||||
|
||||
if (credentials.password.length < 3) {
|
||||
throw new Error("Password must be at least 3 characters")
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: credentials.username
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
displayName: true,
|
||||
role: true,
|
||||
password: true
|
||||
}
|
||||
})
|
||||
|
||||
const hashedPassword = crypto
|
||||
.createHash("sha256")
|
||||
.update(credentials.password + config.nextauth_secret)
|
||||
.digest("hex")
|
||||
|
||||
if (credentials.signingIn === "true") {
|
||||
if (
|
||||
user?.password &&
|
||||
crypto.timingSafeEqual(
|
||||
Buffer.from(user.password),
|
||||
Buffer.from(hashedPassword)
|
||||
)
|
||||
) {
|
||||
return user
|
||||
} else {
|
||||
throw new Error("Incorrect username or password")
|
||||
}
|
||||
} else {
|
||||
if (config.registration_password) {
|
||||
if (!credentials.registration_password) {
|
||||
throw new Error("Missing registration password")
|
||||
}
|
||||
|
||||
if (
|
||||
credentials.registration_password !==
|
||||
config.registration_password
|
||||
) {
|
||||
throw new Error("Incorrect registration password")
|
||||
}
|
||||
}
|
||||
|
||||
if (user) {
|
||||
throw new Error("Username already taken")
|
||||
}
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
username: credentials.username,
|
||||
displayName: credentials.username,
|
||||
role: "user",
|
||||
password: hashedPassword,
|
||||
name: credentials.username
|
||||
}
|
||||
})
|
||||
|
||||
return newUser
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const hashedPassword = crypto
|
||||
.createHash("sha256")
|
||||
.update
|
||||
(credentials
|
||||
.password
|
||||
+ config.nextauth_secret)
|
||||
.digest("hex")
|
||||
|
||||
if (!user) {
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
username: credentials.username,
|
||||
displayName: credentials.username,
|
||||
role: "user",
|
||||
password: hashedPassword,
|
||||
name: credentials.username,
|
||||
}
|
||||
})
|
||||
|
||||
return newUser
|
||||
} else if (
|
||||
user.password &&
|
||||
crypto.timingSafeEqual(
|
||||
Buffer.from(user.password),
|
||||
Buffer.from(hashedPassword)
|
||||
)
|
||||
) {
|
||||
return user
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
})
|
||||
]
|
||||
return providers
|
||||
}
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
// see https://github.com/prisma/prisma/issues/16117 / https://github.com/shadcn/taxonomy
|
||||
adapter: PrismaAdapter(prisma as any),
|
||||
adapter: PrismaAdapter(prisma),
|
||||
session: {
|
||||
strategy: "jwt"
|
||||
},
|
||||
pages: {
|
||||
signIn: "/signin"
|
||||
// TODO
|
||||
// error: "/auth/error",
|
||||
signIn: "/signin",
|
||||
error: "/signin"
|
||||
},
|
||||
providers,
|
||||
providers: providers(),
|
||||
callbacks: {
|
||||
async session({ token, session }) {
|
||||
if (token) {
|
||||
|
@ -94,6 +151,7 @@ export const authOptions: NextAuthOptions = {
|
|||
|
||||
return session
|
||||
},
|
||||
|
||||
async jwt({ token, user }) {
|
||||
const dbUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
|
|
|
@ -57,7 +57,9 @@ const postWithFilesAndAuthor = Prisma.validator<Prisma.PostArgs>()({
|
|||
|
||||
export type ServerPostWithFiles = Prisma.PostGetPayload<typeof postWithFiles>
|
||||
export type PostWithAuthor = Prisma.PostGetPayload<typeof postWithAuthor>
|
||||
export type ServerPostWithFilesAndAuthor = Prisma.PostGetPayload<typeof postWithFilesAndAuthor>
|
||||
export type ServerPostWithFilesAndAuthor = Prisma.PostGetPayload<
|
||||
typeof postWithFilesAndAuthor
|
||||
>
|
||||
|
||||
export type PostWithFiles = Omit<ServerPostWithFiles, "files"> & {
|
||||
files: (Omit<ServerPostWithFiles["files"][number], "content" | "html"> & {
|
||||
|
@ -70,7 +72,10 @@ export type PostWithFilesAndAuthor = Omit<
|
|||
ServerPostWithFilesAndAuthor,
|
||||
"files"
|
||||
> & {
|
||||
files: (Omit<ServerPostWithFilesAndAuthor["files"][number], "content" | "html"> & {
|
||||
files: (Omit<
|
||||
ServerPostWithFilesAndAuthor["files"][number],
|
||||
"content" | "html"
|
||||
> & {
|
||||
content: string
|
||||
html: string
|
||||
})[]
|
||||
|
@ -201,40 +206,25 @@ export const getPostById = async (
|
|||
return post
|
||||
}
|
||||
|
||||
export const getAllPosts = async ({
|
||||
withFiles = false,
|
||||
withAuthor = false,
|
||||
take = 100,
|
||||
...rest
|
||||
}: {
|
||||
withFiles?: boolean
|
||||
withAuthor?: boolean
|
||||
} & Prisma.PostFindManyArgs = {}): Promise<
|
||||
Post[] | ServerPostWithFiles[] | ServerPostWithFilesAndAuthor[]
|
||||
> => {
|
||||
const posts = await prisma.post.findMany({
|
||||
include: {
|
||||
files: withFiles,
|
||||
author: withAuthor
|
||||
},
|
||||
// TODO: optimize which to grab
|
||||
take,
|
||||
...rest
|
||||
})
|
||||
|
||||
return posts as typeof withFiles extends true
|
||||
? typeof withAuthor extends true
|
||||
? PostWithFilesAndAuthor[]
|
||||
: PostWithFiles[]
|
||||
: Post[]
|
||||
export const getAllPosts = async (
|
||||
options?: Prisma.PostFindManyArgs
|
||||
): Promise<Post[] | ServerPostWithFiles[] | ServerPostWithFilesAndAuthor[]> => {
|
||||
const posts = await prisma.post.findMany(options)
|
||||
return posts
|
||||
}
|
||||
|
||||
export type UserWithPosts = User & {
|
||||
posts: Post[]
|
||||
}
|
||||
export const userWithPosts = Prisma.validator<Prisma.UserArgs>()({
|
||||
include: {
|
||||
posts: true
|
||||
}
|
||||
})
|
||||
|
||||
export const getAllUsers = async () => {
|
||||
const users = await prisma.user.findMany({
|
||||
export type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
|
||||
|
||||
export const getAllUsers = async (
|
||||
options?: Prisma.UserFindManyArgs
|
||||
): Promise<User[] | UserWithPosts[]> => {
|
||||
const users = (await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
|
@ -242,8 +232,9 @@ export const getAllUsers = async () => {
|
|||
displayName: true,
|
||||
posts: true,
|
||||
createdAt: true
|
||||
}
|
||||
})
|
||||
},
|
||||
...options
|
||||
})) as User[] | UserWithPosts[]
|
||||
|
||||
return users
|
||||
}
|
||||
|
@ -265,7 +256,7 @@ export const searchPosts = async (
|
|||
OR: [
|
||||
{
|
||||
title: {
|
||||
search: query,
|
||||
search: query
|
||||
},
|
||||
authorId: userId,
|
||||
visibility: publicOnly ? "public" : undefined
|
||||
|
@ -275,7 +266,7 @@ export const searchPosts = async (
|
|||
some: {
|
||||
content: {
|
||||
in: [Buffer.from(query)]
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
visibility: publicOnly ? "public" : undefined
|
||||
|
|
|
@ -6,10 +6,10 @@ const nextConfig = {
|
|||
experimental: {
|
||||
// esmExternals: true,
|
||||
appDir: true,
|
||||
serverComponentsExternalPackages: ['prisma'],
|
||||
serverComponentsExternalPackages: ["prisma", "@prisma/client"],
|
||||
},
|
||||
output: "standalone",
|
||||
async rewrites() {
|
||||
rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/file/raw/:id",
|
||||
|
@ -21,7 +21,6 @@ const nextConfig = {
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"jest": "^29.3.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "13.0.8-canary.0",
|
||||
"next-auth": "^4.18.4",
|
||||
"next-auth": "^4.18.6",
|
||||
"prisma": "^4.7.1",
|
||||
"react": "18.2.0",
|
||||
"react-datepicker": "4.8.0",
|
||||
|
|
|
@ -3,6 +3,7 @@ import { parseQueryParam } from "@lib/server/parse-query-param"
|
|||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { prisma } from "lib/server/prisma"
|
||||
import { getSession } from "next-auth/react"
|
||||
import { deleteUser } from "../user/[id]"
|
||||
|
||||
const actions = [
|
||||
"user",
|
||||
|
@ -94,11 +95,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
return res.status(400).json({ error: "Invalid request" })
|
||||
}
|
||||
|
||||
const user = await prisma.user.delete({
|
||||
where: { id: userId }
|
||||
})
|
||||
await deleteUser(userId)
|
||||
|
||||
return res.status(200).json(user)
|
||||
return res.status(200).send("User deleted")
|
||||
case "delete-post":
|
||||
const { postId } = req.body
|
||||
if (!postId) {
|
||||
|
|
|
@ -42,9 +42,42 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
})
|
||||
case "GET":
|
||||
return res.json(currUser)
|
||||
case "DELETE":
|
||||
if (currUser?.role !== "admin" && currUser?.id !== id) {
|
||||
return res.status(403).json({ message: "Unauthorized" })
|
||||
}
|
||||
|
||||
await deleteUser(id)
|
||||
|
||||
default:
|
||||
return res.status(405).json({ message: "Method not allowed" })
|
||||
}
|
||||
}
|
||||
|
||||
export default withMethods(["GET", "PUT"], handler)
|
||||
export default withMethods(["GET", "PUT", "DELETE"], handler)
|
||||
|
||||
// valid jsdoc
|
||||
/**
|
||||
* @description Deletes a user and all of their posts, files, and accounts
|
||||
* @warning This function does not perform any authorization checks
|
||||
*/
|
||||
export async function deleteUser(id: string | undefined) {
|
||||
|
||||
// first delete all of the user's posts
|
||||
await prisma.post.deleteMany({
|
||||
where: {
|
||||
authorId: id
|
||||
}
|
||||
})
|
||||
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
include: {
|
||||
posts: true,
|
||||
accounts: true,
|
||||
sessions: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ specifiers:
|
|||
jest: ^29.3.1
|
||||
lodash.debounce: ^4.0.8
|
||||
next: 13.0.8-canary.0
|
||||
next-auth: ^4.18.4
|
||||
next-auth: ^4.18.6
|
||||
next-unused: 0.0.6
|
||||
prettier: 2.6.2
|
||||
prisma: ^4.7.1
|
||||
|
@ -46,7 +46,7 @@ specifiers:
|
|||
typescript-plugin-css-modules: 3.4.0
|
||||
|
||||
dependencies:
|
||||
'@next-auth/prisma-adapter': 1.0.5_4eojhct6t46nl4awizrjr4dkya
|
||||
'@next-auth/prisma-adapter': 1.0.5_64qbzg5ec56bux2misz3l4u6g4
|
||||
'@next/eslint-plugin-next': 13.0.7-canary.4
|
||||
'@prisma/client': 4.7.1_prisma@4.7.1
|
||||
'@radix-ui/react-dialog': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u
|
||||
|
@ -61,7 +61,7 @@ dependencies:
|
|||
jest: 29.3.1_@types+node@17.0.23
|
||||
lodash.debounce: 4.0.8
|
||||
next: 13.0.8-canary.0_biqbaboplfbrettd7655fr4n2y
|
||||
next-auth: 4.18.4_rhfownvlqkszea7w3lnpwl7bzy
|
||||
next-auth: 4.18.6_rhfownvlqkszea7w3lnpwl7bzy
|
||||
prisma: 4.7.1
|
||||
react: 18.2.0
|
||||
react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y
|
||||
|
@ -773,14 +773,14 @@ packages:
|
|||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
dev: false
|
||||
|
||||
/@next-auth/prisma-adapter/1.0.5_4eojhct6t46nl4awizrjr4dkya:
|
||||
/@next-auth/prisma-adapter/1.0.5_64qbzg5ec56bux2misz3l4u6g4:
|
||||
resolution: {integrity: sha512-VqMS11IxPXrPGXw6Oul6jcyS/n8GLOWzRMrPr3EMdtD6eOalM6zz05j08PcNiis8QzkfuYnCv49OvufTuaEwYQ==}
|
||||
peerDependencies:
|
||||
'@prisma/client': '>=2.26.0 || >=3'
|
||||
next-auth: ^4
|
||||
dependencies:
|
||||
'@prisma/client': 4.7.1_prisma@4.7.1
|
||||
next-auth: 4.18.4_rhfownvlqkszea7w3lnpwl7bzy
|
||||
next-auth: 4.18.6_rhfownvlqkszea7w3lnpwl7bzy
|
||||
dev: false
|
||||
|
||||
/@next/bundle-analyzer/13.0.7-canary.4:
|
||||
|
@ -5105,8 +5105,8 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/next-auth/4.18.4_rhfownvlqkszea7w3lnpwl7bzy:
|
||||
resolution: {integrity: sha512-tvXOabxv5U/y6ib56XPkOnc/48tYc+xT6GNOLREIme8WVGYHDTc3CGEfe2+0bVCWAm0ax/GYXH0By5NFoaJDww==}
|
||||
/next-auth/4.18.6_rhfownvlqkszea7w3lnpwl7bzy:
|
||||
resolution: {integrity: sha512-0TQwbq5X9Jyd1wUVYUoyvHJh4JWXeW9UOcMEl245Er/Y5vsSbyGJHt8M7xjRMzk9mORVMYehoMdERgyiq/jCgA==}
|
||||
engines: {node: ^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0}
|
||||
peerDependencies:
|
||||
next: ^12.2.5 || ^13
|
||||
|
|
Loading…
Reference in a new issue