feat: Azure (Entra ID) login as user login (#16)
Replaces the client-side PIN login with real authentication against Azure Entra ID via PocketBase's built-in OAuth2 (OIDC) flow. Every Azure user is auto-provisioned as a team_member on first login. - pb_migrations: team_members becomes a PocketBase auth collection (OIDC provider configured from env); all collections require @request.auth.id - pb_hooks: provisioning of role (ENTRA_ADMIN_EMAILS allow-list) and enrollment_status, with admin-role re-sync on every login - frontend: "Sign in with Microsoft" login, pb.authStore-based session, TeamManager manages roster/roles (no PIN/manual create) - infra: Entra env wiring + pb_hooks mount for dev (Labs) and prod - src/lib/azureAuth.js + tests for the canonical role/allow-list logic - docs/auth-spec.md Knowledge base, generated tests and micro-learnings are left untouched. Existing PIN users are dropped (no migration required, per sign-off). Closes #16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,37 +2,25 @@ import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useApp } from '../store/AppContext';
|
||||
import Card from '../components/ui/Card';
|
||||
import Input from '../components/ui/Input';
|
||||
import Select from '../components/ui/Select';
|
||||
import Button from '../components/ui/Button';
|
||||
|
||||
const Login = () => {
|
||||
const { state, login } = useApp();
|
||||
const { loginWithAzure } = useApp();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState('');
|
||||
const [pin, setPin] = useState('');
|
||||
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const userOptions = [
|
||||
{ value: '', label: 'Select a user...' },
|
||||
...state.users.map(u => ({ value: u.id, label: u.name }))
|
||||
];
|
||||
|
||||
const handleLogin = (e) => {
|
||||
e.preventDefault();
|
||||
const handleAzure = async () => {
|
||||
setError('');
|
||||
|
||||
if (!selectedUser || !pin) {
|
||||
setError('Please fill in all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
const success = login(selectedUser, pin);
|
||||
if (success) {
|
||||
setBusy(true);
|
||||
try {
|
||||
await loginWithAzure();
|
||||
navigate('/');
|
||||
} else {
|
||||
setError('Incorrect PIN.');
|
||||
} catch (e) {
|
||||
// PocketBase throws when the popup is closed or the token exchange fails.
|
||||
setError(e?.message || 'Signing in with Microsoft failed. Please try again.');
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,31 +34,17 @@ const Login = () => {
|
||||
</div>
|
||||
|
||||
<Card className="border border-bg-warm">
|
||||
<form onSubmit={handleLogin} className="flex flex-col gap-5">
|
||||
<Select
|
||||
label="User"
|
||||
options={userOptions}
|
||||
value={selectedUser}
|
||||
onChange={(e) => setSelectedUser(e.target.value)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="PIN Code"
|
||||
type="password"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={4}
|
||||
placeholder="••••"
|
||||
value={pin}
|
||||
onChange={(e) => setPin(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-col gap-5">
|
||||
<p className="text-fg-muted text-sm text-center">
|
||||
Sign in with your Respellion Microsoft account.
|
||||
</p>
|
||||
|
||||
{error && <div className="text-red-500 text-sm font-medium">{error}</div>}
|
||||
{error && <div className="text-red-500 text-sm font-medium text-center">{error}</div>}
|
||||
|
||||
<Button type="submit" className="mt-2 w-full">
|
||||
Sign In
|
||||
<Button onClick={handleAzure} disabled={busy} className="w-full">
|
||||
{busy ? 'Signing in…' : 'Sign in with Microsoft'}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user