141 lines
4.5 KiB
JavaScript
141 lines
4.5 KiB
JavaScript
import { useState, useCallback } from 'react';
|
|
import {
|
|
isWebAuthnAvailable,
|
|
preparePublicKeyCreationOptions,
|
|
serializeRegistrationCredential,
|
|
} from '../utils/webauthn';
|
|
|
|
const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }) => {
|
|
const [passkeys, setPasskeys] = useState([]);
|
|
const [passkeysSupported, setPasskeysSupported] = useState(null);
|
|
const [passkeysLoading, setPasskeysLoading] = useState(false);
|
|
const [registeringPasskey, setRegisteringPasskey] = useState(false);
|
|
const [revokingPasskeyId, setRevokingPasskeyId] = useState(null);
|
|
|
|
const refreshPasskeys = useCallback(async () => {
|
|
if (!token) {
|
|
return;
|
|
}
|
|
setPasskeysLoading(true);
|
|
try {
|
|
const { data } = await api.get('/profile/passkeys');
|
|
setPasskeys(Array.isArray(data) ? data : []);
|
|
setPasskeysSupported(true);
|
|
} catch (error) {
|
|
const status = error?.response?.status;
|
|
if (status === 400 || status === 404) {
|
|
setPasskeysSupported(false);
|
|
setPasskeys([]);
|
|
} else {
|
|
notifyApiError(error, 'Failed to load passkeys.');
|
|
}
|
|
} finally {
|
|
setPasskeysLoading(false);
|
|
}
|
|
}, [api, notifyApiError, token]);
|
|
|
|
const registerPasskey = useCallback(
|
|
async ({ nickname } = {}) => {
|
|
if (!isWebAuthnAvailable()) {
|
|
setPasskeysSupported(false);
|
|
setStatusMessage('Passkeys are not supported in this browser.', 'error');
|
|
return { ok: false, reason: 'unsupported' };
|
|
}
|
|
if (registeringPasskey) {
|
|
return { ok: false, reason: 'busy' };
|
|
}
|
|
|
|
setRegisteringPasskey(true);
|
|
try {
|
|
const { data } = await api.post('/auth/passkeys/register/start', {});
|
|
const challengeId = data?.challengeId || data?.challenge_id;
|
|
const publicKeyOptions =
|
|
data?.publicKey
|
|
|| data?.public_key
|
|
|| data?.challenge?.publicKey
|
|
|| data?.publicKeyCredentialCreationOptions;
|
|
|
|
if (!challengeId || !publicKeyOptions) {
|
|
throw new Error('Invalid passkey challenge response.');
|
|
}
|
|
|
|
const publicKey = preparePublicKeyCreationOptions({ publicKey: publicKeyOptions });
|
|
const credential = await navigator.credentials.create({ publicKey });
|
|
|
|
if (!credential) {
|
|
return { ok: false, reason: 'cancelled' };
|
|
}
|
|
|
|
const serialized = serializeRegistrationCredential(credential);
|
|
const payload = {
|
|
challengeId,
|
|
credential: serialized,
|
|
};
|
|
const trimmedNickname = nickname?.trim();
|
|
if (trimmedNickname) {
|
|
payload.nickname = trimmedNickname;
|
|
}
|
|
|
|
await api.post('/auth/passkeys/register/finish', payload);
|
|
await refreshPasskeys();
|
|
setPasskeysSupported(true);
|
|
setStatusMessage('Passkey registered.', 'success');
|
|
return { ok: true };
|
|
} catch (error) {
|
|
if (error?.name === 'NotAllowedError') {
|
|
setStatusMessage('Passkey registration cancelled.', 'info');
|
|
return { ok: false, reason: 'cancelled' };
|
|
}
|
|
|
|
const status = error?.response?.status;
|
|
if (status === 400 || status === 404) {
|
|
setPasskeysSupported(false);
|
|
}
|
|
|
|
const message = error?.response?.data?.error || 'Failed to register passkey.';
|
|
notifyApiError(error, message);
|
|
return { ok: false, reason: 'error', message };
|
|
} finally {
|
|
setRegisteringPasskey(false);
|
|
}
|
|
},
|
|
[api, notifyApiError, refreshPasskeys, registeringPasskey, setStatusMessage],
|
|
);
|
|
|
|
const revokePasskey = useCallback(
|
|
async (passkeyId, reason) => {
|
|
if (!passkeyId) {
|
|
return { ok: false, reason: 'missing-id' };
|
|
}
|
|
setRevokingPasskeyId(passkeyId);
|
|
try {
|
|
const query = reason ? `?reason=${encodeURIComponent(reason)}` : '';
|
|
await api.delete(`/profile/passkeys/${passkeyId}${query}`);
|
|
await refreshPasskeys();
|
|
setStatusMessage('Passkey revoked.', 'success');
|
|
return { ok: true };
|
|
} catch (error) {
|
|
const message = error?.response?.data?.error || 'Failed to revoke passkey.';
|
|
notifyApiError(error, message);
|
|
return { ok: false, reason: 'error', message };
|
|
} finally {
|
|
setRevokingPasskeyId(null);
|
|
}
|
|
},
|
|
[api, notifyApiError, refreshPasskeys, setStatusMessage],
|
|
);
|
|
|
|
return {
|
|
passkeys,
|
|
passkeysSupported,
|
|
passkeysLoading,
|
|
registeringPasskey,
|
|
revokingPasskeyId,
|
|
refreshPasskeys,
|
|
registerPasskey,
|
|
revokePasskey,
|
|
};
|
|
};
|
|
|
|
export default usePasskeys;
|