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,21 +1,22 @@
/** /**
* 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',
storeAuthStateInCookie: false, storeAuthStateInCookie: false,
}, },
}; };
@@ -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,12 +6,13 @@ 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 || '',
contactEmail: prev.contactEmail || user.email || '', lastName: prev.lastName || user.surname || '',
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,17 +50,32 @@ 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-group"> <div className="form-row">
<label htmlFor="contactName">Full Name <span className="required">*</span></label> <div className="form-group">
<input <label htmlFor="firstName">First Name <span className="required">*</span></label>
type="text" <input
id="contactName" type="text"
name="contactName" id="firstName"
value={contactData.contactName} name="firstName"
onChange={handleChange} value={contactData.firstName || ''}
placeholder="Jane Smith" onChange={handleChange}
required placeholder="Jane"
/> required
/>
</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>
<div className="form-group"> <div className="form-group">
@@ -66,7 +84,7 @@ export default function ContactStep() {
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,53 +1,130 @@
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'
const providers = (isNew) => (
<div className="provider-list">
<button
className="btn-provider btn-provider-google"
onClick={() => signIn('google', isNew)}
disabled={isLoading}
>
<span className="provider-icon">G</span>
{isLoading ? 'Connecting...' : 'Continue with Google'}
</button>
<button
className="btn-provider btn-provider-apple"
onClick={() => signIn('apple', isNew)}
disabled={isLoading}
>
<span className="provider-icon">🍎</span>
{isLoading ? 'Connecting...' : 'Continue with Apple'}
</button>
<button
className="btn-provider btn-provider-microsoft"
onClick={() => signIn('microsoft', isNew)}
disabled={isLoading}
>
<span className="provider-icon"></span>
{isLoading ? 'Connecting...' : 'Continue with Microsoft'}
</button>
</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">
By continuing, you agree to AdPlatform's Terms of Service and Privacy Policy.
Your account will be managed through Microsoft Entra External ID.
</p>
</div>
);
}
// Returning applicant — sign in path
return ( return (
<div className="step-card"> <div className="step-card">
<button className="btn-back" onClick={() => setMode(null)}>← Back</button>
<div className="step-header"> <div className="step-header">
<span className="step-icon">🔐</span> <span className="step-icon">🔄</span>
<h2>Create Your Account</h2> <h2>Welcome Back</h2>
<p className="step-description"> <p className="step-description">
Sign in with your preferred provider to get started with AdPlatform. Sign in with the same account you used when you applied.
We'll pick up right where you left off.
</p> </p>
</div> </div>
{error && <div className="error-message">{error}</div>} {error && <div className="error-message">{error}</div>}
<div className="provider-list"> {providers(false)}
<button
className="btn-provider btn-provider-google"
onClick={() => signIn('google')}
disabled={isLoading}
>
<span className="provider-icon">G</span>
{isLoading ? 'Connecting...' : 'Continue with Google'}
</button>
<button
className="btn-provider btn-provider-apple"
onClick={() => signIn('apple')}
disabled={isLoading}
>
<span className="provider-icon">🍎</span>
{isLoading ? 'Connecting...' : 'Continue with Apple'}
</button>
<button
className="btn-provider btn-provider-microsoft"
onClick={() => signIn('microsoft')}
disabled={isLoading}
>
<span className="provider-icon"></span>
{isLoading ? 'Connecting...' : 'Continue with Microsoft'}
</button>
</div>
<p className="step-fine-print"> <p className="step-fine-print">
By continuing, you agree to AdPlatform's Terms of Service and Privacy Policy. Use the same provider you signed up with. If you need help, contact
Your account will be managed through Microsoft Entra External ID. support@positivespend.com.
</p> </p>
</div> </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,10 +20,10 @@ 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',
storeAuthStateInCookie: false, storeAuthStateInCookie: false,
}, },
system: { system: {
@@ -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`;