Skip to content

Commit

Permalink
feat: added update password
Browse files Browse the repository at this point in the history
  • Loading branch information
ImJustChew committed Aug 1, 2024
1 parent f318a3e commit 959d3cb
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 23 deletions.
8 changes: 7 additions & 1 deletion src/app/[lang]/(mods-pages)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {GraduationCap, Hash} from 'lucide-react';
import ChangePasswordDialog from "@/components/Forms/ChangePasswordDialog";

const DisplaySettingsCard = () => {
const { darkMode, setDarkMode, language, setLanguage } = useSettings();
Expand Down Expand Up @@ -86,6 +87,8 @@ const TimetableSettingsCard = () => {
const AccountInfoSettingsCard = () => {
const { user, ais, setAISCredentials } = useHeadlessAIS();
const dict = useDictionary();
const [openChangePassword, setOpenChangePassword] = useState(false);

return <Card id="account">
<CardHeader>
<CardTitle>{dict.settings.account.title}</CardTitle>
Expand All @@ -103,7 +106,10 @@ const AccountInfoSettingsCard = () => {
<div className="text-gray-500 flex flex-row text-sm"><Hash className="w-4 h-4 mr-2" /> {user.studentid}</div>
</div>
</div>
<div className="flex flex-row justify-end items-center w-full">
<div className="flex flex-row justify-end items-center w-full gap-2">
<ChangePasswordDialog open={openChangePassword} setOpen={setOpenChangePassword}>
<Button variant={'ghost'}>更新密碼</Button>
</ChangePasswordDialog>
<Button variant="destructive" onClick={() => setAISCredentials()}>{dict.settings.account.signout}</Button>
</div>
</div>}
Expand Down
109 changes: 109 additions & 0 deletions src/components/Forms/ChangePasswordDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"

import { z } from "zod"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "../ui/form";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { PropsWithChildren } from "react";
import { useHeadlessAIS } from "@/hooks/contexts/useHeadlessAIS";
import { Loader2 } from "lucide-react";
import { toast } from "../ui/use-toast";

// * 8~16字元
// * 至少包含1大寫英文字母
// * 至少包含1小寫英文字母
// * 至少包含1個數字
// * 密碼三代不重覆
const formSchema = z.object({
newPassword: z.string().min(8).max(16).refine((value) => {
const hasLowerCase = /[a-z]/.test(value)
const hasUpperCase = /[A-Z]/.test(value)
const hasNumber = /[0-9]/.test(value)
return hasLowerCase && hasUpperCase && hasNumber
}, {
message: "Password must contain at least 1 uppercase letter, 1 lowercase letter, and 1 number",
}),
})

const ChangePasswordDialog = ({ open, setOpen, children }: PropsWithChildren<{ open: boolean, setOpen: (s: boolean) => void }>) => {
const { setAISCredentials, user } = useHeadlessAIS();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
newPassword: "",
},
})

async function onSubmit(values: z.infer<typeof formSchema>) {
if (!user) return;
await setAISCredentials(user.studentid, values.newPassword)
setOpen(false);
toast({
title: "密碼更新成功",
})
}

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>更新密碼</DialogTitle>
<DialogDescription>同學~你好像在校務資訊系統有跟新密碼哦,請也在這邊跟新!</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<Input placeholder="Password" type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant='destructive'>Logout</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>確定要登出嗎?</DialogTitle>
<DialogDescription>登出後將無法使用校務資訊系統相關功能,確定要登出嗎?</DialogDescription>
</DialogHeader>
<DialogClose asChild>
<Button >Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button variant='destructive' onClick={() => {
setAISCredentials("", "")
setOpen(false)
}}>Logout</Button>
</DialogClose>
</DialogContent>
</Dialog>
<Button type="submit">{form.formState.isSubmitting ? <Loader2 className="animate-spin" /> : "Submit"}</Button>
</form>
</Form>
</DialogContent>
</Dialog>

)
}

export default ChangePasswordDialog;
116 changes: 116 additions & 0 deletions src/components/Forms/UpdatePasswordDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"

import { z } from "zod"
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "../ui/form";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";



// * 8~16字元
// * 至少包含1大寫英文字母
// * 至少包含1小寫英文字母
// * 至少包含1個數字
// * 密碼三代不重覆
const formSchema = z.object({
oldPassword: z.string().min(8),
newPassword: z.string().min(8).max(16).refine((value) => {
const hasLowerCase = /[a-z]/.test(value)
const hasUpperCase = /[A-Z]/.test(value)
const hasNumber = /[0-9]/.test(value)
return hasLowerCase && hasUpperCase && hasNumber
}, {
message: "Password must contain at least 1 uppercase letter, 1 lowercase letter, and 1 number",
}),
confirmPassword: z.string().min(8).max(16)
}).refine((data) => data.newPassword === data.confirmPassword, {
message: "Passwords do not match",
});

const UpdatePasswordDialog = () => {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
oldPassword: "",
newPassword: "",
confirmPassword: "",
},
})

