Skip to content

Commit e3d5f7c

Browse files
committed
feat: enhance UserMfaView with dynamic issuer handling for TOTP and backup codes
1 parent a2188e8 commit e3d5f7c

1 file changed

Lines changed: 17 additions & 9 deletions

File tree

src/packages/ce/src/user/views/UserMfaView.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,24 @@ import {QrCodeComponent} from "@core/components/QrCodeComponent";
1414
import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab";
1515
import CardSection from "@code0-tech/pictor/dist/components/card/CardSection";
1616

17-
const TOTP_ISSUER = "codezero.build"
17+
// Fallback used during SSR (no `window`) or when the hostname can't be read.
18+
const DEFAULT_ISSUER = "codezero.build"
1819

19-
const buildOtpAuthUri = (secret: string, account: string): string => {
20-
const label = encodeURIComponent(`${TOTP_ISSUER}:${account}`)
21-
const params = new URLSearchParams({secret, issuer: TOTP_ISSUER})
20+
// The issuer identifies which instance a TOTP entry belongs to in the
21+
// authenticator app. Derive it from the hostname the user accessed the app
22+
// over so entries from different instances stay distinguishable.
23+
const resolveIssuer = (): string =>
24+
(typeof window !== "undefined" && window.location.hostname) || DEFAULT_ISSUER
25+
26+
const buildOtpAuthUri = (secret: string, account: string, issuer: string): string => {
27+
const label = encodeURIComponent(`${issuer}:${account}`)
28+
const params = new URLSearchParams({secret, issuer})
2229
return `otpauth://totp/${label}?${params.toString()}`
2330
}
2431

25-
const downloadBackupCodes = (codes: string[]): void => {
32+
const downloadBackupCodes = (codes: string[], issuer: string): void => {
2633
const content = [
27-
`${TOTP_ISSUER} backup codes`,
34+
`${issuer} backup codes`,
2835
`Generated: ${new Date().toLocaleString()}`,
2936
"",
3037
...codes,
@@ -37,7 +44,7 @@ const downloadBackupCodes = (codes: string[]): void => {
3744
const url = URL.createObjectURL(blob)
3845
const anchor = document.createElement("a")
3946
anchor.href = url
40-
anchor.download = "codezero-backup-codes.txt"
47+
anchor.download = `${issuer}-backup-codes.txt`
4148
anchor.click()
4249
URL.revokeObjectURL(url)
4350
}
@@ -63,6 +70,7 @@ export const UserMfaView: React.FC = () => {
6370
const totpEnabled = user?.mfaStatus?.totpEnabled ?? false
6471
const backupCodesCount = user?.mfaStatus?.backupCodesCount ?? 0
6572
const account = user?.email ?? user?.username ?? "account"
73+
const issuer = React.useMemo(resolveIssuer, [])
6674

6775
const initialValues = React.useMemo(() => ({code: ""}), [])
6876

@@ -128,7 +136,7 @@ export const UserMfaView: React.FC = () => {
128136
<Spacing spacing={"md"}/>
129137
<Flex style={{gap: ".7rem", textWrap: "nowrap"}}>
130138
<Flex style={{gap: ".35rem"}}>
131-
<Button color={"primary"} onClick={() => downloadBackupCodes(backupCodes)}>
139+
<Button color={"primary"} onClick={() => downloadBackupCodes(backupCodes, issuer)}>
132140
<IconDownload size={16}/>
133141
Download backup codes
134142
</Button>
@@ -173,7 +181,7 @@ export const UserMfaView: React.FC = () => {
173181
</InputWrapper>
174182
</div>
175183
<div>
176-
<QrCodeComponent value={buildOtpAuthUri(secret, account)} size={132}/>
184+
<QrCodeComponent value={buildOtpAuthUri(secret, account, issuer)} size={132}/>
177185
</div>
178186
</Flex>
179187
</CardSection>

0 commit comments

Comments
 (0)