Skip to content

Commit

Permalink
wip: added ids change to account modal
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Dec 10, 2023
1 parent 0ebabd5 commit 197661b
Show file tree
Hide file tree
Showing 13 changed files with 373 additions and 45 deletions.
4 changes: 3 additions & 1 deletion core/components/WebServer/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export default (config: WebServerConfigType) => {
router.post('/auth/addMaster/save', authLimiter, webRoutes.auth_addMasterSave);
router.get('/auth/cfxre/redirect', authLimiter, webRoutes.auth_providerRedirect);
router.post('/auth/cfxre/callback', authLimiter, webRoutes.auth_providerCallback);
router.post('/changePassword', apiAuthMw, webRoutes.auth_changePassword);
router.post('/auth/changePassword', apiAuthMw, webRoutes.auth_changePassword);
router.get('/auth/getIdentifiers', apiAuthMw, webRoutes.auth_getIdentifiers);
router.post('/auth/changeIdentifiers', apiAuthMw, webRoutes.auth_changeIdentifiers);

//Admin Manager
router.post('/adminManager/getModal/:modalType', webAuthMw, webRoutes.adminManager_getModal);
Expand Down
90 changes: 90 additions & 0 deletions core/webroutes/authentication/changeIdentifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const modulename = 'WebServer:AuthChangeIdentifiers';
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';
import got from '@core/extras/got.js';
const console = consoleFactory(modulename);

//Helpers
const cfxHttpReqOptions = {
timeout: { request: 6000 },
};
type ProviderDataType = {id: string, identifier: string};

const bodySchema = z.object({
cfxreId: z.string(),
discordId: z.string(),
});
export type ApiChangeIdentifiersReqSchema = z.infer<typeof bodySchema>;

/**
* Route to change your own identifiers
*/
export default async function AuthChangeIdentifiers(ctx: AuthedCtx) {
//Sanity check
const schemaRes = bodySchema.safeParse(ctx.request.body);
if (!schemaRes.success) {
return ctx.send<GenericApiResp>({
error: `Invalid request body: ${schemaRes.error.message}`,
});
}
const { cfxreId, discordId } = schemaRes.data;

//Validate & translate FiveM ID
let citizenfxData: ProviderDataType | false = false;
if (cfxreId.length) {
try {
if (consts.validIdentifiers.fivem.test(cfxreId)) {
const id = cfxreId.split(':')[1];
const res = await got(`https://policy-live.fivem.net/api/getUserInfo/${id}`, cfxHttpReqOptions).json();
if (!res.username || !res.username.length) {
return ctx.send<GenericApiResp>({
error: `(ERR1) Invalid CitizenFX ID`,
});
}
citizenfxData = {
id: res.username,
identifier: cfxreId,
};
} else {
return ctx.send<GenericApiResp>({
error: `(ERR3) Invalid CitizenFX ID`,
});
}
} catch (error) {
return ctx.send<GenericApiResp>({
error: `Failed to resolve CitizenFX ID to game identifier with error: ${(error as Error).message}`,
});
}
}

//Validate Discord ID
let discordData: ProviderDataType | false = false;
if (discordId.length) {
if (!consts.validIdentifiers.discord.test(discordId)) {
return ctx.send<GenericApiResp>({
error: `Invalid Discord ID.`,
});
}
discordData = {
id: discordId.substring(8),
identifier: discordId,
};
}

//Get vault admin
const vaultAdmin = ctx.txAdmin.adminVault.getAdminByName(ctx.admin.name);
if (!vaultAdmin) throw new Error('Wait, what? Where is that admin?');

//Edit admin and give output
try {
await ctx.txAdmin.adminVault.editAdmin(ctx.admin.name, null, citizenfxData, discordData);

ctx.admin.logAction('Changing own identifiers.');
return ctx.send<GenericApiResp>({ success: true });
} catch (error) {
return ctx.send<GenericApiResp>({ error: (error as Error).message });
}
};
2 changes: 1 addition & 1 deletion core/webroutes/authentication/changePassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type ApiChangePasswordReqSchema = z.infer<typeof bodySchema>;


/**
* Returns the output page containing the admins.
* Route to change your own password
*/
export default async function AuthChangePassword(ctx: AuthedCtx) {
//Sanity check
Expand Down
27 changes: 27 additions & 0 deletions core/webroutes/authentication/getIdentifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const modulename = 'WebServer:AuthGetIdentifiers';
import { AuthedCtx } from '@core/components/WebServer/ctxTypes';
import consoleFactory from '@extras/console';
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 identifiers of the current admin
*/
export default async function AuthGetIdentifiers(ctx: AuthedCtx) {
//Get vault admin
const vaultAdmin = ctx.txAdmin.adminVault.getAdminByName(ctx.admin.name);
if (!vaultAdmin) throw new Error('Wait, what? Where is that admin?');

return ctx.send({
cfxreId: (vaultAdmin.providers.citizenfx) ? vaultAdmin.providers.citizenfx.identifier : '',
discordId: (vaultAdmin.providers.discord) ? vaultAdmin.providers.discord.identifier : '',
});
};
2 changes: 2 additions & 0 deletions core/webroutes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export { default as auth_verifyPassword } from './authentication/verifyPassword'
export { default as auth_changePassword } from './authentication/changePassword';
export { default as auth_self } from './authentication/self';
export { default as auth_logout } from './authentication/logout';
export { default as auth_getIdentifiers } from './authentication/getIdentifiers';
export { default as auth_changeIdentifiers } from './authentication/changeIdentifiers';

export { default as adminManager_page } from './adminManager/page.js';
export { default as adminManager_getModal } from './adminManager/getModal';
Expand Down
175 changes: 159 additions & 16 deletions panel/src/components/AccountDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,28 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAuth } from "@/hooks/auth";
import { useEffect, useState } from "react";
import { memo, 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 { ApiChangeIdentifiersReq, ApiChangePasswordReq } from "@shared/authApiTypes";
import { useAccountModal, useCloseAccountModal } from "@/hooks/dialogs";
import { ApiAuthErrorResp, GenericApiOkResp, GenericApiResp } from "@shared/genericApiTypes";
import { fetchWithTimeout, useAuthedFetcher, useBackendApi } from "@/hooks/fetch";
import consts from "@shared/consts";
import { txToast } from "./TxToaster";

import { useQuery } from "@tanstack/react-query";
import TxAnchor from "./TxAnchor";


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

const [oldPassword, setOldPassword] = useState('');
Expand Down Expand Up @@ -60,7 +62,6 @@ function ChangePasswordTab() {
},
success: (data) => {
setIsSaving(false);
if ('logout' in data) return;
if ('success' in data) {
if (authData.isTempPassword) {
setAccountModalTab('identifiers');
Expand All @@ -70,7 +71,7 @@ function ChangePasswordTab() {
});
} else {
txToast.success('Password changed successfully!');
setAccountModalOpen(false);
closeAccountModal();
}
} else {
setError(data.error)
Expand All @@ -89,7 +90,7 @@ function ChangePasswordTab() {
</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">
<div className="space-y-3 pt-2 pb-6">
{!authData.isTempPassword && (
<div className="space-y-1">
<Label htmlFor="current-password">Current Password</Label>
Expand Down Expand Up @@ -150,16 +151,159 @@ function ChangePasswordTab() {
</form>
</TabsContent>
);
}
})


/**
* Change Identifiers tab
*/
function ChangeIdentifiersTab() {
const authedFetcher = useAuthedFetcher();
const [cfxreId, setCfxreId] = useState('');
const [discordId, setDiscordId] = useState('');
const [error, setError] = useState('');
const [isConvertingFivemId, setIsConvertingFivemId] = useState(false);
const closeAccountModal = useCloseAccountModal();
const [isSaving, setIsSaving] = useState(false);

const { isPending: queryIsPending, error: queryError, data: queryData } = useQuery<ApiChangeIdentifiersReq>({
queryKey: ['getIdentifiers'],
gcTime: 30_000,
queryFn: () => authedFetcher('/auth/getIdentifiers'),
});

const changeIdentifiersApi = useBackendApi<GenericApiOkResp, ApiChangeIdentifiersReq>({
method: 'POST',
path: '/auth/changeIdentifiers'
});

useEffect(() => {
if (!queryData) return;
setCfxreId(queryData.cfxreId);
setDiscordId(queryData.discordId);
}, [queryData]);

useEffect(() => {
setError(queryError ? queryError.message : '');
}, [queryError]);

const handleSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault();
setError('');
setIsSaving(true);
changeIdentifiersApi({
data: { cfxreId, discordId },
error: (error) => {
setError(error);
},
success: (data) => {
setIsSaving(false);
if ('success' in data) {
txToast.success('Identifiers changed successfully!');
closeAccountModal();
} else {
setError(data.error)
}
}
});
};

const handleCfxreIdBlur = async () => {
if (!cfxreId) return;
const trimmed = cfxreId.trim();
if (/^\d+$/.test(trimmed)) {
setCfxreId(`fivem:${trimmed}`);
} else if (!trimmed.startsWith('fivem:')) {
try {
setIsConvertingFivemId(true);
const forumData = await fetchWithTimeout(`https://forum.cfx.re/u/${trimmed}.json`);
if (forumData.user && typeof forumData.user.id === 'number') {
setCfxreId(`fivem:${forumData.user.id}`);
} else {
setError('Could not find the user in the forum. Make sure you typed the username correctly.');
}
} catch (error) {
setError('Failed to check the identifiers on the forum API.');
}
setIsConvertingFivemId(false);
} else if (cfxreId !== trimmed) {
setCfxreId(trimmed);
}
}

const handleDiscordIdBlur = () => {
if (!discordId) return;
const trimmed = discordId.trim();
if (/^\d+$/.test(trimmed)) {
setDiscordId(`discord:${trimmed}`);
} else if (discordId !== trimmed) {
setDiscordId(trimmed);
}
}

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.
<form onSubmit={handleSubmit}>
<p className="text-sm text-muted-foreground">
The identifiers are optional for accessing the <strong>Web Panel</strong> but required for you to be able to use the <strong>In Game Menu</strong> and the <strong>Discord Bot</strong>. <br />
<strong>It is recommended that you configure at least one.</strong>
</p>
<div className="space-y-3 pt-2 pb-6">
<div className="space-y-1">
<Label htmlFor="cfxreId">FiveM identifier <span className="text-sm opacity-75 text-info">(optional)</span></Label>
<Input
id="cfxreId"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
placeholder="fivem:000000"
value={queryIsPending || isConvertingFivemId ? 'loading...' : cfxreId}
disabled={queryIsPending || isConvertingFivemId}
autoFocus
onBlur={handleCfxreIdBlur}
onChange={(e) => {
setCfxreId(e.target.value);
setError('');
}}
/>
<p className="text-sm text-muted-foreground">
Your identifier can be found by clicking in your name in the playerlist and going to the IDs page. <br />
You can also type in your <TxAnchor href="https://forum.cfx.re/">forum.cfx.re</TxAnchor> username and it will be converted automatically. <br />
This is required if you want to login using the Cfx.re button.
</p>
</div>
<div className="space-y-1">
<Label htmlFor="discordId">Discord identifier <span className="text-sm opacity-75 text-info">(optional)</span></Label>
<Input
id="discordId"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
placeholder="discord:000000000000000000"
value={queryIsPending ? 'loading...' : discordId}
disabled={queryIsPending}
onBlur={handleDiscordIdBlur}
onChange={(e) => {
setDiscordId(e.target.value);
setError('');
}}
/>
<p className="text-sm text-muted-foreground">
You can get your Discord User ID by following <TxAnchor href="https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID">this guide</TxAnchor>. <br />
This is required if you want to use the Discord Bot slash commands.
</p>
</div>
</div>

{error && <p className="text-destructive text-center -mt-2 mb-4">{error}</p>}
<Button
className="w-full"
type="submit"
disabled={!queryData || isSaving}
>
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</form>
</TabsContent>
);
}
Expand Down Expand Up @@ -204,7 +348,6 @@ export default function AccountDialog() {
{authData.isTempPassword ? 'Welcome to txAdmin!' : `Your Account - ${authData.name}`}
</DialogTitle>
</DialogHeader>

<Tabs
defaultValue="password"
value={accountModalTab}
Expand Down
6 changes: 6 additions & 0 deletions panel/src/hooks/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export const useOpenAccountModal = () => {
}
}

export const useCloseAccountModal = () => {
const setAccountModalOpen = useSetAtom(accountModalOpenAtom);
return () => {
setAccountModalOpen(false);
}
}


/**
Expand Down

0 comments on commit 197661b

Please sign in to comment.