@@ -14,17 +14,24 @@ import {QrCodeComponent} from "@core/components/QrCodeComponent";
1414import { TabContent } from "@code0-tech/pictor/dist/components/tab/Tab" ;
1515import 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