diff --git a/web/normogen-web/src/pages/RegisterPage.tsx b/web/normogen-web/src/pages/RegisterPage.tsx
index 5da1fc5..fcd8d7b 100644
--- a/web/normogen-web/src/pages/RegisterPage.tsx
+++ b/web/normogen-web/src/pages/RegisterPage.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import { useState, type FC } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
Container,
@@ -12,14 +12,15 @@ import {
} from '@mui/material';
import { useAuthStore } from '../store/useStore';
-export const RegisterPage: React.FC = () => {
+export const RegisterPage: FC = () => {
const navigate = useNavigate();
+ const { register, isLoading, error, clearError } = useAuthStore();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
+ const [recoveryPhrase, setRecoveryPhrase] = useState('');
const [passwordError, setPasswordError] = useState('');
- const { register, isLoading, error, clearError } = useAuthStore();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -30,54 +31,32 @@ export const RegisterPage: React.FC = () => {
setPasswordError('Passwords do not match');
return;
}
-
- if (password.length < 6) {
- setPasswordError('Password must be at least 6 characters');
+ if (password.length < 8) {
+ setPasswordError('Password must be at least 8 characters');
return;
}
-
+
try {
- await register(username, email, password);
+ // Pass the recovery phrase (optional) to the store; the store derives
+ // the wrapped-DEK forms from it.
+ await register(username, email, password, recoveryPhrase || undefined);
navigate('/');
- } catch (err) {
- // Error is handled by the store
+ } catch {
+ /* error surfaced via store */
}
};
return (
-
-
+
+
Create Account
-
- Join Normogen Healthcare Platform
-
- {error && (
+ {(error || passwordError) && (
- {error}
-
- )}
-
- {passwordError && (
-
- {passwordError}
+ {error || passwordError}
)}
@@ -86,23 +65,18 @@ export const RegisterPage: React.FC = () => {
margin="normal"
required
fullWidth
- id="username"
label="Username"
- name="username"
- autoComplete="username"
- autoFocus
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={isLoading}
+ autoFocus
/>
setEmail(e.target.value)}
disabled={isLoading}
@@ -111,11 +85,8 @@ export const RegisterPage: React.FC = () => {
margin="normal"
required
fullWidth
- name="password"
label="Password"
type="password"
- id="password"
- autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
@@ -124,14 +95,21 @@ export const RegisterPage: React.FC = () => {
margin="normal"
required
fullWidth
- name="confirmPassword"
label="Confirm Password"
type="password"
- id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
disabled={isLoading}
/>
+ setRecoveryPhrase(e.target.value)}
+ disabled={isLoading}
+ helperText="If you forget your password, this phrase is your only way to recover your encrypted data. Save it somewhere safe."
+ />
);
};
+
+export default RegisterPage;
diff --git a/web/normogen-web/src/store/useStore.ts b/web/normogen-web/src/store/useStore.ts
index 4d460fa..32b1fe2 100644
--- a/web/normogen-web/src/store/useStore.ts
+++ b/web/normogen-web/src/store/useStore.ts
@@ -35,7 +35,7 @@ interface AuthState {
// Actions
login: (email: string, password: string) => Promise;
- register: (username: string, email: string, password: string) => Promise;
+ register: (username: string, email: string, password: string, recoveryPhrase?: string) => Promise;
recover: (email: string, recoveryPhrase: string, newPassword: string) => Promise;
logout: () => Promise;
clearError: () => void;
@@ -162,19 +162,25 @@ export const useAuthStore = create()(
}
},
- register: async (username: string, email: string, password: string) => {
+ register: async (username: string, email: string, password: string, recoveryPhrase?: string) => {
set({ isLoading: true, error: null });
try {
- const { authSecret, encKey } = await deriveAuthAndEncKeys(password);
- setEncKey(encKey);
- // For now register uses the Phase 1 key derivation (no recovery phrase
- // in the basic register flow). The wrapped-DEK recovery fields are
- // optional on the register request.
+ // Zero-knowledge: generate a DEK, wrap it under the password KEK
+ // and (if provided) the recovery KEK. Send the wrapped forms + the
+ // recovery proof to the server; the server stores them verbatim.
+ const setup = await setupEncryption(password, recoveryPhrase);
+ setEncKey(setup.dek);
+
const response = await apiService.register({
username,
email,
- password: authSecret,
- });
+ password: setup.authSecret,
+ wrapped_dek: setup.passwordWrappedDek.data,
+ wrapped_dek_iv: setup.passwordWrappedDek.iv,
+ recovery_wrapped_dek: setup.recoveryWrappedDek?.data,
+ recovery_wrapped_dek_iv: setup.recoveryWrappedDek?.iv,
+ recovery_phrase: setup.recoveryKekHash,
+ } as any);
set({
user: {
user_id: response.user_id,