Merge pull request #655 from Oloodi/codex/feat-staff-auth-clockin-fixes

fix(auth): align demo staff phone identity and clock-in payload
This commit is contained in:
Achintha Isuru
2026-03-17 10:01:07 -04:00
committed by GitHub
5 changed files with 103 additions and 1 deletions

View File

@@ -630,8 +630,14 @@ export async function listTodayShifts(actorUid) {
SELECT SELECT
a.id AS "assignmentId", a.id AS "assignmentId",
s.id AS "shiftId", s.id AS "shiftId",
COALESCE(s.title, sr.role_name || ' shift') AS title,
b.business_name AS "clientName",
ROUND(COALESCE(sr.pay_rate_cents, 0)::numeric / 100, 2) AS "hourlyRate",
sr.role_name AS "roleName", sr.role_name AS "roleName",
COALESCE(cp.label, s.location_name) AS location, COALESCE(cp.label, s.location_name) AS location,
COALESCE(s.location_address, cp.address) AS "locationAddress",
COALESCE(s.latitude, cp.latitude) AS latitude,
COALESCE(s.longitude, cp.longitude) AS longitude,
s.starts_at AS "startTime", s.starts_at AS "startTime",
s.ends_at AS "endTime", s.ends_at AS "endTime",
COALESCE(s.clock_in_mode, cp.default_clock_in_mode, 'EITHER') AS "clockInMode", COALESCE(s.clock_in_mode, cp.default_clock_in_mode, 'EITHER') AS "clockInMode",
@@ -643,6 +649,7 @@ export async function listTodayShifts(actorUid) {
FROM assignments a FROM assignments a
JOIN shifts s ON s.id = a.shift_id JOIN shifts s ON s.id = a.shift_id
JOIN shift_roles sr ON sr.id = a.shift_role_id JOIN shift_roles sr ON sr.id = a.shift_role_id
JOIN businesses b ON b.id = s.business_id
LEFT JOIN clock_points cp ON cp.id = s.clock_point_id LEFT JOIN clock_points cp ON cp.id = s.clock_point_id
LEFT JOIN attendance_sessions ON attendance_sessions.assignment_id = a.id LEFT JOIN attendance_sessions ON attendance_sessions.assignment_id = a.id
WHERE a.tenant_id = $1 WHERE a.tenant_id = $1

View File

@@ -1,10 +1,24 @@
import { signInWithPassword, signUpWithPassword } from '../src/services/identity-toolkit.js'; import { signInWithPassword, signUpWithPassword } from '../src/services/identity-toolkit.js';
import { applicationDefault, getApps, initializeApp } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
const ownerEmail = process.env.V2_DEMO_OWNER_EMAIL || 'legendary.owner+v2@krowd.com'; const ownerEmail = process.env.V2_DEMO_OWNER_EMAIL || 'legendary.owner+v2@krowd.com';
const staffEmail = process.env.V2_DEMO_STAFF_EMAIL || 'ana.barista+v2@krowd.com'; const staffEmail = process.env.V2_DEMO_STAFF_EMAIL || 'ana.barista+v2@krowd.com';
const staffPhone = process.env.V2_DEMO_STAFF_PHONE || '+15557654321';
const ownerPassword = process.env.V2_DEMO_OWNER_PASSWORD || 'Demo2026!'; const ownerPassword = process.env.V2_DEMO_OWNER_PASSWORD || 'Demo2026!';
const staffPassword = process.env.V2_DEMO_STAFF_PASSWORD || 'Demo2026!'; const staffPassword = process.env.V2_DEMO_STAFF_PASSWORD || 'Demo2026!';
function ensureAdminApp() {
if (getApps().length === 0) {
initializeApp({ credential: applicationDefault() });
}
}
function getAdminAuth() {
ensureAdminApp();
return getAuth();
}
async function ensureUser({ email, password, displayName }) { async function ensureUser({ email, password, displayName }) {
try { try {
const signedIn = await signInWithPassword({ email, password }); const signedIn = await signInWithPassword({ email, password });
@@ -40,6 +54,44 @@ async function ensureUser({ email, password, displayName }) {
} }
} }
async function getUserByPhoneNumber(phoneNumber) {
try {
return await getAdminAuth().getUserByPhoneNumber(phoneNumber);
} catch (error) {
if (error?.code === 'auth/user-not-found') return null;
throw error;
}
}
async function reconcileStaffPhoneIdentity({ uid, email, displayName, phoneNumber }) {
const auth = getAdminAuth();
const current = await auth.getUser(uid);
const existingPhoneUser = await getUserByPhoneNumber(phoneNumber);
let deletedConflictingUid = null;
if (existingPhoneUser && existingPhoneUser.uid !== uid) {
deletedConflictingUid = existingPhoneUser.uid;
await auth.deleteUser(existingPhoneUser.uid);
}
const updatePayload = {};
if (current.displayName !== displayName) updatePayload.displayName = displayName;
if (current.email !== email) updatePayload.email = email;
if (current.phoneNumber !== phoneNumber) updatePayload.phoneNumber = phoneNumber;
if (Object.keys(updatePayload).length > 0) {
await auth.updateUser(uid, updatePayload);
}
const reconciled = await auth.getUser(uid);
return {
uid: reconciled.uid,
email: reconciled.email,
phoneNumber: reconciled.phoneNumber,
deletedConflictingUid,
};
}
async function main() { async function main() {
const owner = await ensureUser({ const owner = await ensureUser({
email: ownerEmail, email: ownerEmail,
@@ -53,8 +105,15 @@ async function main() {
displayName: 'Ana Barista V2', displayName: 'Ana Barista V2',
}); });
const reconciledStaff = await reconcileStaffPhoneIdentity({
uid: staff.uid,
email: staff.email,
displayName: staff.displayName,
phoneNumber: staffPhone,
});
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(JSON.stringify({ owner, staff }, null, 2)); console.log(JSON.stringify({ owner, staff: { ...staff, ...reconciledStaff } }, null, 2));
} }
main().catch((error) => { main().catch((error) => {

View File

@@ -631,6 +631,10 @@ async function main() {
assert.ok(Array.isArray(todaysShifts.items)); assert.ok(Array.isArray(todaysShifts.items));
const assignedTodayShift = todaysShifts.items.find((shift) => shift.shiftId === fixture.shifts.assigned.id); const assignedTodayShift = todaysShifts.items.find((shift) => shift.shiftId === fixture.shifts.assigned.id);
assert.ok(assignedTodayShift); assert.ok(assignedTodayShift);
assert.equal(assignedTodayShift.clientName, fixture.business.name);
assert.equal(typeof assignedTodayShift.hourlyRate, 'number');
assert.equal(typeof assignedTodayShift.latitude, 'number');
assert.equal(typeof assignedTodayShift.longitude, 'number');
assert.equal(assignedTodayShift.clockInMode, fixture.shifts.assigned.clockInMode); assert.equal(assignedTodayShift.clockInMode, fixture.shifts.assigned.clockInMode);
assert.equal(assignedTodayShift.allowClockInOverride, fixture.shifts.assigned.allowClockInOverride); assert.equal(assignedTodayShift.allowClockInOverride, fixture.shifts.assigned.allowClockInOverride);
logStep('staff.clock-in.shifts-today.ok', { count: todaysShifts.items.length }); logStep('staff.clock-in.shifts-today.ok', { count: todaysShifts.items.length });

View File

@@ -182,6 +182,13 @@ Possible response A:
This is the normal mobile path when frontend does **not** send recaptcha or integrity tokens. This is the normal mobile path when frontend does **not** send recaptcha or integrity tokens.
Current dev demo worker:
- phone number: `+15557654321`
- email: `ana.barista+v2@krowd.com`
Those two now resolve to the same Firebase user and the same seeded staff profile in v2.
Possible response B: Possible response B:
```json ```json

View File

@@ -114,6 +114,31 @@ Full auth behavior, including staff phone flow and refresh rules, is documented
- `GET /staff/faqs` - `GET /staff/faqs`
- `GET /staff/faqs/search` - `GET /staff/faqs/search`
Example `GET /staff/clock-in/shifts/today` item:
```json
{
"assignmentId": "uuid",
"shiftId": "uuid",
"title": "Assigned espresso shift",
"clientName": "Google Mountain View Cafes",
"hourlyRate": 23,
"roleName": "Barista",
"location": "Google MV Cafe Clock Point",
"locationAddress": "1600 Amphitheatre Pkwy, Mountain View, CA",
"latitude": 37.4221,
"longitude": -122.0841,
"startTime": "2026-03-17T13:48:23.482Z",
"endTime": "2026-03-17T21:48:23.482Z",
"clockInMode": "GEO_REQUIRED",
"allowClockInOverride": true,
"geofenceRadiusMeters": 120,
"nfcTagId": "NFC-DEMO-ANA-001",
"attendanceStatus": "NOT_CLOCKED_IN",
"clockInAt": null
}
```
### Staff writes ### Staff writes
- `POST /staff/profile/setup` - `POST /staff/profile/setup`