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:
@@ -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
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
Reference in New Issue
Block a user