import React from 'react'; import superagent from 'superagent'; import { Modal, Button, Form, Message, Icon } from 'semantic-ui-react'; /** * Step-up verification flow for OAuth client mutations. * * Opened automatically when the host component catches a 428 * `step_up_required` response from a mutation. The flow: * * 1. User clicks "Send code". We POST to `/:id/step-up/challenge` * which emails a 6-digit code to their Bigscreen account. * 2. User enters the code in a text field. * 3. User clicks "Verify". We POST to `/:id/step-up/verify`. * On success, the modal closes and the host calls `onVerified()` * which should retry whatever mutation originally failed with 428. * * Props: * open bool whether the modal is visible * clientUniqueId string /admin/oauth/clients/:uniqueId — the scope of the unlock * clientName string for display * onClose () user dismissed without verifying * onVerified () verification succeeded — host should retry its mutation */ export default class StepUpModal extends React.Component { constructor(props) { super(props); this.state = this.initialState(); } initialState() { return { phase: 'request', // 'request' | 'enter_code' | 'verifying' | 'sent_error' challengeId: null, code: '', error: null, sending: false }; } componentDidUpdate(prevProps) { // Reset when the modal re-opens so a prior failure doesn't carry over. if (this.props.open && !prevProps.open) { this.setState(this.initialState()); } } async onSendCode() { const { clientUniqueId } = this.props; this.setState({ sending: true, error: null }); try { const res = await superagent .post(`/api/admin/oauth/clients/${clientUniqueId}/step-up/challenge`) .accept('json'); this.setState({ sending: false, phase: 'enter_code', challengeId: res.body.challengeId }); } catch (e) { const body = e.response?.body || {}; this.setState({ sending: false, error: body.error_description || body.error || e.message, phase: body.error === 'locked' ? 'request' : this.state.phase }); } } async onVerify() { const { clientUniqueId, onVerified } = this.props; const { challengeId, code } = this.state; if (!challengeId || !code) return; this.setState({ sending: true, error: null, phase: 'verifying' }); try { await superagent .post(`/api/admin/oauth/clients/${clientUniqueId}/step-up/verify`) .accept('json') .send({ challengeId, code: code.trim() }); this.setState({ sending: false }); if (onVerified) onVerified(); } catch (e) { const body = e.response?.body || {}; const reason = body.error || e.message; // `mismatch` keeps them in the same phase to retry; `locked` / // `expired_or_unknown` force a restart from Send code. const backToRequest = reason === 'locked' || reason === 'expired_or_unknown'; this.setState({ sending: false, error: reasonToMessage(reason), phase: backToRequest ? 'request' : 'enter_code', challengeId: backToRequest ? null : challengeId, code: '' }); } } renderRequestPhase() { return ( <>
To modify {this.props.clientName}, we need to verify it's you.
We'll email a 6-digit code to the address on your Bigscreen account. The code is valid for 10 minutes and unlocks mutations on this client only.
> ); } renderEnterCodePhase() { return ( <>