diff --git a/client/components/header/index.tsx b/client/components/header/index.tsx
index c1d58440..7b5efdef 100644
--- a/client/components/header/index.tsx
+++ b/client/components/header/index.tsx
@@ -88,12 +88,12 @@ const Header = () => {
value: "yours",
href: "/mine"
},
- // {
- // name: 'settings',
- // icon: ,
- // value: 'settings',
- // href: '/settings'
- // },
+ {
+ name: 'settings',
+ icon: ,
+ value: 'settings',
+ href: '/settings'
+ },
{
name: "sign out",
icon: ,
diff --git a/client/components/settings/index.tsx b/client/components/settings/index.tsx
new file mode 100644
index 00000000..802cb1ea
--- /dev/null
+++ b/client/components/settings/index.tsx
@@ -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 (
)
+}
+
+export default SettingsPage
\ No newline at end of file
diff --git a/client/components/settings/sections/password.tsx b/client/components/settings/sections/password.tsx
new file mode 100644
index 00000000..4f46a3c1
--- /dev/null
+++ b/client/components/settings/sections/password.tsx
@@ -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('')
+ const [newPassword, setNewPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+
+ const { setToast } = useToasts()
+
+ const handlePasswordChange = (e: React.ChangeEvent) => {
+ setPassword(e.target.value)
+ }
+
+ const handleNewPasswordChange = (e: React.ChangeEvent) => {
+ setNewPassword(e.target.value)
+ }
+
+ const handleConfirmPasswordChange = (e: React.ChangeEvent) => {
+ setConfirmPassword(e.target.value)
+ }
+
+ const onSubmit = async (e: React.FormEvent) => {
+ 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 (
+ )
+}
+
+export default Password
\ No newline at end of file
diff --git a/client/components/settings/sections/profile.tsx b/client/components/settings/sections/profile.tsx
new file mode 100644
index 00000000..ed95851a
--- /dev/null
+++ b/client/components/settings/sections/profile.tsx
@@ -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()
+ const [email, setEmail] = useState()
+ const [bio, setBio] = useState()
+
+ 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) => {
+ setName(e.target.value)
+ }
+
+ const handleEmailChange = (e: React.ChangeEvent) => {
+ setEmail(e.target.value)
+ }
+
+ const handleBioChange = (e: React.ChangeEvent) => {
+ setBio(e.target.value)
+ }
+
+ const onSubmit = async (e: React.FormEvent) => {
+ 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 (<>
+
+ This information will be publicly available on your profile
+
+ >)
+}
+
+export default Profile
\ No newline at end of file
diff --git a/client/components/settings/settings-group/index.tsx b/client/components/settings/settings-group/index.tsx
new file mode 100644
index 00000000..adf9affd
--- /dev/null
+++ b/client/components/settings/settings-group/index.tsx
@@ -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
+}
+
+export default SettingsGroup
diff --git a/client/components/settings/settings-group/settings-group.module.css b/client/components/settings/settings-group/settings-group.module.css
new file mode 100644
index 00000000..bb12c0b4
--- /dev/null
+++ b/client/components/settings/settings-group/settings-group.module.css
@@ -0,0 +1,4 @@
+.content form label {
+ display: block;
+ margin-bottom: 5px;
+}
diff --git a/client/lib/hooks/use-user-data.ts b/client/lib/hooks/use-user-data.ts
index ce0c9e00..455c26fc 100644
--- a/client/lib/hooks/use-user-data.ts
+++ b/client/lib/hooks/use-user-data.ts
@@ -19,7 +19,7 @@ const useUserData = () => {
useEffect(() => {
if (authToken) {
const fetchUser = async () => {
- const response = await fetch(`/server-api/users/self`, {
+ const response = await fetch(`/server-api/user/self`, {
headers: {
Authorization: `Bearer ${authToken}`
}
diff --git a/client/lib/types.d.ts b/client/lib/types.d.ts
index 3e3006f1..e7d5ec7a 100644
--- a/client/lib/types.d.ts
+++ b/client/lib/types.d.ts
@@ -34,4 +34,7 @@ type User = {
posts?: Post[]
role: "admin" | "user" | ""
createdAt: string
+ displayName?: string
+ bio?: string
+ email?: string
}
diff --git a/client/pages/settings.tsx b/client/pages/settings.tsx
new file mode 100644
index 00000000..ce40051e
--- /dev/null
+++ b/client/pages/settings.tsx
@@ -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 = () => (
+
+
+
+
+
+
+)
+
+export default Settings
diff --git a/server/src/app.ts b/server/src/app.ts
index 635b3d20..cbb60c92 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -1,7 +1,7 @@
import * as express from "express"
import * as bodyParser from "body-parser"
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 secretKey from "@lib/middleware/secret-key"
import markdown from "@lib/render-markdown"
@@ -14,7 +14,7 @@ app.use(bodyParser.json({ limit: "5mb" }))
app.use("/auth", auth)
app.use("/posts", posts)
-app.use("/users", users)
+app.use("/user", user)
app.use("/files", files)
app.use("/admin", admin)
app.use("/health", health)
diff --git a/server/src/lib/models/User.ts b/server/src/lib/models/User.ts
index b4653137..06f42c6f 100644
--- a/server/src/lib/models/User.ts
+++ b/server/src/lib/models/User.ts
@@ -61,4 +61,13 @@ export class User extends Model {
@Column
role!: string
+
+ @Column
+ email?: string
+
+ @Column
+ displayName?: string
+
+ @Column
+ bio?: string
}
diff --git a/server/src/migrations/09_add_more_user_settings.ts b/server/src/migrations/09_add_more_user_settings.ts
new file mode 100644
index 00000000..4624306e
--- /dev/null
+++ b/server/src/migrations/09_add_more_user_settings.ts
@@ -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"),
+ ])
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
index f25c3557..742adcd2 100644
--- a/server/src/routes/auth.ts
+++ b/server/src/routes/auth.ts
@@ -4,7 +4,7 @@ import { User } from "@lib/models/User"
import { AuthToken } from "@lib/models/AuthToken"
import { sign, verify } from "jsonwebtoken"
import config from "@lib/config"
-import jwt from "@lib/middleware/jwt"
+import jwt, { UserJwtRequest } from "@lib/middleware/jwt"
import { celebrate, Joi } from "celebrate"
import secretKey from "@lib/middleware/secret-key"
@@ -194,3 +194,38 @@ auth.post("/signout", secretKey, async (req, res, next) => {
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)
+ }
+ }
+)
diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts
index 3b5e7211..72605d17 100644
--- a/server/src/routes/index.ts
+++ b/server/src/routes/index.ts
@@ -1,6 +1,6 @@
export { auth } from "./auth"
export { posts } from "./posts"
-export { users } from "./users"
+export { user } from "./user"
export { files } from "./files"
export { admin } from "./admin"
export { health } from "./health"
diff --git a/server/src/routes/user.ts b/server/src/routes/user.ts
new file mode 100644
index 00000000..29ea6c28
--- /dev/null
+++ b/server/src/routes/user.ts
@@ -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)
+ }
+ }
+)
diff --git a/server/src/routes/users.ts b/server/src/routes/users.ts
deleted file mode 100644
index a74a443b..00000000
--- a/server/src/routes/users.ts
+++ /dev/null
@@ -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)
- }
-})