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"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { startTransition, useEffect, useRef, useState } from "react"
|
||||||
import styles from "./auth.module.css"
|
import styles from "./auth.module.css"
|
||||||
import Link from "../../components/link"
|
import Link from "../../components/link"
|
||||||
import { signIn } from "next-auth/react"
|
import { getSession, signIn } from "next-auth/react"
|
||||||
import Input from "@components/input"
|
import Input from "@components/input"
|
||||||
import Button from "@components/button"
|
import Button from "@components/button"
|
||||||
import Note from "@components/note"
|
|
||||||
import { GitHub } from "react-feather"
|
import { GitHub } from "react-feather"
|
||||||
|
import { useToasts } from "@components/toasts"
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import Note from "@components/note"
|
||||||
const Auth = ({
|
const Auth = ({
|
||||||
page,
|
page,
|
||||||
requiresServerPassword
|
requiresServerPassword,
|
||||||
|
isGithubEnabled
|
||||||
}: {
|
}: {
|
||||||
page: "signup" | "signin"
|
page: "signup" | "signin"
|
||||||
requiresServerPassword?: boolean
|
requiresServerPassword?: boolean
|
||||||
|
isGithubEnabled?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const [serverPassword, setServerPassword] = useState("")
|
const [serverPassword, setServerPassword] = useState("")
|
||||||
const [errorMsg, setErrorMsg] = useState("")
|
const { setToast } = useToasts()
|
||||||
const signingIn = page === "signin"
|
const signingIn = page === "signin"
|
||||||
|
const router = useRouter()
|
||||||
const signText = signingIn ? "In" : "Up"
|
const signText = signingIn ? "In" : "Up"
|
||||||
const [username, setUsername] = useState("")
|
const [username, setUsername] = useState("")
|
||||||
const [password, setPassword] = 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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
@ -28,16 +82,37 @@ const Auth = ({
|
||||||
<div className={styles.formContentSpace}>
|
<div className={styles.formContentSpace}>
|
||||||
<h1>Sign {signText}</h1>
|
<h1>Sign {signText}</h1>
|
||||||
</div>
|
</div>
|
||||||
{/* <form onSubmit={handleSubmit}> */}
|
<form onSubmit={handleSubmit}>
|
||||||
<form>
|
|
||||||
<div className={styles.formGroup}>
|
<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
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(event) => setUsername(event.currentTarget.value)}
|
onChange={handleChangeUsername}
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
required
|
required={true}
|
||||||
minLength={3}
|
minLength={3}
|
||||||
width="100%"
|
width="100%"
|
||||||
aria-label="Username"
|
aria-label="Username"
|
||||||
|
@ -46,59 +121,37 @@ const Auth = ({
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(event) => setPassword(event.currentTarget.value)}
|
onChange={handleChangePassword}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
required
|
required={true}
|
||||||
minLength={6}
|
minLength={6}
|
||||||
width="100%"
|
width="100%"
|
||||||
aria-label="Password"
|
aria-label="Password"
|
||||||
/>
|
/>
|
||||||
{requiresServerPassword && (
|
<Button width={"100%"} type="submit">
|
||||||
<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: "/"
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Sign {signText}
|
Sign {signText}
|
||||||
</Button>
|
</Button>
|
||||||
<hr style={{ width: "100%" }} />
|
{isGithubEnabled ? <hr style={{ width: "100%" }} /> : null}
|
||||||
<Button
|
{isGithubEnabled ? (
|
||||||
type="submit"
|
<Button
|
||||||
buttonType="primary"
|
type="submit"
|
||||||
width="100%"
|
buttonType="primary"
|
||||||
style={{
|
width="100%"
|
||||||
color: "var(--fg)"
|
style={{
|
||||||
}}
|
color: "var(--fg)"
|
||||||
iconLeft={<GitHub />}
|
}}
|
||||||
onClick={(e) => {
|
iconLeft={<GitHub />}
|
||||||
e.preventDefault()
|
onClick={(e) => {
|
||||||
signIn("github", {
|
e.preventDefault()
|
||||||
callbackUrl: "/"
|
signIn("github", {
|
||||||
})
|
callbackUrl: "/",
|
||||||
}}
|
registration_password: serverPassword
|
||||||
>
|
})
|
||||||
Sign {signText.toLowerCase()} with GitHub
|
}}
|
||||||
</Button>
|
>
|
||||||
|
Sign {signText.toLowerCase()} with GitHub
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formContentSpace}>
|
<div className={styles.formContentSpace}>
|
||||||
{signingIn ? (
|
{signingIn ? (
|
||||||
|
@ -117,7 +170,6 @@ const Auth = ({
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{errorMsg && <Note type="error">{errorMsg}</Note>}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
|
import config from "@lib/config"
|
||||||
import Auth from "../components"
|
import Auth from "../components"
|
||||||
|
|
||||||
export default function SignInPage() {
|
export function isGithubEnabled() {
|
||||||
return <Auth page="signin" />
|
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 Auth from "../components"
|
||||||
import { getRequiresPasscode } from "pages/api/auth/requires-passcode"
|
import { getRequiresPasscode } from "pages/api/auth/requires-passcode"
|
||||||
|
import { isGithubEnabled } from "../signin/page"
|
||||||
|
|
||||||
const getPasscode = async () => {
|
const getPasscode = async () => {
|
||||||
return await getRequiresPasscode()
|
return await getRequiresPasscode()
|
||||||
|
@ -7,5 +8,5 @@ const getPasscode = async () => {
|
||||||
|
|
||||||
export default async function SignUpPage() {
|
export default async function SignUpPage() {
|
||||||
const requiresPasscode = await getPasscode()
|
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") {
|
if (session.status === "unauthenticated") {
|
||||||
router.push("/login")
|
router.push("/signin")
|
||||||
return null
|
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() {
|
export default function AdminLoading() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,100 +1,55 @@
|
||||||
import { getAllPosts, getAllUsers } from "@lib/server/prisma"
|
|
||||||
import { Spinner } from "@components/spinner"
|
import { Spinner } from "@components/spinner"
|
||||||
import styles from "./admin.module.css"
|
import { getAllPosts, getAllUsers } from "@lib/server/prisma"
|
||||||
|
import { PostTable, UserTable } from "./components/tables"
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function AdminPage() {
|
export default async function AdminPage() {
|
||||||
const usersPromise = getAllUsers()
|
const usersPromise = getAllUsers({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true
|
||||||
|
}
|
||||||
|
})
|
||||||
const postsPromise = getAllPosts({
|
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 [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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Admin</h1>
|
<h1>Admin</h1>
|
||||||
<h2>Users</h2>
|
<h2>Users</h2>
|
||||||
<UserTable users={users} />
|
<UserTable users={serializedUsers} />
|
||||||
<h2>Posts</h2>
|
<h2>Posts</h2>
|
||||||
<PostTable posts={posts} />
|
<PostTable posts={serializedPosts} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ type Tab = {
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const session = useSession()
|
const session = useSession()
|
||||||
|
console.log("session", session)
|
||||||
const isSignedIn = session?.status === "authenticated"
|
const isSignedIn = session?.status === "authenticated"
|
||||||
const isAdmin = session?.data?.user?.role === "admin"
|
const isAdmin = session?.data?.user?.role === "admin"
|
||||||
const isLoading = session?.status === "loading"
|
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
|
// eslint-disable-next-line react/display-name
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ label, className, width, height, labelClassName, ...props }, ref) => {
|
({ label, className, required, width, height, labelClassName, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.wrapper}
|
className={styles.wrapper}
|
||||||
|
|
|
@ -13,9 +13,24 @@ const getWelcomeData = async () => {
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const { content, rendered, title } = await getWelcomeData()
|
const { content, rendered, title } = await getWelcomeData()
|
||||||
const getPostsPromise = getAllPosts({
|
const getPostsPromise = getAllPosts({
|
||||||
where: { visibility: "public" },
|
select: {
|
||||||
include: {
|
id: true,
|
||||||
files: 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 = {
|
type Config = {
|
||||||
is_production: boolean
|
is_production: boolean
|
||||||
enable_admin: boolean
|
enable_admin: boolean
|
||||||
|
@ -9,6 +8,7 @@ type Config = {
|
||||||
github_client_id: string
|
github_client_id: string
|
||||||
github_client_secret: string
|
github_client_secret: string
|
||||||
nextauth_secret: string
|
nextauth_secret: string
|
||||||
|
credential_auth: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnvironmentValue = string | undefined
|
type EnvironmentValue = string | undefined
|
||||||
|
@ -77,6 +77,9 @@ export const config = (env: Environment): Config => {
|
||||||
github_client_id: env.GITHUB_CLIENT_ID ?? "",
|
github_client_id: env.GITHUB_CLIENT_ID ?? "",
|
||||||
github_client_secret: env.GITHUB_CLIENT_SECRET ?? "",
|
github_client_secret: env.GITHUB_CLIENT_SECRET ?? "",
|
||||||
nextauth_secret: throwIfUndefined("NEXTAUTH_SECRET"),
|
nextauth_secret: throwIfUndefined("NEXTAUTH_SECRET"),
|
||||||
|
credential_auth: stringToBoolean(
|
||||||
|
developmentDefault("CREDENTIAL_AUTH", "true")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,83 +5,140 @@ import CredentialsProvider from "next-auth/providers/credentials"
|
||||||
import { prisma } from "@lib/server/prisma"
|
import { prisma } from "@lib/server/prisma"
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
|
import { Provider } from "next-auth/providers"
|
||||||
|
|
||||||
const providers: NextAuthOptions["providers"] = [
|
const credentialsOptions = () => {
|
||||||
GitHubProvider({
|
const options: Record<string, any> = {
|
||||||
clientId: config.github_client_id,
|
username: {
|
||||||
clientSecret: config.github_client_secret
|
label: "Username",
|
||||||
}),
|
required: true,
|
||||||
CredentialsProvider({
|
type: "text"
|
||||||
name: "Credentials",
|
|
||||||
credentials: {
|
|
||||||
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
|
||||||
password: { label: "Password", type: "password" }
|
|
||||||
},
|
},
|
||||||
async authorize(credentials) {
|
password: {
|
||||||
if (!credentials) {
|
label: "Password",
|
||||||
return null
|
required: true,
|
||||||
}
|
type: "password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
if (config.registration_password) {
|
||||||
where: {
|
options["registration_password"] = {
|
||||||
username: credentials.username
|
label: "Server Password",
|
||||||
},
|
type: "password",
|
||||||
select: {
|
optional: true
|
||||||
id: true,
|
}
|
||||||
username: true,
|
}
|
||||||
displayName: true,
|
|
||||||
role: true,
|
return options
|
||||||
password: true
|
}
|
||||||
|
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
|
return providers
|
||||||
.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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
|
|
||||||
export const authOptions: NextAuthOptions = {
|
export const authOptions: NextAuthOptions = {
|
||||||
// see https://github.com/prisma/prisma/issues/16117 / https://github.com/shadcn/taxonomy
|
adapter: PrismaAdapter(prisma),
|
||||||
adapter: PrismaAdapter(prisma as any),
|
|
||||||
session: {
|
session: {
|
||||||
strategy: "jwt"
|
strategy: "jwt"
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
signIn: "/signin"
|
signIn: "/signin",
|
||||||
// TODO
|
error: "/signin"
|
||||||
// error: "/auth/error",
|
|
||||||
},
|
},
|
||||||
providers,
|
providers: providers(),
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async session({ token, session }) {
|
async session({ token, session }) {
|
||||||
if (token) {
|
if (token) {
|
||||||
|
@ -94,6 +151,7 @@ export const authOptions: NextAuthOptions = {
|
||||||
|
|
||||||
return session
|
return session
|
||||||
},
|
},
|
||||||
|
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
const dbUser = await prisma.user.findFirst({
|
const dbUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -57,7 +57,9 @@ const postWithFilesAndAuthor = Prisma.validator<Prisma.PostArgs>()({
|
||||||
|
|
||||||
export type ServerPostWithFiles = Prisma.PostGetPayload<typeof postWithFiles>
|
export type ServerPostWithFiles = Prisma.PostGetPayload<typeof postWithFiles>
|
||||||
export type PostWithAuthor = Prisma.PostGetPayload<typeof postWithAuthor>
|
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"> & {
|
export type PostWithFiles = Omit<ServerPostWithFiles, "files"> & {
|
||||||
files: (Omit<ServerPostWithFiles["files"][number], "content" | "html"> & {
|
files: (Omit<ServerPostWithFiles["files"][number], "content" | "html"> & {
|
||||||
|
@ -70,7 +72,10 @@ export type PostWithFilesAndAuthor = Omit<
|
||||||
ServerPostWithFilesAndAuthor,
|
ServerPostWithFilesAndAuthor,
|
||||||
"files"
|
"files"
|
||||||
> & {
|
> & {
|
||||||
files: (Omit<ServerPostWithFilesAndAuthor["files"][number], "content" | "html"> & {
|
files: (Omit<
|
||||||
|
ServerPostWithFilesAndAuthor["files"][number],
|
||||||
|
"content" | "html"
|
||||||
|
> & {
|
||||||
content: string
|
content: string
|
||||||
html: string
|
html: string
|
||||||
})[]
|
})[]
|
||||||
|
@ -201,40 +206,25 @@ export const getPostById = async (
|
||||||
return post
|
return post
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAllPosts = async ({
|
export const getAllPosts = async (
|
||||||
withFiles = false,
|
options?: Prisma.PostFindManyArgs
|
||||||
withAuthor = false,
|
): Promise<Post[] | ServerPostWithFiles[] | ServerPostWithFilesAndAuthor[]> => {
|
||||||
take = 100,
|
const posts = await prisma.post.findMany(options)
|
||||||
...rest
|
return posts
|
||||||
}: {
|
|
||||||
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 type UserWithPosts = User & {
|
export const userWithPosts = Prisma.validator<Prisma.UserArgs>()({
|
||||||
posts: Post[]
|
include: {
|
||||||
}
|
posts: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const getAllUsers = async () => {
|
export type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
|
||||||
const users = await prisma.user.findMany({
|
|
||||||
|
export const getAllUsers = async (
|
||||||
|
options?: Prisma.UserFindManyArgs
|
||||||
|
): Promise<User[] | UserWithPosts[]> => {
|
||||||
|
const users = (await prisma.user.findMany({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
|
@ -242,8 +232,9 @@ export const getAllUsers = async () => {
|
||||||
displayName: true,
|
displayName: true,
|
||||||
posts: true,
|
posts: true,
|
||||||
createdAt: true
|
createdAt: true
|
||||||
}
|
},
|
||||||
})
|
...options
|
||||||
|
})) as User[] | UserWithPosts[]
|
||||||
|
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
@ -265,7 +256,7 @@ export const searchPosts = async (
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
title: {
|
title: {
|
||||||
search: query,
|
search: query
|
||||||
},
|
},
|
||||||
authorId: userId,
|
authorId: userId,
|
||||||
visibility: publicOnly ? "public" : undefined
|
visibility: publicOnly ? "public" : undefined
|
||||||
|
@ -275,7 +266,7 @@ export const searchPosts = async (
|
||||||
some: {
|
some: {
|
||||||
content: {
|
content: {
|
||||||
in: [Buffer.from(query)]
|
in: [Buffer.from(query)]
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visibility: publicOnly ? "public" : undefined
|
visibility: publicOnly ? "public" : undefined
|
||||||
|
|
|
@ -6,10 +6,10 @@ const nextConfig = {
|
||||||
experimental: {
|
experimental: {
|
||||||
// esmExternals: true,
|
// esmExternals: true,
|
||||||
appDir: true,
|
appDir: true,
|
||||||
serverComponentsExternalPackages: ['prisma'],
|
serverComponentsExternalPackages: ["prisma", "@prisma/client"],
|
||||||
},
|
},
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
async rewrites() {
|
rewrites() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/file/raw/:id",
|
source: "/file/raw/:id",
|
||||||
|
@ -21,7 +21,6 @@ const nextConfig = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(
|
export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"next": "13.0.8-canary.0",
|
"next": "13.0.8-canary.0",
|
||||||
"next-auth": "^4.18.4",
|
"next-auth": "^4.18.6",
|
||||||
"prisma": "^4.7.1",
|
"prisma": "^4.7.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-datepicker": "4.8.0",
|
"react-datepicker": "4.8.0",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
import { prisma } from "lib/server/prisma"
|
import { prisma } from "lib/server/prisma"
|
||||||
import { getSession } from "next-auth/react"
|
import { getSession } from "next-auth/react"
|
||||||
|
import { deleteUser } from "../user/[id]"
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
"user",
|
"user",
|
||||||
|
@ -94,11 +95,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
return res.status(400).json({ error: "Invalid request" })
|
return res.status(400).json({ error: "Invalid request" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.delete({
|
await deleteUser(userId)
|
||||||
where: { id: userId }
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.status(200).json(user)
|
return res.status(200).send("User deleted")
|
||||||
case "delete-post":
|
case "delete-post":
|
||||||
const { postId } = req.body
|
const { postId } = req.body
|
||||||
if (!postId) {
|
if (!postId) {
|
||||||
|
|
|
@ -42,9 +42,42 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
})
|
})
|
||||||
case "GET":
|
case "GET":
|
||||||
return res.json(currUser)
|
return res.json(currUser)
|
||||||
|
case "DELETE":
|
||||||
|
if (currUser?.role !== "admin" && currUser?.id !== id) {
|
||||||
|
return res.status(403).json({ message: "Unauthorized" })
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteUser(id)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return res.status(405).json({ message: "Method not allowed" })
|
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
|
jest: ^29.3.1
|
||||||
lodash.debounce: ^4.0.8
|
lodash.debounce: ^4.0.8
|
||||||
next: 13.0.8-canary.0
|
next: 13.0.8-canary.0
|
||||||
next-auth: ^4.18.4
|
next-auth: ^4.18.6
|
||||||
next-unused: 0.0.6
|
next-unused: 0.0.6
|
||||||
prettier: 2.6.2
|
prettier: 2.6.2
|
||||||
prisma: ^4.7.1
|
prisma: ^4.7.1
|
||||||
|
@ -46,7 +46,7 @@ specifiers:
|
||||||
typescript-plugin-css-modules: 3.4.0
|
typescript-plugin-css-modules: 3.4.0
|
||||||
|
|
||||||
dependencies:
|
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
|
'@next/eslint-plugin-next': 13.0.7-canary.4
|
||||||
'@prisma/client': 4.7.1_prisma@4.7.1
|
'@prisma/client': 4.7.1_prisma@4.7.1
|
||||||
'@radix-ui/react-dialog': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u
|
'@radix-ui/react-dialog': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u
|
||||||
|
@ -61,7 +61,7 @@ dependencies:
|
||||||
jest: 29.3.1_@types+node@17.0.23
|
jest: 29.3.1_@types+node@17.0.23
|
||||||
lodash.debounce: 4.0.8
|
lodash.debounce: 4.0.8
|
||||||
next: 13.0.8-canary.0_biqbaboplfbrettd7655fr4n2y
|
next: 13.0.8-canary.0_biqbaboplfbrettd7655fr4n2y
|
||||||
next-auth: 4.18.4_rhfownvlqkszea7w3lnpwl7bzy
|
next-auth: 4.18.6_rhfownvlqkszea7w3lnpwl7bzy
|
||||||
prisma: 4.7.1
|
prisma: 4.7.1
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y
|
react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y
|
||||||
|
@ -773,14 +773,14 @@ packages:
|
||||||
'@jridgewell/sourcemap-codec': 1.4.14
|
'@jridgewell/sourcemap-codec': 1.4.14
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@next-auth/prisma-adapter/1.0.5_4eojhct6t46nl4awizrjr4dkya:
|
/@next-auth/prisma-adapter/1.0.5_64qbzg5ec56bux2misz3l4u6g4:
|
||||||
resolution: {integrity: sha512-VqMS11IxPXrPGXw6Oul6jcyS/n8GLOWzRMrPr3EMdtD6eOalM6zz05j08PcNiis8QzkfuYnCv49OvufTuaEwYQ==}
|
resolution: {integrity: sha512-VqMS11IxPXrPGXw6Oul6jcyS/n8GLOWzRMrPr3EMdtD6eOalM6zz05j08PcNiis8QzkfuYnCv49OvufTuaEwYQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@prisma/client': '>=2.26.0 || >=3'
|
'@prisma/client': '>=2.26.0 || >=3'
|
||||||
next-auth: ^4
|
next-auth: ^4
|
||||||
dependencies:
|
dependencies:
|
||||||
'@prisma/client': 4.7.1_prisma@4.7.1
|
'@prisma/client': 4.7.1_prisma@4.7.1
|
||||||
next-auth: 4.18.4_rhfownvlqkszea7w3lnpwl7bzy
|
next-auth: 4.18.6_rhfownvlqkszea7w3lnpwl7bzy
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@next/bundle-analyzer/13.0.7-canary.4:
|
/@next/bundle-analyzer/13.0.7-canary.4:
|
||||||
|
@ -5105,8 +5105,8 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/next-auth/4.18.4_rhfownvlqkszea7w3lnpwl7bzy:
|
/next-auth/4.18.6_rhfownvlqkszea7w3lnpwl7bzy:
|
||||||
resolution: {integrity: sha512-tvXOabxv5U/y6ib56XPkOnc/48tYc+xT6GNOLREIme8WVGYHDTc3CGEfe2+0bVCWAm0ax/GYXH0By5NFoaJDww==}
|
resolution: {integrity: sha512-0TQwbq5X9Jyd1wUVYUoyvHJh4JWXeW9UOcMEl245Er/Y5vsSbyGJHt8M7xjRMzk9mORVMYehoMdERgyiq/jCgA==}
|
||||||
engines: {node: ^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0}
|
engines: {node: ^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
next: ^12.2.5 || ^13
|
next: ^12.2.5 || ^13
|
||||||
|
|
Loading…
Reference in a new issue