Skip to content

Commit

Permalink
wip: "your account" modal - change password
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Dec 9, 2023
1 parent 263c5b1 commit 5043d43
Show file tree
Hide file tree
Showing 10 changed files with 378 additions and 32 deletions.
52 changes: 32 additions & 20 deletions core/webroutes/authentication/changePassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,57 @@ const modulename = 'WebServer:AuthChangePassword';
import { AuthedCtx } from '@core/components/WebServer/ctxTypes';
import consoleFactory from '@extras/console';
import consts from '@shared/consts';
import { GenericApiResp } from '@shared/genericApiTypes';
import { z } from 'zod';
const console = consoleFactory(modulename);

//Helper functions
const bodySchema = z.object({
oldPassword: z.string().optional(),
newPassword: z.string(),
});
export type ApiChangePasswordReqSchema = z.infer<typeof bodySchema>;


/**
* Returns the output page containing the admins.
*/
export default async function AuthChangePassword(ctx: AuthedCtx) {
//Sanity check
if (typeof ctx.request?.body?.newPassword !== 'string') {
return ctx.utils.error(400, 'Invalid Request');
const schemaRes = bodySchema.safeParse(ctx.request.body);
if (!schemaRes.success) {
return ctx.send<GenericApiResp>({
error: `Invalid request body: ${schemaRes.error.message}`,
});
}
const { newPassword, oldPassword } = schemaRes.data;

//Check if temp password
if (!ctx.admin.isTempPassword && typeof ctx.request.body.oldPassword !== 'string') {
return ctx.send({type: 'danger', message: 'The permanent password was already set.'});
//Validate new password
if (newPassword.trim() !== newPassword) {
return ctx.send<GenericApiResp>({
error: 'Your password either starts or ends with a space, which was likely an accident. Please remove it and try again.',
});
}
if (newPassword.length < consts.adminPasswordMinLength || newPassword.length > consts.adminPasswordMaxLength) {
return ctx.send<GenericApiResp>({ error: 'Invalid new password length.' });
}

//Validate fields
const newPassword = ctx.request.body.newPassword.trim();
if (!ctx.admin.isTempPassword && ctx.request.body?.oldPassword !== undefined) {
const vaultAdmin = ctx.txAdmin.adminVault.getAdminByName(ctx.admin.name);
if (!vaultAdmin) throw new Error('Wait, what? Where is that admin?');
const oldPassword = ctx.request.body.oldPassword.trim();
if (!VerifyPasswordHash(oldPassword, vaultAdmin.password_hash)) {
return ctx.send({type: 'danger', message: 'Wrong current password'});
//Get vault admin
const vaultAdmin = ctx.txAdmin.adminVault.getAdminByName(ctx.admin.name);
if (!vaultAdmin) throw new Error('Wait, what? Where is that admin?');
if (!ctx.admin.isTempPassword) {
if (!oldPassword || !VerifyPasswordHash(oldPassword, vaultAdmin.password_hash)) {
return ctx.send<GenericApiResp>({ error: 'Wrong current password.' });
}
}
if (newPassword.length < consts.adminPasswordMinLength || newPassword.length > consts.adminPasswordMaxLength) {
return ctx.send({type: 'danger', message: 'Invalid new password length.'});
}

//Add admin and give output
//Edit admin and give output
try {
const newHash = await ctx.txAdmin.adminVault.editAdmin(ctx.admin.name, newPassword);

//Update session hash if logged in via password
const currSess = ctx.sessTools.get();
if(currSess?.auth?.type === 'password') {
if (currSess?.auth?.type === 'password') {
ctx.sessTools.set({
auth: {
...currSess.auth,
Expand All @@ -50,8 +62,8 @@ export default async function AuthChangePassword(ctx: AuthedCtx) {
}

ctx.admin.logAction('Changing own password.');
return ctx.send({type: 'success', message: 'Password changed successfully'});
return ctx.send<GenericApiResp>({ success: true });
} catch (error) {
return ctx.send({type: 'danger', message: (error as Error).message});
return ctx.send<GenericApiResp>({ error: (error as Error).message });
}
};
12 changes: 6 additions & 6 deletions docs/dev_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ Processo:
- [x][2d] useBackendApi hook - wrapper around fetch with optional toast management
- [x][2h] server controls
- [x][1h] server scheduled restarts (legacy style)
- [ ][3d] "my account" modal
- if isTempPassword change message and disallows closing before changing the password
- give the chance to change modifiers
- remove legacy header + change password code
- admin manager should open "my account" when trying to edit self
- maybe separate the backend routes
- [ ][3d] "your account" modal
- [x] if isTempPassword change message and disallows closing before changing the password
- [ ] give the chance to change modifiers
- [ ] remove legacy header + change password code
- [ ] admin manager should open "my account" when trying to edit self
- [ ] maybe separate the backend routes
- [ ][3d] playerlist
- [ ][1d] add the new logos to shell+auth pages
- [ ][3h] playerlist click opens legacy player modal (`iframe.contentWindow.postMessage("openModal", ???);`)
Expand Down
31 changes: 31 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions panel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.0.5",
Expand Down
223 changes: 223 additions & 0 deletions panel/src/components/AccountDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent, DialogHeader,
DialogTitle
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAuth } from "@/hooks/auth";
import { useEffect, useState } from "react";
import { TabsTrigger, TabsList, TabsContent, Tabs } from "@/components/ui/tabs";
import { ApiChangePasswordReq } from "@shared/authApiTypes";
import { useAccountModal } from "@/hooks/dialogs";
import { GenericApiResp } from "@shared/genericApiTypes";
import { useBackendApi } from "@/hooks/useBackendApi";
import consts from "@shared/consts";
import { txToast } from "./TxToaster";



/**
* Change Password tab
*/
function ChangePasswordTab() {
const { authData, setAuthData } = useAuth();
const { setAccountModalOpen, setAccountModalTab } = useAccountModal();
const changePasswordApi = useBackendApi<GenericApiResp, ApiChangePasswordReq>({
method: 'POST',
path: '/changePassword'
});

const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newPasswordConfirm, setNewPasswordConfirm] = useState('');
const [error, setError] = useState('');
const [isSaving, setIsSaving] = useState(false);

const handleSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault();
if (!authData) return;
setError('');

if (newPassword.length < consts.adminPasswordMinLength || newPassword.length > consts.adminPasswordMaxLength) {
setError(`The password must be between ${consts.adminPasswordMinLength} and ${consts.adminPasswordMaxLength} digits long.`);
return;
} else if (newPassword !== newPasswordConfirm) {
setError('The passwords do not match.');
return;
}

setIsSaving(true);
changePasswordApi({
data: {
newPassword,
oldPassword: authData.isTempPassword ? undefined : oldPassword,
},
error: (error) => {
setIsSaving(false);
setError(error);
},
success: (data) => {
setIsSaving(false);
if ('logout' in data) return;
if ('success' in data) {
if (authData.isTempPassword) {
setAccountModalTab('identifiers');
setAuthData({
...authData,
isTempPassword: false,
});
} else {
txToast.success('Password changed successfully!');
setAccountModalOpen(false);
}
} else {
setError(data.error)
}
}
});
};

if (!authData) return;
return (
<TabsContent value="password" tabIndex={undefined}>
<form onSubmit={handleSubmit}>
{authData.isTempPassword ? (<p className="text-sm text-warning">
Your account has a temporary password that needs to be changed before you can use this web panel. <br />
<strong>Make sure to take note of your new password before saving.</strong>
</p>) : (<p className="text-sm text-muted-foreground">
You can use your password to login to the txAdmin inferface even without using the Cfx.re login button.
</p>)}
<div className="space-y-2 pt-2 pb-6">
{!authData.isTempPassword && (
<div className="space-y-1">
<Label htmlFor="current-password">Current Password</Label>
<Input
id="current-password"
placeholder="Enter new password"
type="password"
value={oldPassword}
autoFocus
required
onChange={(e) => {
setOldPassword(e.target.value);
setError('');
}}
/>
</div>
)}
<div className="space-y-1">
<Label htmlFor="new-password">New Password</Label>
<Input
id="new-password"
autoComplete="new-password"
placeholder="Enter new password"
type="password"
value={newPassword}
autoFocus={authData.isTempPassword}
required
onChange={(e) => {
setNewPassword(e.target.value);
setError('');
}}
/>
</div>
<div className="space-y-1">
<Label htmlFor="confirm-password">Confirm Password</Label>
<Input
id="confirm-password"
autoComplete="new-password"
placeholder="Repeat new password"
type="password"
required
onChange={(e) => {
setNewPasswordConfirm(e.target.value);
setError('');
}}
/>
</div>
</div>

{error && <p className="text-destructive text-center -mt-2 mb-4">{error}</p>}
<Button
className="w-full"
type="submit"
disabled={isSaving}
>
{isSaving ? 'Saving...' : authData.isTempPassword ? 'Save & Next' : 'Change Password'}
</Button>
</form>
</TabsContent>
);
}


/**
* Change Identifiers tab
*/
function ChangeIdentifiersTab() {
return (
<TabsContent value="identifiers" tabIndex={undefined}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</TabsContent>
);
}


/**
* Account Dialog
*/
export default function AccountDialog() {
const { authData } = useAuth();
const {
isAccountModalOpen, setAccountModalOpen,
accountModalTab, setAccountModalTab
} = useAccountModal();

useEffect(() => {
if (!authData) return;
if (authData.isTempPassword) {
setAccountModalOpen(true);
setAccountModalTab('password');
}
}, []);

const dialogSetIsClose = (newState: boolean) => {
if (!newState && authData && !authData.isTempPassword) {
setAccountModalOpen(false);
setTimeout(() => {
setAccountModalTab('password');
}, 500);
}
}

if (!authData) return;
return (
<Dialog
open={isAccountModalOpen}
onOpenChange={dialogSetIsClose}
>
<DialogContent className="sm:max-w-lg" tabIndex={undefined}>
<DialogHeader>
<DialogTitle className="text-2xl font-bold">
{authData.isTempPassword ? 'Welcome to txAdmin!' : `Your Account - ${authData.name}`}
</DialogTitle>
</DialogHeader>

<Tabs
defaultValue="password"
value={accountModalTab}
onValueChange={setAccountModalTab}
>
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="identifiers" disabled={authData.isTempPassword}>Identifiers</TabsTrigger>
</TabsList>
<ChangePasswordTab />
<ChangeIdentifiersTab />
</Tabs>
</DialogContent>
</Dialog>
);
}

0 comments on commit 5043d43

Please sign in to comment.