Compare commits

..

18 Commits

Author SHA1 Message Date
Grae Jones
a6e96bd61a home urls
All checks were successful
Client Home / build-deploy (push) Successful in 7s
2026-03-25 10:28:12 -07:00
Grae Jones
7925b45d76 Rewg Fix 2
All checks were successful
Client Tech / build-deploy (push) Successful in 11s
2026-03-23 14:10:25 -07:00
Grae Jones
d8dda59f4c reg fix 1
All checks were successful
Client Registration / build-deploy (push) Successful in 10s
2026-03-23 14:01:15 -07:00
Grae Jones
541251f065 agin... 2026-03-23 13:59:56 -07:00
Grae Jones
981117629a Fix looping
All checks were successful
Client Tech / build-deploy (push) Successful in 11s
2026-03-23 13:55:16 -07:00
Grae Jones
ba3af87c70 Tech CLient ID fix
All checks were successful
Client Tech / build-deploy (push) Successful in 11s
2026-03-23 13:23:47 -07:00
Grae Jones
d2c2b328b2 Align Client ID's
All checks were successful
Client Admin / build-deploy (push) Successful in 14s
Client Registration / build-deploy (push) Successful in 10s
2026-03-23 13:09:03 -07:00
Grae Jones
7e270c2a9e ...apply 4
All checks were successful
Client Registration / build-deploy (push) Successful in 10s
2026-03-23 10:46:14 -07:00
Grae Jones
23fc92bfb6 ...apply 3
All checks were successful
Client Registration / build-deploy (push) Successful in 10s
2026-03-23 10:25:12 -07:00
Grae Jones
8929eda2fa ...reapply
All checks were successful
Client Registration / build-deploy (push) Successful in 10s
Client Tech / build-deploy (push) Successful in 11s
2026-03-23 10:03:49 -07:00
Grae Jones
b9950e3316 ...reapply
All checks were successful
Client Tech / build-deploy (push) Successful in 11s
2026-03-23 09:48:44 -07:00
Grae Jones
732f81333b Spit Sign In - Sign Up
All checks were successful
Client Registration / build-deploy (push) Successful in 10s
2026-03-23 09:39:44 -07:00
Grae Jones
3a310c5d3f Azure Settings
All checks were successful
Client Registration / build-deploy (push) Successful in 9s
2026-03-23 08:48:19 -07:00
Grae Jones
8b0e5ea2f5 clean up
All checks were successful
Client Registration / build-deploy (push) Successful in 10s
2026-03-22 21:06:59 -07:00
Grae Jones
8c37f3d624 correct utr for MSAL
All checks were successful
Client Registration / build-deploy (push) Successful in 9s
2026-03-22 20:44:35 -07:00
Grae Jones
245c85a08b updated configauth
All checks were successful
Client Registration / build-deploy (push) Successful in 9s
2026-03-22 10:05:57 -07:00
Grae Jones
6006834265 Revised authconfig
All checks were successful
Client Registration / build-deploy (push) Successful in 14s
2026-03-22 09:54:12 -07:00
Grae Jones
ef378b7cbf Revised Registration 2026-03-22 09:41:07 -07:00
14 changed files with 238 additions and 114 deletions

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@ const TENANT_ID = 'f56a3c51-9b5c-4356-920f-b4dcf932a96b'; // positivespend tenan
// AdPlatform Admin SPA app registration — registered in positivespend tenant // AdPlatform Admin SPA app registration — registered in positivespend tenant
// Portal → App Registrations → AdPlatform Admin SPA → Application (client) ID // Portal → App Registrations → AdPlatform Admin SPA → Application (client) ID
const CLIENT_ID = '6873dee3-aff8-405d-9bc1-120c20794f98'; // TODO: replace after creating Admin SPA registration const CLIENT_ID = '6873dee3-aff8-405d-9bc1-120c20794f98';
const AUTHORITY = `https://login.microsoftonline.com/${TENANT_ID}`; const AUTHORITY = `https://login.microsoftonline.com/${TENANT_ID}`;

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
// - APP_URL: the URL that should open the *app* (direct entry / dashboard). // - APP_URL: the URL that should open the *app* (direct entry / dashboard).
// - REGISTRATION_URL: the URL to your external registration experience. // - REGISTRATION_URL: the URL to your external registration experience.
// //
// Tip: keep these as full absolute URLs. // Tip: keep these as full absolute
export const APP_URL = 'https://adpclient.usimdev.com/'; export const APP_URL = 'https://client.positivespend.com/';
export const REGISTRATION_URL = 'https://adpregist.usimdev.com/'; export const REGISTRATION_URL = 'https://register.positivespend.com/';

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -13,7 +13,7 @@ import { msalConfig, loginRequest } from './authConfig';
* isSignedIn — true when a user is authenticated * isSignedIn — true when a user is authenticated
* user — { entraSubjectId, email, displayName, firstName, surname, provider } * user — { entraSubjectId, email, displayName, firstName, surname, provider }
* error — string | null * error — string | null
* signIn(hint) — initiates login popup for 'google' | 'apple' | 'microsoft' * signIn(hint, isNew) — initiates login popup; hint = 'google'|'apple'|'microsoft', isNew=true forces sign-up screen
* signOut() — clears session * signOut() — clears session
* getAccessToken()— returns a fresh ID token for authenticating API calls * getAccessToken()— returns a fresh ID token for authenticating API calls
* clearError() — clears the error state * clearError() — clears the error state
@@ -41,12 +41,17 @@ export function useAuth() {
// ── Map ID token claims → user object ───────────────────────────────────── // ── Map ID token claims → user object ─────────────────────────────────────
function claimsToUser(claims, provider) { function claimsToUser(claims, provider) {
const firstName = claims.given_name ?? null;
const surname = claims.family_name ?? null;
const displayName = claims.name
?? [firstName, surname].filter(Boolean).join(' ')
?? null;
return { return {
entraSubjectId: claims.oid ?? claims.sub ?? null, entraSubjectId: claims.oid ?? claims.sub ?? null,
email: claims.email ?? claims.preferred_username ?? null, email: claims.email ?? claims.preferred_username ?? null,
displayName: claims.name ?? null, displayName,
firstName: claims.given_name ?? null, firstName,
surname: claims.family_name ?? null, surname,
provider: provider ?? 'unknown', provider: provider ?? 'unknown',
}; };
} }
@@ -109,13 +114,16 @@ export function AuthProvider({ children }) {
}, []); }, []);
// ── Sign in ──────────────────────────────────────────────────────── // ── Sign in ────────────────────────────────────────────────────────
const signIn = useCallback(async (providerHint) => { // isNewUser=true → prompt:'create' forces CIAM sign-up screen (Apply path)
// isNewUser=false → standard sign-in screen (Returning path)
const signIn = useCallback(async (providerHint, isNewUser = false) => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
const hint = PROVIDER_HINTS[providerHint]; const hint = PROVIDER_HINTS[providerHint];
const request = { const request = {
...loginRequest, ...loginRequest,
...(isNewUser && { prompt: 'create' }),
...(hint && { extraQueryParameters: { identity_provider_hint: hint } }), ...(hint && { extraQueryParameters: { identity_provider_hint: hint } }),
}; };

View File

@@ -1,18 +1,19 @@
/** /**
* MSAL Configuration for Entra External ID (Customer Identity) * MSAL Configuration for Entra External ID (Customer Identity / CIAM)
* *
* This uses the External ID tenant (CIAM) — different from the * Tenant: Positive Spend Clients
* internal Entra tenant used by the Admin/Management console. * Domain: positiveclients.onmicrosoft.com
* * Tenant ID: cbf8b7d7-1e13-486d-b5b0-287ba79fdf0b
* TODO: Replace placeholder values with actual External ID tenant details. * SPA App: AdPlatform Client SPA
* Client ID: 43c493e4-e1ed-4cd7-ab0a-e507e20af724
*/ */
export const msalConfig = { export const msalConfig = {
auth: { auth: {
clientId: '154c9111-14a0-4c0f-8132-7bc68254a74e', clientId: '43c493e4-e1ed-4cd7-ab0a-e507e20af724',
authority: 'https://usimclients.ciamlogin.com/891f98f1-ed34-42a1-9b6c-28b0554d92c2', authority: 'https://positiveclients.ciamlogin.com/positiveclients.onmicrosoft.com',
redirectUri: window.location.origin, redirectUri: 'https://register.positivespend.com',
postLogoutRedirectUri: window.location.origin, postLogoutRedirectUri: 'https://register.positivespend.com',
knownAuthorities: ['usimclients.ciamlogin.com'], knownAuthorities: ['positiveclients.ciamlogin.com'],
}, },
cache: { cache: {
cacheLocation: 'sessionStorage', cacheLocation: 'sessionStorage',
@@ -24,9 +25,5 @@ export const loginRequest = {
scopes: ['openid', 'profile', 'email'], scopes: ['openid', 'profile', 'email'],
}; };
// Registration Function API export const API_BASE_URL = 'https://portal.positivespend.com';
export const API_BASE_URL = 'https://adpregapi.usimdev.com';
// Function key for Registration API (AuthorizationLevel.Function)
// TODO: Set this from your Azure Function → App Keys → default host key
export const API_FUNCTION_KEY = ''; export const API_FUNCTION_KEY = '';

View File

@@ -6,11 +6,12 @@ export default function ContactStep() {
const { user } = useAuth(); const { user } = useAuth();
const { contactData, setContactData, saveContact, goBack, loading, error } = useRegistration(); const { contactData, setContactData, saveContact, goBack, loading, error } = useRegistration();
// Pre-fill from auth claims on mount // Pre-fill from CIAM claims on mount
useEffect(() => { useEffect(() => {
if (user) { if (user) {
setContactData(prev => ({ setContactData(prev => ({
contactName: prev.contactName || user.displayName || '', firstName: prev.firstName || user.firstName || '',
lastName: prev.lastName || user.surname || '',
contactEmail: prev.contactEmail || user.email || '', contactEmail: prev.contactEmail || user.email || '',
contactPhone: prev.contactPhone || '', contactPhone: prev.contactPhone || '',
})); }));
@@ -39,7 +40,9 @@ export default function ContactStep() {
{user && ( {user && (
<div className="info-card"> <div className="info-card">
<div className="info-card-primary">{user.displayName}</div> <div className="info-card-primary">
{[user.firstName, user.surname].filter(Boolean).join(' ') || user.displayName || user.email}
</div>
<div className="info-card-secondary">{user.email}</div> <div className="info-card-secondary">{user.email}</div>
</div> </div>
)} )}
@@ -47,26 +50,41 @@ export default function ContactStep() {
{error && <div className="error-message">{error}</div>} {error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form-row">
<div className="form-group"> <div className="form-group">
<label htmlFor="contactName">Full Name <span className="required">*</span></label> <label htmlFor="firstName">First Name <span className="required">*</span></label>
<input <input
type="text" type="text"
id="contactName" id="firstName"
name="contactName" name="firstName"
value={contactData.contactName} value={contactData.firstName || ''}
onChange={handleChange} onChange={handleChange}
placeholder="Jane Smith" placeholder="Jane"
required required
/> />
</div> </div>
<div className="form-group">
<label htmlFor="lastName">Last Name <span className="required">*</span></label>
<input
type="text"
id="lastName"
name="lastName"
value={contactData.lastName || ''}
onChange={handleChange}
placeholder="Smith"
required
/>
</div>
</div>
<div className="form-group"> <div className="form-group">
<label htmlFor="contactEmail">Email <span className="required">*</span></label> <label htmlFor="contactEmail">Email <span className="required">*</span></label>
<input <input
type="email" type="email"
id="contactEmail" id="contactEmail"
name="contactEmail" name="contactEmail"
value={contactData.contactEmail} value={contactData.contactEmail || ''}
onChange={handleChange} onChange={handleChange}
placeholder="jane@example.com" placeholder="jane@example.com"
required required
@@ -79,7 +97,7 @@ export default function ContactStep() {
type="tel" type="tel"
id="contactPhone" id="contactPhone"
name="contactPhone" name="contactPhone"
value={contactData.contactPhone} value={contactData.contactPhone || ''}
onChange={handleChange} onChange={handleChange}
placeholder="(555) 123-4567" placeholder="(555) 123-4567"
/> />

View File

@@ -1,25 +1,15 @@
import React from 'react'; import React, { useState } from 'react';
import { useAuth } from '../../auth/AuthProvider'; import { useAuth } from '../../auth/AuthProvider';
export default function SignInStep() { export default function SignInStep() {
const { signIn, isLoading, error } = useAuth(); const { signIn, isLoading, error } = useAuth();
const [mode, setMode] = useState(null); // null | 'new' | 'returning'
return ( const providers = (isNew) => (
<div className="step-card">
<div className="step-header">
<span className="step-icon">🔐</span>
<h2>Create Your Account</h2>
<p className="step-description">
Sign in with your preferred provider to get started with AdPlatform.
</p>
</div>
{error && <div className="error-message">{error}</div>}
<div className="provider-list"> <div className="provider-list">
<button <button
className="btn-provider btn-provider-google" className="btn-provider btn-provider-google"
onClick={() => signIn('google')} onClick={() => signIn('google', isNew)}
disabled={isLoading} disabled={isLoading}
> >
<span className="provider-icon">G</span> <span className="provider-icon">G</span>
@@ -28,7 +18,7 @@ export default function SignInStep() {
<button <button
className="btn-provider btn-provider-apple" className="btn-provider btn-provider-apple"
onClick={() => signIn('apple')} onClick={() => signIn('apple', isNew)}
disabled={isLoading} disabled={isLoading}
> >
<span className="provider-icon">🍎</span> <span className="provider-icon">🍎</span>
@@ -37,13 +27,75 @@ export default function SignInStep() {
<button <button
className="btn-provider btn-provider-microsoft" className="btn-provider btn-provider-microsoft"
onClick={() => signIn('microsoft')} onClick={() => signIn('microsoft', isNew)}
disabled={isLoading} disabled={isLoading}
> >
<span className="provider-icon"></span> <span className="provider-icon"></span>
{isLoading ? 'Connecting...' : 'Continue with Microsoft'} {isLoading ? 'Connecting...' : 'Continue with Microsoft'}
</button> </button>
</div> </div>
);
// Landing — user hasn't chosen yet
if (!mode) {
return (
<div className="step-card">
<div className="step-header">
<span className="step-icon">🔷</span>
<h2>Welcome to AdPlatform</h2>
<p className="step-description">
What would you like to do?
</p>
</div>
{error && <div className="error-message">{error}</div>}
<div className="path-choice">
<button
className="btn-path btn-path-primary"
onClick={() => setMode('new')}
>
<span className="path-icon"></span>
<div className="path-text">
<strong>Apply for Access</strong>
<span>New to AdPlatform? Start your registration here.</span>
</div>
<span className="path-arrow"></span>
</button>
<button
className="btn-path btn-path-secondary"
onClick={() => setMode('returning')}
>
<span className="path-icon">🔄</span>
<div className="path-text">
<strong>Check My Application</strong>
<span>Already applied? Sign in to view your status.</span>
</div>
<span className="path-arrow"></span>
</button>
</div>
</div>
);
}
// New applicant — sign up path
if (mode === 'new') {
return (
<div className="step-card">
<button className="btn-back" onClick={() => setMode(null)}> Back</button>
<div className="step-header">
<span className="step-icon"></span>
<h2>Create Your Account</h2>
<p className="step-description">
Choose how you'd like to sign up. We'll create your AdPlatform identity
and walk you through the application.
</p>
</div>
{error && <div className="error-message">{error}</div>}
{providers(true)}
<p className="step-fine-print"> <p className="step-fine-print">
By continuing, you agree to AdPlatform's Terms of Service and Privacy Policy. By continuing, you agree to AdPlatform's Terms of Service and Privacy Policy.
@@ -52,3 +104,28 @@ export default function SignInStep() {
</div> </div>
); );
} }
// Returning applicant — sign in path
return (
<div className="step-card">
<button className="btn-back" onClick={() => setMode(null)}>← Back</button>
<div className="step-header">
<span className="step-icon">🔄</span>
<h2>Welcome Back</h2>
<p className="step-description">
Sign in with the same account you used when you applied.
We'll pick up right where you left off.
</p>
</div>
{error && <div className="error-message">{error}</div>}
{providers(false)}
<p className="step-fine-print">
Use the same provider you signed up with. If you need help, contact
support@positivespend.com.
</p>
</div>
);
}

View File

@@ -53,7 +53,8 @@ export function RegistrationProvider({ children }) {
// Form data — maps to RegisterRequest model fields (minus entraSubjectId) // Form data — maps to RegisterRequest model fields (minus entraSubjectId)
const [contactData, setContactData] = useState({ const [contactData, setContactData] = useState({
contactName: '', firstName: '',
lastName: '',
contactEmail: '', contactEmail: '',
contactPhone: '', contactPhone: '',
}); });
@@ -83,8 +84,9 @@ export function RegistrationProvider({ children }) {
// ─── Validation ───────────────────────────────────────────────────── // ─── Validation ─────────────────────────────────────────────────────
const validateContact = useCallback(() => { const validateContact = useCallback(() => {
if (!contactData.contactName.trim()) return 'Contact name is required'; if (!contactData.firstName?.trim()) return 'First name is required';
if (!contactData.contactEmail.trim()) return 'Email is required'; if (!contactData.lastName?.trim()) return 'Last name is required';
if (!contactData.contactEmail?.trim()) return 'Email is required';
return null; return null;
}, [contactData]); }, [contactData]);
@@ -132,6 +134,8 @@ export function RegistrationProvider({ children }) {
const request = { const request = {
...contactData, ...contactData,
...businessData, ...businessData,
// Combine for server-side contactName field
contactName: [contactData.firstName, contactData.lastName].filter(Boolean).join(' '),
}; };
// Pre-fill email from auth if the user left it blank // Pre-fill email from auth if the user left it blank

View File

@@ -288,6 +288,33 @@ body {
.btn-provider-microsoft { background: #0078d4; color: #fff; border-color: #0078d4; } .btn-provider-microsoft { background: #0078d4; color: #fff; border-color: #0078d4; }
.btn-provider-microsoft:hover { background: #006abc; border-color: #006abc; } .btn-provider-microsoft:hover { background: #006abc; border-color: #006abc; }
/* Two-path landing */
.path-choice { display: flex; flex-direction: column; gap: 12px; margin-top: 8px; }
.btn-path {
display: flex; align-items: center; gap: 16px;
padding: 18px 20px; border-radius: 12px; border: 2px solid var(--border);
background: #fff; cursor: pointer; text-align: left;
transition: border-color 0.15s, box-shadow 0.15s;
width: 100%;
}
.btn-path:hover { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
.btn-path-primary:hover { border-color: #0078d4; box-shadow: 0 0 0 3px rgba(0,120,212,0.1); }
.path-icon { font-size: 24px; flex-shrink: 0; }
.path-text { flex: 1; display: flex; flex-direction: column; gap: 2px; }
.path-text strong { font-size: 15px; color: var(--text); font-weight: 600; }
.path-text span { font-size: 13px; color: var(--text-muted); }
.path-arrow { font-size: 18px; color: var(--text-muted); flex-shrink: 0; }
.btn-back {
display: inline-flex; align-items: center; gap: 4px;
background: none; border: none; cursor: pointer;
color: var(--text-muted); font-size: 14px; padding: 0 0 16px 0;
transition: color 0.15s;
}
.btn-back:hover { color: var(--text); }
.provider-icon { .provider-icon {
font-size: 18px; font-size: 18px;
width: 24px; width: 24px;
@@ -299,6 +326,16 @@ body {
Forms Forms
============================================================ */ ============================================================ */
.form-row {
display: flex;
gap: 12px;
}
.form-row .form-group {
flex: 1;
margin-bottom: 0;
}
.form-group { .form-group {
margin-bottom: 16px; margin-bottom: 16px;
} }

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +1,5 @@
/** /**
* authConfig.js - Tech Client (Staff Plane) * authConfig.js Tech Client (Staff Plane)
*
* APP REGISTRATION MAP (positivespend tenant: f56a3c51-9b5c-4356-920f-b4dcf932a96b)
* -------------------------------------------------------------------------
* Tech SPA (this app) 217928a9-4591-4dff-9f09-5b233824cf4f
* - Platform: SPA
* - Redirect URI: <Tech deployment origin> - must be registered in portal,
* matches window.location.origin at runtime.
* - API permissions: api://af95fa13-.../access_as_user (delegated)
*
* Management Staff API af95fa13-2ef4-4911-b137-7acc6a784cfa
* - Exposes scope: access_as_user
* - App roles: Staff.Admin, Staff.Tech
* - Management validates JWTs issued for this audience
*
* FLOW: MSAL authenticates as 217928a9, acquires a token scoped to
* api://af95fa13-.../access_as_user, sends as Bearer to Management API.
* Management validates: issuer = login.microsoftonline.com/f56a3c51/v2.0,
* audience = af95fa13 or api://af95fa13, roles = Staff.Admin | Staff.Tech.
*/ */
// ── Staff Identity Config ───────────────────────────────────────────────────── // ── Staff Identity Config ─────────────────────────────────────────────────────
@@ -25,9 +7,11 @@
const STAFF_TENANT_ID = 'f56a3c51-9b5c-4356-920f-b4dcf932a96b'; const STAFF_TENANT_ID = 'f56a3c51-9b5c-4356-920f-b4dcf932a96b';
const STAFF_CLIENT_ID = '217928a9-4591-4dff-9f09-5b233824cf4f'; const STAFF_CLIENT_ID = '217928a9-4591-4dff-9f09-5b233824cf4f';
// PROD: swap to → 'https://login.microsoftonline.com/' + STAFF_TENANT_ID
const STAFF_AUTHORITY = 'https://login.microsoftonline.com/' + STAFF_TENANT_ID; const STAFF_AUTHORITY = 'https://login.microsoftonline.com/' + STAFF_TENANT_ID;
// Management Staff API — resource the Tech SPA requests a token for
const MGMT_APP_ID = 'af95fa13-2ef4-4911-b137-7acc6a784cfa';
// ── MSAL Config ─────────────────────────────────────────────────────────────── // ── MSAL Config ───────────────────────────────────────────────────────────────
export const msalConfig = { export const msalConfig = {
@@ -36,7 +20,7 @@ export const msalConfig = {
authority: STAFF_AUTHORITY, authority: STAFF_AUTHORITY,
redirectUri: window.location.origin, redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin, postLogoutRedirectUri: window.location.origin,
navigateToLoginRequestUrl: true, navigateToLoginRequestUrl: false, // ← was true, caused the loop
}, },
cache: { cache: {
cacheLocation: 'sessionStorage', cacheLocation: 'sessionStorage',
@@ -53,19 +37,18 @@ export const msalConfig = {
case 3: console.debug(message); break; case 3: console.debug(message); break;
} }
}, },
logLevel: 3, logLevel: 1, // warn + error only in prod
}, },
}, },
}; };
export const loginRequest = { export const loginRequest = {
scopes: ["api://af95fa13-2ef4-4911-b137-7acc6a784cfa/access_as_user"] scopes: [`api://${MGMT_APP_ID}/access_as_user`] // ← fixed
}; };
// ── API Endpoints ───────────────────────────────────────────────────────────── // ── API Endpoints ─────────────────────────────────────────────────────────────
export const API_BASE = 'https://portal.positivespend.com'; // Gateway API export const API_BASE = 'https://portal.positivespend.com'; // ← fixed
export const MGMT_BASE = 'https://mgmt.positivespend.com'; // Management API export const MGMT_BASE = 'https://mgmt.positivespend.com'; // ← fixed
// Legacy — kept for backward compatibility with apiClient.js
export const SESSION_ENDPOINT = `${API_BASE}/api/auth/session`; export const SESSION_ENDPOINT = `${API_BASE}/api/auth/session`;