diff --git a/src/client/index.ts b/src/client/index.ts index 5552541..8e169d3 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -160,11 +160,6 @@ export interface SchemaUpdateUser { /** @maxLength 100 */ name: string need_promotion?: boolean - /** - * @minLength 8 - * @maxLength 100 - */ - password: string /** @maxLength 20 */ phone: string /** @maxLength 100 */ @@ -211,6 +206,12 @@ export interface SchemaUser { username?: string } +export interface SchemaUserChangePassword { + currentPassword?: string + newPassword?: string + username?: string +} + export interface SchemaUserDetail { address?: string address2?: string @@ -242,6 +243,11 @@ export interface SchemaUserListResponse { offset?: number } +export interface SchemaUserResetPassword { + newPassword?: string + username?: string +} + export type QueryParamsType = Record export type ResponseFormat = keyof Omit @@ -642,6 +648,56 @@ export class Api< ...params }), + /** + * @description Change the user's password. + * + * @tags Auth + * @name V1AuthChangepasswordCreate + * @summary change password + * @request POST:/api/v1/auth/changepassword + */ + v1AuthChangepasswordCreate: ( + changePassword: SchemaUserChangePassword, + query?: { + /** Redirect url after login */ + redirect_url?: string + }, + params: RequestParams = {} + ) => + this.request({ + path: `/api/v1/auth/changepassword`, + method: 'POST', + query: query, + body: changePassword, + type: ContentType.Json, + format: 'json', + ...params + }), + + /** + * @description Initiate the password reset process. + * + * @tags Auth + * @name V1AuthForgotpasswordCreate + * @summary forgot password + * @request POST:/api/v1/auth/forgotpassword + */ + v1AuthForgotpasswordCreate: ( + query?: { + /** Email */ + email?: string + }, + params: RequestParams = {} + ) => + this.request({ + path: `/api/v1/auth/forgotpassword`, + method: 'POST', + query: query, + type: ContentType.Json, + format: 'json', + ...params + }), + /** * @description Get current JWT. * @@ -725,6 +781,32 @@ export class Api< type: ContentType.Json, format: 'json', ...params + }), + + /** + * @description Reset the user's password. + * + * @tags Auth + * @name V1AuthResetpasswordCreate + * @summary reset password + * @request POST:/api/v1/auth/resetpassword + */ + v1AuthResetpasswordCreate: ( + resetPassword: SchemaUserResetPassword, + query?: { + /** Redirect url after login */ + redirect_url?: string + }, + params: RequestParams = {} + ) => + this.request({ + path: `/api/v1/auth/resetpassword`, + method: 'POST', + query: query, + body: resetPassword, + type: ContentType.Json, + format: 'json', + ...params }) } movie = { @@ -856,6 +938,26 @@ export class Api< type: ContentType.Json, format: 'json', ...params + }), + + /** + * @description update user info. + * + * @tags User + * @name V1UsersMeUpdate + * @summary update user info. + * @request PUT:/api/v1/users/me + * @secure + */ + v1UsersMeUpdate: (user: SchemaUpdateUser, params: RequestParams = {}) => + this.request({ + path: `/api/v1/users/me`, + method: 'PUT', + body: user, + secure: true, + type: ContentType.Json, + format: 'json', + ...params }) } misc = { diff --git a/src/pages/ChangePassword/index.tsx b/src/pages/ChangePassword/index.tsx new file mode 100644 index 0000000..104f514 --- /dev/null +++ b/src/pages/ChangePassword/index.tsx @@ -0,0 +1,122 @@ +import { useRequest } from 'ahooks' +import PageContainer from 'components/PageContainer' +import { useAuth } from 'hooks/useAuth' +import { useState } from 'react' +import { Col, Row } from 'react-bootstrap' +import Button from 'react-bootstrap/Button' +import Form from 'react-bootstrap/Form' +import { Navigate, useNavigate, useSearchParams } from 'react-router-dom' +import { DOMAIN_HOST } from 'utils/constants' +import Backend from 'utils/service' + +const ChangeForm: React.FC = () => { + const { user } = useAuth() + const [searchParams] = useSearchParams() + const [username, setUsername] = useState('') + const [currentPassword, setCurrentPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const navigate = useNavigate() + const { run: changePasssword } = useRequest( + async () => { + const from = searchParams.get('from') ?? '/' + return Backend.auth.v1AuthChangepasswordCreate( + { + username, + currentPassword, + newPassword + }, + { + redirect_url: `${DOMAIN_HOST}${from}` + } + ) + }, + { + manual: true, + onSuccess: () => { + navigate('/logout') // Navigate to the login page + } + } + ) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + changePasssword() + } + + return user ? ( + <> +
+

Reset Password

+
+ +
+ + + Username + + + { + setUsername(e.target.value) + }} + /> + + + + + Current Password + + + { + setCurrentPassword(e.target.value) + }} + /> + + + + + New Password + + + { + setNewPassword(e.target.value) + }} + /> + + + + + + + +
+ + + ) : ( + + ) +} + +const Index: React.FC = () => ( + + + +) + +export default Index diff --git a/src/pages/EditProfile/index.tsx b/src/pages/EditProfile/index.tsx index 3e80974..3ab7772 100644 --- a/src/pages/EditProfile/index.tsx +++ b/src/pages/EditProfile/index.tsx @@ -1,22 +1,120 @@ import { useRequest } from 'ahooks' -import type { SchemaUserDetail } from 'client' +import type { SchemaUpdateUser, SchemaUserDetail } from 'client' import PageContainer from 'components/PageContainer' import type React from 'react' import { useState } from 'react' -import { Form, Button, Col, Row, Accordion } from 'react-bootstrap' +import { Form, Button, Col, Row, Accordion, Alert } from 'react-bootstrap' +import { useNavigate } from 'react-router-dom' import Backend from 'utils/service' const UserProfileForm: React.FC = () => { - // TODO: submit update user - const { run: submit } = useRequest(async () => null) + const navigate = useNavigate() + const [success, setSuccess] = useState('') + const [error, setError] = useState('') + const [form, setForm] = useState({ + email: '', + name: '', + username: '', + address: '', + address2: '', + phone: '', + need_promotion: false, + city: '', + state: '', + zip: '', + card_address: '', + card_address2: '', + card_city: '', + card_state: '', + card_zip: '', + card_type: '', + card_number: '', + card_expiration: '' + }) + const usStates = [ + 'AL', + 'AK', + 'AZ', + 'AR', + 'CA', + 'CO', + 'CT', + 'DE', + 'FL', + 'GA', + 'HI', + 'ID', + 'IL', + 'IN', + 'IA', + 'KS', + 'KY', + 'LA', + 'ME', + 'MD', + 'MA', + 'MI', + 'MN', + 'MS', + 'MO', + 'MT', + 'NE', + 'NV', + 'NH', + 'NJ', + 'NM', + 'NY', + 'NC', + 'ND', + 'OH', + 'OK', + 'OR', + 'PA', + 'RI', + 'SC', + 'SD', + 'TN', + 'TX', + 'UT', + 'VT', + 'VA', + 'WA', + 'WV', + 'WI', + 'WY' + ] + const handleChange = ( + event: React.ChangeEvent<{ name?: string; value: unknown }> + ) => { + const { name, value } = event.target as { name?: string; value: unknown } + if (name === 'need_promotion') { + setForm(changeForm => ({ + ...changeForm, + need_promotion: !changeForm.need_promotion + })) + } else if (typeof name === 'string') { + setForm(changeForm => ({ ...changeForm, [name]: value })) + } + } + const { run: submit } = useRequest( + async () => Backend.user.v1UsersMeUpdate(form as SchemaUpdateUser), + { + manual: true, + onSuccess: () => { + setSuccess('Done!') + }, + onError: () => { + setError('Update profile failed. Please try again.') + } + } + ) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() submit() } - const [user, setUser] = useState() const { loading } = useRequest(async () => Backend.user.v1UsersMeList(), { onSuccess: res => { - setUser(res.data) + setForm(res.data) } }) @@ -31,187 +129,108 @@ const UserProfileForm: React.FC = () => { Email - - Password - { - setUser(prevUser => ({ - ...prevUser, - password: e.target.value - })) - }} - /> + + Password + Name { - setUser(prevUser => ({ - ...prevUser, - name: e.target.value - })) - }} + value={form.name} + onChange={handleChange} /> Phone { - setUser(prevUser => ({ - ...prevUser, - phone: e.target.value - })) - }} + value={form.phone} + onChange={handleChange} /> Address { - setUser(prevUser => ({ - ...prevUser, - address: e.target.value - })) - }} + value={form.address} + onChange={handleChange} /> Address 2 { - setUser(prevUser => ({ - ...prevUser, - address2: e.target.value - })) - }} + value={form.address2} + onChange={handleChange} /> City { - setUser(prevUser => ({ - ...prevUser, - city: e.target.value - })) - }} + value={form.city} + onChange={handleChange} /> State { - setUser(prevUser => ({ - ...prevUser, - state: e.target.value - })) - }} + name='state' + defaultValue='Choose...' + onChange={handleChange} + value={form.state} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + {usStates.map((state, index) => ( + + ))} Zip { - setUser(prevUser => ({ - ...prevUser, - zip: e.target.value - })) - }} + value={form.zip} + onChange={handleChange} /> { - setUser(prevUser => ({ - ...prevUser, - need_promotion: e.target.checked - })) - }} + checked={form.need_promotion ?? false} + onChange={handleChange} /> @@ -222,13 +241,9 @@ const UserProfileForm: React.FC = () => { Card Type { - setUser(prevUser => ({ - ...prevUser, - card_type: e.target.value - })) - }} + name='card_type' + value={form.card_type} + onChange={handleChange} > @@ -241,157 +256,93 @@ const UserProfileForm: React.FC = () => { Card Number { - setUser(prevUser => ({ - ...prevUser, - card_number: e.target.value - })) - }} + name='card_number' + value={form.card_number} + onChange={handleChange} /> Card Expiration { - setUser(prevUser => ({ - ...prevUser, - card_expiration: e.target.value - })) - }} + value={form.card_expiration} + onChange={handleChange} /> Address { - setUser(prevUser => ({ - ...prevUser, - card_address: e.target.value - })) - }} + value={form.card_address} + onChange={handleChange} /> Address 2 { - setUser(prevUser => ({ - ...prevUser, - card_address2: e.target.value - })) - }} + value={form.card_address2} + onChange={handleChange} /> City { - setUser(prevUser => ({ - ...prevUser, - card_city: e.target.value - })) - }} + name='card_city' + value={form.card_city} + onChange={handleChange} /> State { - setUser(prevUser => ({ - ...prevUser, - card_state: e.target.value - })) - }} + name='card_state' + defaultValue='Choose...' + onChange={handleChange} + value={form.card_state} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + {usStates.map((state, index) => ( + + ))} Zip { - setUser(prevUser => ({ - ...prevUser, - card_zip: e.target.value - })) - }} + name='card_zip' + value={form.card_zip} + onChange={handleChange} /> - + {success ? ( + + {success} + + ) : null} + {error ? ( + + {error} + + ) : null} diff --git a/src/pages/ForgotPassword/index.tsx b/src/pages/ForgotPassword/index.tsx new file mode 100644 index 0000000..06ec760 --- /dev/null +++ b/src/pages/ForgotPassword/index.tsx @@ -0,0 +1,58 @@ +import { useRequest } from 'ahooks' +import PageContainer from 'components/PageContainer' +import { useState } from 'react' +import { Col, Row, Form, Button, Alert } from 'react-bootstrap' +import Backend from 'utils/service' + +const ForgotPassword: React.FC = () => { + const [email, setEmail] = useState('') + const [info, setInfo] = useState('') + const { run } = useRequest( + async () => Backend.auth.v1AuthForgotpasswordCreate({ email }), + { + manual: true, + onSuccess: () => { + setInfo('Done! Check you email!') + } + } + ) + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + run() + } + return ( + +
+

Forgot Password

+
+ + {info ? {info} : null} +
+ + + Email + + + setEmail(e.target.value)} + /> + + + + + + + +
+ +
+ ) +} + +export default ForgotPassword diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx index da8bc36..30dff13 100644 --- a/src/pages/Login/index.tsx +++ b/src/pages/Login/index.tsx @@ -25,8 +25,8 @@ const LoginForm: React.FC = () => { return Backend.auth.v1AuthLoginCreate( { username, - password, - remember + password + // remember }, { redirect_url: `${DOMAIN_HOST}${from}` diff --git a/src/pages/Register/index.tsx b/src/pages/Register/index.tsx index d959ac8..946e308 100644 --- a/src/pages/Register/index.tsx +++ b/src/pages/Register/index.tsx @@ -214,7 +214,6 @@ const RegistrationForm = () => { name='address2' type='address2' placeholder='Apartment, studio, or floor' - required value={form.address2} onChange={handleChange} /> @@ -239,6 +238,7 @@ const RegistrationForm = () => { defaultValue='Choose...' onChange={handleChange} name='state' + value={form.state} > {usStates.map((state, index) => ( @@ -296,7 +296,6 @@ const RegistrationForm = () => { @@ -307,7 +306,6 @@ const RegistrationForm = () => { { { { { defaultValue='Choose...' onChange={handleChange} name='card_state' + value={form.card_state} > {usStates.map((state, index) => ( diff --git a/src/pages/ResetPassword/index.tsx b/src/pages/ResetPassword/index.tsx new file mode 100644 index 0000000..1c4e831 --- /dev/null +++ b/src/pages/ResetPassword/index.tsx @@ -0,0 +1,104 @@ +import { useRequest } from 'ahooks' +import PageContainer from 'components/PageContainer' +import { useAuth } from 'hooks/useAuth' +import { useState } from 'react' +import { Col, Row } from 'react-bootstrap' +import Button from 'react-bootstrap/Button' +import Form from 'react-bootstrap/Form' +import { Navigate, useNavigate, useSearchParams } from 'react-router-dom' +import { DOMAIN_HOST } from 'utils/constants' +import Backend from 'utils/service' + +const ResetForm: React.FC = () => { + const { user } = useAuth() + const [searchParams] = useSearchParams() + const [username, setUsername] = useState('') + const [newPassword, setNewPassword] = useState('') + const navigate = useNavigate() + const { run: resetPassword } = useRequest( + async () => { + const from = searchParams.get('from') ?? '/' + return Backend.auth.v1AuthResetpasswordCreate( + { + username, + newPassword + }, + { + redirect_url: `${DOMAIN_HOST}${from}` + } + ) + }, + { + manual: true, + onSuccess: () => { + navigate('/login') // Navigate to the Login page + } + } + ) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + resetPassword() + } + + return user ? ( + + ) : ( + <> +
+

Reset Password

+
+ +
+ + + Username + + + { + setUsername(e.target.value) + }} + /> + + + + + New Password + + + { + setNewPassword(e.target.value) + }} + /> + + + + + + + +
+ + + ) +} + +const Index: React.FC = () => ( + + + +) + +export default Index diff --git a/src/routes.tsx b/src/routes.tsx index d968f8f..394112a 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,8 +1,10 @@ import { ProtectedRoute } from 'components/ProtectedRoute' import Activate from 'pages/Activate' import Admin from 'pages/Admin' +import ChangePassword from 'pages/ChangePassword' import Checkout from 'pages/Checkout' import EditProfile from 'pages/EditProfile' +import ForgotPassword from 'pages/ForgotPassword' import Login from 'pages/Login' import Logout from 'pages/Logout' import Main from 'pages/Main' @@ -15,6 +17,7 @@ import OrderSummary from 'pages/OrderSummary' import PromoManage from 'pages/PromoManage' import Register from 'pages/Register' import RegisterConfirmation from 'pages/RegisterConfirmation' +import ResetPassword from 'pages/ResetPassword' import UserManage from 'pages/UserManage' import type { RouteObject } from 'react-router-dom' @@ -32,12 +35,16 @@ const children: RouteObject[] = [ element: }, { - path: '/activate', - element: + path: '/forgotPassword', + element: }, { - path: '/profile/edit', - element: + path: '/resetPassword', + element: + }, + { + path: '/activate', + element: }, { path: '/register/confirm', @@ -65,6 +72,14 @@ const children: RouteObject[] = [ { path: '/admin/promo', element: + }, + { + path: '/profile/edit', + element: + }, + { + path: '/changePassword', + element: } ] },