client/server: add user settings page; add displayName, email, bio fields to user model (#108)
This commit is contained in:
parent
a884fdbfef
commit
519b6cdc71
16 changed files with 423 additions and 42 deletions
|
@ -88,12 +88,12 @@ const Header = () => {
|
||||||
value: "yours",
|
value: "yours",
|
||||||
href: "/mine"
|
href: "/mine"
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// name: 'settings',
|
name: 'settings',
|
||||||
// icon: <SettingsIcon />,
|
icon: <SettingsIcon />,
|
||||||
// value: 'settings',
|
value: 'settings',
|
||||||
// href: '/settings'
|
href: '/settings'
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
name: "sign out",
|
name: "sign out",
|
||||||
icon: <SignOutIcon />,
|
icon: <SignOutIcon />,
|
||||||
|
|
23
client/components/settings/index.tsx
Normal file
23
client/components/settings/index.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Fieldset, Text, Divider, Note, Input, Textarea, Button } from "@geist-ui/core"
|
||||||
|
import Password from "./sections/password"
|
||||||
|
import Profile from "./sections/profile"
|
||||||
|
import SettingsGroup from "./settings-group"
|
||||||
|
|
||||||
|
const SettingsPage = () => {
|
||||||
|
return (<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 'var(--gap)',
|
||||||
|
marginBottom: 'var(--gap)',
|
||||||
|
}}>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<SettingsGroup title="Profile">
|
||||||
|
<Profile />
|
||||||
|
</SettingsGroup>
|
||||||
|
<SettingsGroup title="Password">
|
||||||
|
<Password />
|
||||||
|
</SettingsGroup>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsPage
|
97
client/components/settings/sections/password.tsx
Normal file
97
client/components/settings/sections/password.tsx
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import { Input, Button, useToasts } from "@geist-ui/core"
|
||||||
|
import Cookies from "js-cookie"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
const Password = () => {
|
||||||
|
const [password, setPassword] = useState<string>('')
|
||||||
|
const [newPassword, setNewPassword] = useState<string>('')
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState<string>('')
|
||||||
|
|
||||||
|
const { setToast } = useToasts()
|
||||||
|
|
||||||
|
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPassword(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setNewPassword(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setConfirmPassword(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!password || !newPassword || !confirmPassword) {
|
||||||
|
setToast({
|
||||||
|
text: "Please fill out all fields",
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setToast({
|
||||||
|
text: "New password and confirm password do not match",
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/server-api/auth/change-password", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${Cookies.get("drift-token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
oldPassword: password,
|
||||||
|
newPassword,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
setToast({
|
||||||
|
text: "Password updated successfully",
|
||||||
|
type: "success",
|
||||||
|
})
|
||||||
|
setPassword('')
|
||||||
|
setNewPassword('')
|
||||||
|
setConfirmPassword('')
|
||||||
|
} else {
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
setToast({
|
||||||
|
text: data.error ?? "Failed to update password",
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "var(--gap)",
|
||||||
|
maxWidth: "300px",
|
||||||
|
}}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="current-password">Current password</label>
|
||||||
|
<Input onChange={handlePasswordChange} minLength={6} maxLength={128} value={password} id="current-password" htmlType="password" required autoComplete="current-password" placeholder="Current Password" width={"100%"} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="new-password">New password</label>
|
||||||
|
<Input onChange={handleNewPasswordChange} minLength={6} maxLength={128} value={newPassword} id="new-password" htmlType="password" required autoComplete="new-password" placeholder="New Password" width={"100%"} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="confirm-password">Confirm password</label>
|
||||||
|
<Input onChange={handleConfirmPasswordChange} minLength={6} maxLength={128} value={confirmPassword} id="confirm-password" htmlType="password" required autoComplete="confirm-password" placeholder="Confirm Password" width={"100%"} />
|
||||||
|
</div>
|
||||||
|
<Button htmlType="submit" auto>Change Password</Button>
|
||||||
|
</form>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Password
|
100
client/components/settings/sections/profile.tsx
Normal file
100
client/components/settings/sections/profile.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { Note, Input, Textarea, Button, useToasts } from "@geist-ui/core"
|
||||||
|
import useUserData from "@lib/hooks/use-user-data"
|
||||||
|
import Cookies from "js-cookie"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
const Profile = () => {
|
||||||
|
const user = useUserData()
|
||||||
|
const [name, setName] = useState<string>()
|
||||||
|
const [email, setEmail] = useState<string>()
|
||||||
|
const [bio, setBio] = useState<string>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(user)
|
||||||
|
if (user?.displayName) setName(user.displayName)
|
||||||
|
if (user?.email) setEmail(user.email)
|
||||||
|
if (user?.bio) setBio(user.bio)
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
const { setToast } = useToasts()
|
||||||
|
|
||||||
|
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setName(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setEmail(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBioChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setBio(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!name && !email && !bio) {
|
||||||
|
setToast({
|
||||||
|
text: "Please fill out at least one field",
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
displayName: name,
|
||||||
|
email,
|
||||||
|
bio,
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/server-api/user/profile", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${Cookies.get("drift-token")}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
setToast({
|
||||||
|
text: "Profile updated",
|
||||||
|
type: "success",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setToast({
|
||||||
|
text: "Something went wrong updating your profile",
|
||||||
|
type: "error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<Note type="warning" marginBottom={"var(--gap)"}>
|
||||||
|
This information will be publicly available on your profile
|
||||||
|
</Note>
|
||||||
|
<form
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "var(--gap)",
|
||||||
|
maxWidth: "300px",
|
||||||
|
}}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="displayName">Display name</label>
|
||||||
|
<Input id="displayName" width={"100%"} placeholder="my name" value={name || ''} onChange={handleNameChange} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email">Email</label>
|
||||||
|
<Input id="email" htmlType="email" width={"100%"} placeholder="my@email.io" value={email || ''} onChange={handleEmailChange} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="bio">Biography (max 250 characters)</label>
|
||||||
|
<Textarea id="bio" width="100%" maxLength={250} placeholder="I enjoy..." value={bio || ''} onChange={handleBioChange} />
|
||||||
|
</div>
|
||||||
|
<Button htmlType="submit" auto>Submit</Button>
|
||||||
|
</form></>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profile
|
24
client/components/settings/settings-group/index.tsx
Normal file
24
client/components/settings/settings-group/index.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { Fieldset, Text, Divider } from "@geist-ui/core"
|
||||||
|
import styles from './settings-group.module.css'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string,
|
||||||
|
children: React.ReactNode | React.ReactNode[],
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsGroup = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
|
return <Fieldset>
|
||||||
|
<Fieldset.Content>
|
||||||
|
<Text h4>{title}</Text>
|
||||||
|
</Fieldset.Content>
|
||||||
|
<Divider />
|
||||||
|
<Fieldset.Content className={styles.content}>
|
||||||
|
{children}
|
||||||
|
</Fieldset.Content>
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsGroup
|
|
@ -0,0 +1,4 @@
|
||||||
|
.content form label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ const useUserData = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
||||||
const response = await fetch(`/server-api/users/self`, {
|
const response = await fetch(`/server-api/user/self`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${authToken}`
|
Authorization: `Bearer ${authToken}`
|
||||||
}
|
}
|
||||||
|
|
3
client/lib/types.d.ts
vendored
3
client/lib/types.d.ts
vendored
|
@ -34,4 +34,7 @@ type User = {
|
||||||
posts?: Post[]
|
posts?: Post[]
|
||||||
role: "admin" | "user" | ""
|
role: "admin" | "user" | ""
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
displayName?: string
|
||||||
|
bio?: string
|
||||||
|
email?: string
|
||||||
}
|
}
|
||||||
|
|
15
client/pages/settings.tsx
Normal file
15
client/pages/settings.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Button, Divider, Text, Fieldset, Input, Page, Note, Textarea } from "@geist-ui/core"
|
||||||
|
import PageSeo from "@components/page-seo"
|
||||||
|
import styles from "@styles/Home.module.css"
|
||||||
|
import SettingsPage from "@components/settings"
|
||||||
|
|
||||||
|
const Settings = () => (
|
||||||
|
<Page width={"100%"}>
|
||||||
|
<PageSeo title="Drift - Settings" />
|
||||||
|
<Page.Content className={styles.main} style={{ gap: 'var(--gap)', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<SettingsPage />
|
||||||
|
</Page.Content>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Settings
|
|
@ -1,7 +1,7 @@
|
||||||
import * as express from "express"
|
import * as express from "express"
|
||||||
import * as bodyParser from "body-parser"
|
import * as bodyParser from "body-parser"
|
||||||
import * as errorhandler from "strong-error-handler"
|
import * as errorhandler from "strong-error-handler"
|
||||||
import { posts, users, auth, files, admin, health } from "@routes/index"
|
import { posts, user, auth, files, admin, health } from "@routes/index"
|
||||||
import { errors } from "celebrate"
|
import { errors } from "celebrate"
|
||||||
import secretKey from "@lib/middleware/secret-key"
|
import secretKey from "@lib/middleware/secret-key"
|
||||||
import markdown from "@lib/render-markdown"
|
import markdown from "@lib/render-markdown"
|
||||||
|
@ -14,7 +14,7 @@ app.use(bodyParser.json({ limit: "5mb" }))
|
||||||
|
|
||||||
app.use("/auth", auth)
|
app.use("/auth", auth)
|
||||||
app.use("/posts", posts)
|
app.use("/posts", posts)
|
||||||
app.use("/users", users)
|
app.use("/user", user)
|
||||||
app.use("/files", files)
|
app.use("/files", files)
|
||||||
app.use("/admin", admin)
|
app.use("/admin", admin)
|
||||||
app.use("/health", health)
|
app.use("/health", health)
|
||||||
|
|
|
@ -61,4 +61,13 @@ export class User extends Model {
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
role!: string
|
role!: string
|
||||||
|
|
||||||
|
@Column
|
||||||
|
email?: string
|
||||||
|
|
||||||
|
@Column
|
||||||
|
displayName?: string
|
||||||
|
|
||||||
|
@Column
|
||||||
|
bio?: string
|
||||||
}
|
}
|
||||||
|
|
26
server/src/migrations/09_add_more_user_settings.ts
Normal file
26
server/src/migrations/09_add_more_user_settings.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"use strict"
|
||||||
|
import { DataTypes } from "sequelize"
|
||||||
|
import type { Migration } from "../database"
|
||||||
|
|
||||||
|
export const up: Migration = async ({ context: queryInterface }) =>
|
||||||
|
Promise.all([
|
||||||
|
queryInterface.addColumn("users", "email", {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
}),
|
||||||
|
queryInterface.addColumn("users", "displayName", {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
}),
|
||||||
|
queryInterface.addColumn("users", "bio", {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
export const down: Migration = async ({ context: queryInterface }) =>
|
||||||
|
Promise.all([
|
||||||
|
queryInterface.removeColumn("users", "email"),
|
||||||
|
queryInterface.removeColumn("users", "displayName"),
|
||||||
|
queryInterface.removeColumn("users", "bio"),
|
||||||
|
])
|
|
@ -4,7 +4,7 @@ import { User } from "@lib/models/User"
|
||||||
import { AuthToken } from "@lib/models/AuthToken"
|
import { AuthToken } from "@lib/models/AuthToken"
|
||||||
import { sign, verify } from "jsonwebtoken"
|
import { sign, verify } from "jsonwebtoken"
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import jwt from "@lib/middleware/jwt"
|
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
||||||
import { celebrate, Joi } from "celebrate"
|
import { celebrate, Joi } from "celebrate"
|
||||||
import secretKey from "@lib/middleware/secret-key"
|
import secretKey from "@lib/middleware/secret-key"
|
||||||
|
|
||||||
|
@ -194,3 +194,38 @@ auth.post("/signout", secretKey, async (req, res, next) => {
|
||||||
next(e)
|
next(e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
auth.put("/change-password",
|
||||||
|
jwt,
|
||||||
|
celebrate({
|
||||||
|
body: {
|
||||||
|
oldPassword: Joi.string().required().min(6).max(128),
|
||||||
|
newPassword: Joi.string().required().min(6).max(128)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
async (req: UserJwtRequest, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findOne({ where: { id: req.user?.id } })
|
||||||
|
if (!user) {
|
||||||
|
return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
|
||||||
|
const password_valid = await compare(req.body.oldPassword, user.password)
|
||||||
|
if (!password_valid) {
|
||||||
|
res.status(401).json({
|
||||||
|
error: "Old password is incorrect"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = await genSalt(10)
|
||||||
|
user.password = await hash(req.body.newPassword, salt)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: "Password changed"
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
next(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export { auth } from "./auth"
|
export { auth } from "./auth"
|
||||||
export { posts } from "./posts"
|
export { posts } from "./posts"
|
||||||
export { users } from "./users"
|
export { user } from "./user"
|
||||||
export { files } from "./files"
|
export { files } from "./files"
|
||||||
export { admin } from "./admin"
|
export { admin } from "./admin"
|
||||||
export { health } from "./health"
|
export { health } from "./health"
|
||||||
|
|
76
server/src/routes/user.ts
Normal file
76
server/src/routes/user.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { Router } from "express"
|
||||||
|
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
||||||
|
import { User } from "@lib/models/User"
|
||||||
|
import { celebrate, Joi } from "celebrate"
|
||||||
|
|
||||||
|
export const user = Router()
|
||||||
|
|
||||||
|
user.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
|
||||||
|
const error = () =>
|
||||||
|
res.status(401).json({
|
||||||
|
message: "Unauthorized"
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
return error()
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findByPk(req.user?.id, {
|
||||||
|
attributes: {
|
||||||
|
exclude: ["password"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!user) {
|
||||||
|
return error()
|
||||||
|
}
|
||||||
|
res.json(user)
|
||||||
|
} catch (error) {
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
user.put("/profile",
|
||||||
|
jwt,
|
||||||
|
celebrate({
|
||||||
|
body: {
|
||||||
|
displayName: Joi.string().optional().allow(""),
|
||||||
|
bio: Joi.string().optional().allow(""),
|
||||||
|
email: Joi.string().optional().email().allow(""),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
async (req: UserJwtRequest, res, next) => {
|
||||||
|
const error = () =>
|
||||||
|
res.status(401).json({
|
||||||
|
message: "Unauthorized"
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
return error()
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findByPk(req.user?.id)
|
||||||
|
if (!user) {
|
||||||
|
return error()
|
||||||
|
}
|
||||||
|
console.log(req.body)
|
||||||
|
const { displayName, bio, email } = req.body
|
||||||
|
const toUpdate = {} as any
|
||||||
|
if (displayName) {
|
||||||
|
toUpdate.displayName = displayName
|
||||||
|
}
|
||||||
|
if (bio) {
|
||||||
|
toUpdate.bio = bio
|
||||||
|
}
|
||||||
|
if (email) {
|
||||||
|
toUpdate.email = email
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.update(toUpdate)
|
||||||
|
res.json(user)
|
||||||
|
} catch (error) {
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
|
@ -1,31 +0,0 @@
|
||||||
import { Router } from "express"
|
|
||||||
import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
|
|
||||||
import { User } from "@lib/models/User"
|
|
||||||
|
|
||||||
export const users = Router()
|
|
||||||
|
|
||||||
users.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
|
|
||||||
const error = () =>
|
|
||||||
res.status(401).json({
|
|
||||||
message: "Unauthorized"
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!req.user) {
|
|
||||||
return error()
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findByPk(req.user?.id, {
|
|
||||||
attributes: {
|
|
||||||
exclude: ["password"]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!user) {
|
|
||||||
return error()
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(user)
|
|
||||||
} catch (error) {
|
|
||||||
next(error)
|
|
||||||
}
|
|
||||||
})
|
|
Loading…
Reference in a new issue