Initial commit

This commit is contained in:
Grae Jones
2026-02-03 15:45:39 -08:00
commit 3647b304a3
74 changed files with 27121 additions and 0 deletions

View File

@@ -0,0 +1,321 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { PublicClientApplication } from '@azure/msal-browser';
// ─── Config ───
const TENANT_ID = '891f98f1-ed34-42a1-9b6c-28b0554d92c2';
const CLIENT_ID = '154c9111-14a0-4c0f-8132-7bc68254a74e';
const AUTHORITY = `https://USIMClients.ciamlogin.com/${TENANT_ID}`;
const GATEWAY_URL = 'https://adsapi.usimdev.com';
const CLIENT_APP = 'https://adpclient.usimdev.com';
const msalConfig = {
auth: {
clientId: CLIENT_ID,
authority: AUTHORITY,
redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin,
knownAuthorities: ['USIMClients.ciamlogin.com'],
navigateToLoginRequestUrl: true
},
cache: { cacheLocation: 'sessionStorage', storeAuthStateInCookie: false },
system: {
loggerOptions: {
loggerCallback: (level, msg, pii) => { if (!pii && level <= 1) console.warn(msg); },
logLevel: 1
}
}
};
const loginRequest = { scopes: ['openid', 'profile', 'email'] };
const msalInstance = new PublicClientApplication(msalConfig);
const msalReady = msalInstance.initialize();
// ─── App ───
export default function App() {
// States: loading | unauthenticated | form | submitting | success | error
const [state, setState] = useState('loading');
const [jwt, setJwt] = useState(null);
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
const [form, setForm] = useState({ companyName: '', industry: '', website: '' });
const initRef = useRef(false);
// ─── Initialize MSAL & authenticate ───
useEffect(() => {
if (initRef.current) return;
initRef.current = true;
(async () => {
try {
await msalReady;
console.log('[Reg] MSAL initialized');
// Handle redirect response first
const redirectResult = await msalInstance.handleRedirectPromise();
if (redirectResult?.accessToken || redirectResult?.idToken) {
const token = redirectResult.accessToken || redirectResult.idToken;
console.log('[Reg] Got token from redirect');
setJwt(token);
setUser({
name: redirectResult.account?.name,
email: redirectResult.account?.username
});
setState('form');
return;
}
// Try silent token acquisition (SSO from client app)
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
try {
const silent = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0]
});
console.log('[Reg] Silent token acquired');
setJwt(silent.accessToken || silent.idToken);
setUser({ name: accounts[0].name, email: accounts[0].username });
setState('form');
return;
} catch (e) {
console.warn('[Reg] Silent failed, need interaction:', e.message);
}
}
setState('unauthenticated');
} catch (err) {
console.error('[Reg] Init error:', err);
setError(err.message);
setState('error');
}
})();
}, []);
// ─── Sign in ───
const signIn = useCallback(async () => {
try {
setState('loading');
await msalInstance.loginRedirect(loginRequest);
} catch (err) {
setError(err.message);
setState('error');
}
}, []);
// ─── Submit registration ───
const handleSubmit = useCallback(async (e) => {
e.preventDefault();
if (!form.companyName.trim()) return;
setState('submitting');
setError(null);
try {
const res = await fetch(`${GATEWAY_URL}/api/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwt}`
},
body: JSON.stringify({
companyName: form.companyName.trim(),
industry: form.industry.trim() || null,
website: form.website.trim() || null
})
});
const body = await res.json();
console.log('[Reg] Register response:', res.status, body);
if (body.ok) {
setState('success');
// Redirect to client app after short delay
setTimeout(() => {
window.location.href = CLIENT_APP;
}, 3000);
} else {
setError(body.error || 'Registration failed');
setState('form');
}
} catch (err) {
console.error('[Reg] Submit error:', err);
setError('Unable to connect to the server. Please try again.');
setState('form');
}
}, [jwt, form]);
const updateField = (field) => (e) => setForm({ ...form, [field]: e.target.value });
// ─── Render ───
return (
<div className="reg-page">
<header className="reg-header">
<div className="reg-logo" onClick={() => window.location.href = CLIENT_APP}>
<div className="reg-logo-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<span>AdPlatform</span>
</div>
</header>
<main className="reg-main">
{/* Loading */}
{state === 'loading' && (
<div className="reg-card center">
<div className="spinner" />
<p className="reg-text-muted">Preparing your account</p>
</div>
)}
{/* Unauthenticated */}
{state === 'unauthenticated' && (
<div className="reg-card">
<div className="reg-card-header">
<div className="step-badge">Step 1 of 2</div>
<h1>Welcome to AdPlatform</h1>
<p>Sign in with your Microsoft, Google, or Apple account to get started.</p>
</div>
<button className="btn btn-primary btn-lg btn-full" onClick={signIn}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4M10 17l5-5-5-5M15 12H3" />
</svg>
Sign In to Continue
</button>
<p className="reg-text-muted small">You'll be redirected to Microsoft's sign-in page. If you already have an account, you'll be signed in automatically.</p>
</div>
)}
{/* Registration Form */}
{state === 'form' && (
<div className="reg-card">
<div className="reg-card-header">
<div className="step-badge">Step 2 of 2</div>
<h1>Set Up Your Account</h1>
<p>Tell us about your organization to complete registration.</p>
</div>
{user && (
<div className="reg-user-info">
<div className="reg-user-avatar">{(user.name || user.email || 'U')[0].toUpperCase()}</div>
<div>
<div className="reg-user-name">{user.name || 'User'}</div>
<div className="reg-user-email">{user.email}</div>
</div>
</div>
)}
{error && (
<div className="reg-error">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/></svg>
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Organization / Company Name <span className="required">*</span></label>
<input
type="text"
className="form-input"
placeholder="e.g. Acme Marketing Inc."
value={form.companyName}
onChange={updateField('companyName')}
autoFocus
required
/>
<span className="form-hint">This will be your client name in AdPlatform.</span>
</div>
<div className="form-group">
<label>Industry</label>
<select className="form-input" value={form.industry} onChange={updateField('industry')}>
<option value="">Select an industry (optional)</option>
<option value="Retail / E-commerce">Retail / E-commerce</option>
<option value="Technology / SaaS">Technology / SaaS</option>
<option value="Healthcare">Healthcare</option>
<option value="Finance / Insurance">Finance / Insurance</option>
<option value="Real Estate">Real Estate</option>
<option value="Education">Education</option>
<option value="Travel / Hospitality">Travel / Hospitality</option>
<option value="Food / Restaurant">Food / Restaurant</option>
<option value="Professional Services">Professional Services</option>
<option value="Non-profit">Non-profit</option>
<option value="Other">Other</option>
</select>
</div>
<div className="form-group">
<label>Website</label>
<input
type="url"
className="form-input"
placeholder="https://www.example.com"
value={form.website}
onChange={updateField('website')}
/>
</div>
<button type="submit" className="btn btn-primary btn-lg btn-full" disabled={!form.companyName.trim()}>
Complete Registration
</button>
<p className="reg-text-muted small center-text">
By registering, your advertising account will be managed under the USIM agency umbrella.
</p>
</form>
</div>
)}
{/* Submitting */}
{state === 'submitting' && (
<div className="reg-card center">
<div className="spinner" />
<h2>Creating Your Account</h2>
<p className="reg-text-muted">Setting up {form.companyName}…</p>
</div>
)}
{/* Success */}
{state === 'success' && (
<div className="reg-card center">
<div className="success-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
</div>
<h1>You're All Set!</h1>
<p className="reg-text-muted">
<strong>{form.companyName}</strong> has been registered. Redirecting you to the dashboard
</p>
<div className="progress-bar"><div className="progress-fill" /></div>
<a href={CLIENT_APP} className="btn btn-outline">Go to Dashboard Now</a>
</div>
)}
{/* Error */}
{state === 'error' && (
<div className="reg-card center">
<div className="error-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" />
</svg>
</div>
<h2>Something Went Wrong</h2>
<p className="reg-text-muted">{error}</p>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
<button className="btn btn-primary" onClick={() => { setError(null); setState('unauthenticated'); }}>Try Again</button>
<a href={CLIENT_APP} className="btn btn-outline">Back to Home</a>
</div>
</div>
)}
</main>
<footer className="reg-footer">
<span>Powered by <strong>USIM</strong></span>
</footer>
</div>
);
}

View File

@@ -0,0 +1,6 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles.css';
createRoot(document.getElementById('root')).render(<App />);

View File

@@ -0,0 +1,327 @@
/* ─── Registration Portal Styles ─── */
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-primary-light: #eff6ff;
--color-success: #059669;
--color-success-light: #ecfdf5;
--color-error: #dc2626;
--color-error-light: #fef2f2;
--color-bg: #f8fafc;
--color-surface: #ffffff;
--color-border: #e2e8f0;
--color-text: #0f172a;
--color-text-secondary: #475569;
--color-text-muted: #94a3b8;
--radius: 12px;
--radius-sm: 8px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.04);
--shadow-lg: 0 4px 24px rgba(0,0,0,0.08), 0 12px 48px rgba(0,0,0,0.04);
}
body {
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* ─── Page Layout ─── */
.reg-page {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.reg-header {
padding: 20px 32px;
display: flex;
align-items: center;
}
.reg-logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 700;
color: var(--color-text);
cursor: pointer;
text-decoration: none;
}
.reg-logo-icon {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--color-primary), #3b82f6);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.reg-logo-icon svg { width: 20px; height: 20px; }
.reg-main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.reg-footer {
padding: 20px 32px;
text-align: center;
font-size: 13px;
color: var(--color-text-muted);
}
/* ─── Card ─── */
.reg-card {
width: 100%;
max-width: 480px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
padding: 40px;
}
.reg-card.center { text-align: center; }
.reg-card-header {
margin-bottom: 28px;
}
.reg-card-header h1 {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
letter-spacing: -0.02em;
}
.reg-card-header p {
color: var(--color-text-secondary);
font-size: 15px;
}
.reg-card h2 {
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
/* ─── Step Badge ─── */
.step-badge {
display: inline-block;
font-size: 12px;
font-weight: 600;
color: var(--color-primary);
background: var(--color-primary-light);
padding: 4px 12px;
border-radius: 20px;
margin-bottom: 16px;
letter-spacing: 0.02em;
text-transform: uppercase;
}
/* ─── User Info Bar ─── */
.reg-user-info {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
margin-bottom: 24px;
}
.reg-user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--color-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 16px;
flex-shrink: 0;
}
.reg-user-name { font-weight: 600; font-size: 14px; }
.reg-user-email { font-size: 13px; color: var(--color-text-muted); }
/* ─── Forms ─── */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: var(--color-text);
}
.required { color: var(--color-error); }
.form-input {
width: 100%;
padding: 10px 14px;
font-size: 15px;
font-family: inherit;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
color: var(--color-text);
transition: border-color 0.15s, box-shadow 0.15s;
outline: none;
appearance: none;
-webkit-appearance: none;
}
.form-input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-input::placeholder { color: var(--color-text-muted); }
select.form-input {
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
}
.form-hint {
display: block;
font-size: 12px;
color: var(--color-text-muted);
margin-top: 6px;
}
/* ─── Buttons ─── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: inherit;
font-size: 15px;
font-weight: 600;
padding: 10px 20px;
border-radius: var(--radius-sm);
border: 1px solid transparent;
cursor: pointer;
transition: all 0.15s ease;
text-decoration: none;
line-height: 1.4;
}
.btn-primary {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.btn-primary:hover { background: var(--color-primary-hover); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-outline {
background: var(--color-surface);
color: var(--color-text);
border-color: var(--color-border);
}
.btn-outline:hover { background: var(--color-bg); border-color: var(--color-text-muted); }
.btn-lg { padding: 14px 24px; font-size: 16px; }
.btn-full { width: 100%; }
/* ─── Error Box ─── */
.reg-error {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--color-error-light);
color: var(--color-error);
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 500;
margin-bottom: 20px;
}
/* ─── Success / Error Icons ─── */
.success-icon {
color: var(--color-success);
margin-bottom: 16px;
}
.success-icon svg { width: 64px; height: 64px; }
.error-icon {
color: var(--color-error);
margin-bottom: 16px;
}
.error-icon svg { width: 64px; height: 64px; }
/* ─── Spinner ─── */
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ─── Progress Bar (success state) ─── */
.progress-bar {
width: 100%;
height: 4px;
background: var(--color-border);
border-radius: 4px;
overflow: hidden;
margin: 20px 0;
}
.progress-fill {
height: 100%;
background: var(--color-success);
border-radius: 4px;
animation: fill-progress 3s linear forwards;
}
@keyframes fill-progress { from { width: 0; } to { width: 100%; } }
/* ─── Text Utilities ─── */
.reg-text-muted { color: var(--color-text-secondary); font-size: 15px; }
.reg-text-muted.small { font-size: 13px; margin-top: 16px; }
.center-text { text-align: center; }
/* ─── Responsive ─── */
@media (max-width: 540px) {
.reg-card { padding: 28px 24px; }
.reg-card-header h1 { font-size: 20px; }
.reg-header { padding: 16px 20px; }
}