function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values)
}

return (
<Dialog>
<DialogTrigger>Update Password</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Password Update</DialogTitle>
<DialogDescription>
Due to the perfect security of changing your password, we will help you to update your password, while logged in.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="oldPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Old Password</FormLabel>
<FormControl>
<Input placeholder="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<Input placeholder="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input placeholder="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
</DialogContent>
</Dialog>

)
}

export default UpdatePasswordDialog;
36 changes: 29 additions & 7 deletions src/hooks/contexts/useHeadlessAIS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useDictionary from "@/dictionaries/useDictionary";
import { useCookies } from "react-cookie";
import { decodeJwt } from 'jose';
import { refreshUserSession, signInToCCXP } from '@/lib/headless_ais';
import dynamic from 'next/dynamic';
const headlessAISContext = createContext<ReturnType<typeof useHeadlessAISProvider>>({
user: undefined,
ais: {
Expand All @@ -18,9 +19,11 @@ const headlessAISContext = createContext<ReturnType<typeof useHeadlessAISProvide
initializing: true,
setAISCredentials: async () => false,
getACIXSTORE: async () => undefined,
openChangePassword: false,
setOpenChangePassword: () => {}
});


const ChangePasswordDialogDynamic = dynamic(() => import('@/components/Forms/ChangePasswordDialog'), { ssr: false });

const useHeadlessAISProvider = () => {
const [headlessAIS, setHeadlessAIS] = useLocalStorage<HeadlessAISStorage>("headless_ais", { enabled: false });
Expand All @@ -29,6 +32,7 @@ const useHeadlessAISProvider = () => {
const [error, setError] = useState<LoginError | undefined>(undefined);
const [cookies, setCookies, removeCookies, updateCookies] = useCookies(['accessToken']);
const dict = useDictionary();
const [openChangePassword, setOpenChangePassword] = useState(false);

useEffect(() => { setInitializing(false) }, []);

Expand Down Expand Up @@ -66,7 +70,12 @@ const useHeadlessAISProvider = () => {
password: res.encryptedPassword,
encrypted: true,
ACIXSTORE: res.ACIXSTORE,
lastUpdated: Date.now()
lastUpdated: Date.now(),
expired: res.passwordExpired
});
if(res.passwordExpired) toast({
title: "提醒您校務系統密碼已經過期~ ",
description: "但是NTHUMods 的功能都不會被影響 ヾ(≧▽≦*)o",
});
setLoading(false);
setError(undefined);
Expand All @@ -77,9 +86,6 @@ const useHeadlessAISProvider = () => {
title: "代理登入失敗",
description: dict.ccxp.errors[err.message as keyof typeof dict.ccxp.errors] ?? "目前認證服务降级,请稍后再试,敬請見諒",
})
setHeadlessAIS({
enabled: false
});
setLoading(false);
setError(err);
return false;
Expand Down Expand Up @@ -117,7 +123,12 @@ const useHeadlessAISProvider = () => {
password: res.encryptedPassword,
encrypted: true,
ACIXSTORE: res.ACIXSTORE,
lastUpdated: Date.now()
lastUpdated: Date.now(),
expired: res.passwordExpired
});
if(res.passwordExpired) toast({
title: "提醒您校務系統密碼已經過期~ ",
description: "但是NTHUMods 的功能都不會被影響 ヾ(≧▽≦*)o",
});
setLoading(false);
setError(undefined);
Expand All @@ -134,13 +145,21 @@ const useHeadlessAISProvider = () => {
password: headlessAIS.password,
encrypted: true,
ACIXSTORE: res.ACIXSTORE,
lastUpdated: Date.now()
lastUpdated: Date.now(),
expired: res.passwordExpired
});
if(res.passwordExpired) toast({
title: "提醒您校務系統密碼已經過期~ ",
description: "但是NTHUMods 的功能都不會被影響 ヾ(≧▽≦*)o",
});
setLoading(false);
setError(undefined);
return res.ACIXSTORE;
})
.catch(err => {
if(err.message == LoginError.IncorrectCredentials) {
setOpenChangePassword(true);
}
toast({
title: "代理登入失敗",
description: dict.ccxp.errors[err.message as keyof typeof dict.ccxp.errors] ?? "目前認證服务降级,请稍后再试,敬請見諒",
Expand All @@ -166,6 +185,8 @@ const useHeadlessAISProvider = () => {
setAISCredentials,
getACIXSTORE,
initializing,
openChangePassword,
setOpenChangePassword
};
}

Expand All @@ -178,6 +199,7 @@ const HeadlessAISProvider: FC<PropsWithChildren> = ({ children }) => {
return (
<headlessAISContext.Provider value={headlessAIS}>
{children}
<ChangePasswordDialogDynamic open={headlessAIS.openChangePassword} setOpen={headlessAIS.setOpenChangePassword}/>
</headlessAISContext.Provider>
);
};
Expand Down
Loading

0 comments on commit 959d3cb

Please sign in to comment.