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
// 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}`;

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).
// - 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 REGISTRATION_URL = 'https://adpregist.usimdev.com/';
export const APP_URL = 'https://client.positivespend.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
* user — { entraSubjectId, email, displayName, firstName, surname, provider }
* 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
* getAccessToken()— returns a fresh ID token for authenticating API calls
* clearError() — clears the error state
@@ -41,12 +41,17 @@ export function useAuth() {
// ── Map ID token claims → user object ─────────────────────────────────────
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 {
entraSubjectId: claims.oid ?? claims.sub ?? null,
email: claims.email ?? claims.preferred_username ?? null,
displayName: claims.name ?? null,
firstName: claims.given_name ?? null,
surname: claims.family_name ?? null,
displayName,
firstName,
surname,
provider: provider ?? 'unknown',
};
}
@@ -109,13 +114,16 @@ export function AuthProvider({ children }) {
}, []);
// ── 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);
setError(null);
const hint = PROVIDER_HINTS[providerHint];
const request = {
...loginRequest,
...(isNewUser && { prompt: 'create' }),
...(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
* internal Entra tenant used by the Admin/Management console.
*
* TODO: Replace placeholder values with actual External ID tenant details.
* Tenant: Positive Spend Clients
* Domain: positiveclients.onmicrosoft.com
* Tenant ID: cbf8b7d7-1e13-486d-b5b0-287ba79fdf0b
* SPA App: AdPlatform Client SPA
* Client ID: 43c493e4-e1ed-4cd7-ab0a-e507e20af724
*/
export const msalConfig = {
auth: {
clientId: '154c9111-14a0-4c0f-8132-7bc68254a74e',
authority: 'https://usimclients.ciamlogin.com/891f98f1-ed34-42a1-9b6c-28b0554d92c2',
redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin,
knownAuthorities: ['usimclients.ciamlogin.com'],
clientId: '43c493e4-e1ed-4cd7-ab0a-e507e20af724',
authority: 'https://positiveclients.ciamlogin.com/positiveclients.onmicrosoft.com',
redirectUri: 'https://register.positivespend.com',
postLogoutRedirectUri: 'https://register.positivespend.com',
knownAuthorities: ['positiveclients.ciamlogin.com'],
},
cache: {
cacheLocation: 'sessionStorage',
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
};
@@ -24,9 +25,5 @@ export const loginRequest = {
scopes: ['openid', 'profile', 'email'],
};
// Registration Function API
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_BASE_URL = 'https://portal.positivespend.com';
export const API_FUNCTION_KEY = '';

View File

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

View File

@@ -1,53 +1,130 @@
import React from 'react';
import React, { useState } from 'react';
import { useAuth } from '../../auth/AuthProvider';
export default function SignInStep() {
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 (
<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>
<span className="step-icon">🔄</span>
<h2>Welcome Back</h2>
<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>
</div>
{error && <div className="error-message">{error}</div>}
<div className="provider-list">
<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>
{providers(false)}
<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.
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)
const [contactData, setContactData] = useState({
contactName: '',
firstName: '',
lastName: '',
contactEmail: '',
contactPhone: '',
});
@@ -83,8 +84,9 @@ export function RegistrationProvider({ children }) {
// ─── Validation ─────────────────────────────────────────────────────
const validateContact = useCallback(() => {
if (!contactData.contactName.trim()) return 'Contact name is required';
if (!contactData.contactEmail.trim()) return 'Email is required';
if (!contactData.firstName?.trim()) return 'First name is required';
if (!contactData.lastName?.trim()) return 'Last name is required';
if (!contactData.contactEmail?.trim()) return 'Email is required';
return null;
}, [contactData]);
@@ -132,6 +134,8 @@ export function RegistrationProvider({ children }) {
const request = {
...contactData,
...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

View File

@@ -288,6 +288,33 @@ body {
.btn-provider-microsoft { background: #0078d4; color: #fff; border-color: #0078d4; }
.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 {
font-size: 18px;
width: 24px;
@@ -299,6 +326,16 @@ body {
Forms
============================================================ */
.form-row {
display: flex;
gap: 12px;
}
.form-row .form-group {
flex: 1;
margin-bottom: 0;
}
.form-group {
margin-bottom: 16px;
}

File diff suppressed because one or more lines are too long

View File

@@ -1,33 +1,17 @@
/**
* 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.
* authConfig.js Tech Client (Staff Plane)
*/
// ── Staff Identity Config ─────────────────────────────────────────────────────
const STAFF_TENANT_ID = 'f56a3c51-9b5c-4356-920f-b4dcf932a96b';
const STAFF_CLIENT_ID = '217928a9-4591-4dff-9f09-5b233824cf4f';
const STAFF_TENANT_ID = 'f56a3c51-9b5c-4356-920f-b4dcf932a96b';
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;
// Management Staff API — resource the Tech SPA requests a token for
const MGMT_APP_ID = 'af95fa13-2ef4-4911-b137-7acc6a784cfa';
// ── MSAL Config ───────────────────────────────────────────────────────────────
export const msalConfig = {
@@ -36,10 +20,10 @@ export const msalConfig = {
authority: STAFF_AUTHORITY,
redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin,
navigateToLoginRequestUrl: true,
navigateToLoginRequestUrl: false, // ← was true, caused the loop
},
cache: {
cacheLocation: 'sessionStorage',
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
system: {
@@ -53,19 +37,18 @@ export const msalConfig = {
case 3: console.debug(message); break;
}
},
logLevel: 3,
logLevel: 1, // warn + error only in prod
},
},
};
export const loginRequest = {
scopes: ["api://af95fa13-2ef4-4911-b137-7acc6a784cfa/access_as_user"]
scopes: [`api://${MGMT_APP_ID}/access_as_user`] // ← fixed
};
// ── API Endpoints ─────────────────────────────────────────────────────────────
export const API_BASE = 'https://portal.positivespend.com'; // Gateway API
export const MGMT_BASE = 'https://mgmt.positivespend.com'; // Management API
export const API_BASE = 'https://portal.positivespend.com'; // ← fixed
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`;