Driver onboarding now photographs the licence, ID card and vehicle
registration and reads the credential fields off them, plus a camera-only
profile selfie riders check the arriving driver against. Adds in-app chat
and WebRTC calls, push-backed ride offers, ratings, cancellation and
payment sheets, settlement, and the owner dashboard endpoints behind them.
Camera permission on Android:
- Declare CAMERA and READ_MEDIA_IMAGES in the manifest. expo-image-picker's
own plugin never declares CAMERA, and Android denies a request for an
undeclared permission instantly and silently — no dialog is ever shown,
which is indistinguishable from the app not asking at all.
- Handle canAskAgain: once Android stops showing the dialog, repeating why
we need it is a dead end, so offer Open Settings instead (lib/capture-
permission.ts), matching what the location flow already did.
Session: a 401 on a request that carried a token now ends the session
instead of being reinterpreted per-screen — driver-home had been reading it
as "this user has no driver profile" and showing an onboarding form to an
already-onboarded driver. Requests without a token are exempt so a failed
sign-in doesn't sign you out, and the notification is latched per token so
concurrent polls tear the session down once. (root) gains the auth guard
that turns that into the sign-in screen; app/index.tsx only guarded the way
in, leaving a session that ended mid-screen with nowhere to go.
Also ignore .uploads/ — it holds driver licence, ID and vehicle scans plus
profile photos, which are personal data and must not be committed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
3.4 KiB
TypeScript
111 lines
3.4 KiB
TypeScript
// Driver-side auth helper. Every driver-action endpoint first calls
|
|
// requireDriverProfile: it proves the request is from a signed-in user and
|
|
// that the user has completed onboarding (has a linked drivers row). A
|
|
// driver-role user who hasn't onboarded yet gets a 403 so the client can
|
|
// route them to the onboarding form rather than showing a bare 404.
|
|
|
|
import { requireAuth } from "@/lib/jwt";
|
|
import { sql } from "@/lib/db";
|
|
import type { ServiceId } from "@/constants/services";
|
|
|
|
type Auth = { userId: string; email: string };
|
|
|
|
/**
|
|
* Vetting state of a driver profile.
|
|
* pending — onboarded, waiting on an owner review. Cannot go online.
|
|
* approved — cleared to drive. The only state dispatch will match.
|
|
* rejected — review failed; the driver sees why and can resubmit.
|
|
* suspended — was approved, pulled by an owner.
|
|
*/
|
|
export const DRIVER_APPROVAL_STATUSES = [
|
|
"pending",
|
|
"approved",
|
|
"rejected",
|
|
"suspended",
|
|
] as const;
|
|
|
|
export type DriverApprovalStatus = (typeof DRIVER_APPROVAL_STATUSES)[number];
|
|
|
|
export const isApprovalStatus = (v: unknown): v is DriverApprovalStatus =>
|
|
typeof v === "string" &&
|
|
(DRIVER_APPROVAL_STATUSES as readonly string[]).includes(v);
|
|
|
|
export type DriverProfile = {
|
|
auth: Auth;
|
|
driverId: number;
|
|
service: ServiceId;
|
|
online: boolean;
|
|
approvalStatus: DriverApprovalStatus;
|
|
};
|
|
|
|
export type AuthError = { error: Response };
|
|
|
|
const VALID_SERVICES = ["car", "moto", "courier", "chauffeur"] as const;
|
|
export const isServiceId = (v: unknown): v is ServiceId =>
|
|
typeof v === "string" && (VALID_SERVICES as readonly string[]).includes(v);
|
|
|
|
// Returns the driver profile for the authenticated user, or a 401/403 the
|
|
// caller can return directly. A 403 with the onboarding code tells the client
|
|
// to show the onboarding form instead of treating it as a hard error.
|
|
export const requireDriverProfile = async (
|
|
req: Request,
|
|
): Promise<DriverProfile | AuthError> => {
|
|
const auth = requireAuth(req);
|
|
if ("error" in auth) return { error: auth.error };
|
|
|
|
const rows = await sql<{
|
|
id: number;
|
|
service: ServiceId;
|
|
online: boolean;
|
|
approval_status: DriverApprovalStatus;
|
|
}>`
|
|
SELECT id, service, online, approval_status
|
|
FROM drivers WHERE user_id = ${auth.userId}
|
|
`;
|
|
|
|
if (!rows[0]) {
|
|
return {
|
|
error: Response.json(
|
|
{ error: "No driver profile — complete onboarding.", code: "ONBOARD" },
|
|
{ status: 403 },
|
|
),
|
|
};
|
|
}
|
|
|
|
const { id, service, online, approval_status } = rows[0];
|
|
return {
|
|
auth,
|
|
driverId: id,
|
|
service,
|
|
online,
|
|
approvalStatus: approval_status,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Gate for anything a driver can only do once they've been cleared to drive:
|
|
* going online, taking an offer, moving a ride through its states. Returns a
|
|
* ready-to-return 403 carrying the current status, so the client can show the
|
|
* pending / rejected screen instead of a bare error.
|
|
*/
|
|
export const requireApprovedDriver = async (
|
|
req: Request,
|
|
): Promise<DriverProfile | AuthError> => {
|
|
const result = await requireDriverProfile(req);
|
|
if ("error" in result) return result;
|
|
|
|
if (result.approvalStatus !== "approved") {
|
|
return {
|
|
error: Response.json(
|
|
{
|
|
error: "Your driver account is not approved yet.",
|
|
code: "NOT_APPROVED",
|
|
approval_status: result.approvalStatus,
|
|
},
|
|
{ status: 403 },
|
|
),
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}; |