API Reference¶
Base path: /api
Auth style:
- Access token:
Authorization: Bearer <token> - Refresh token: HttpOnly cookie (
REFRESH_TOKEN_COOKIE_NAME, defaultfmr_refresh_token) scoped to/api/auth
System¶
GET /system/status¶
Returns:
has_users: booleanallow_self_register: boolean(currently alwaystrue)
Auth¶
POST /auth/bootstrap¶
Create first admin user (only when no users exist).
Request body:
username: string(3-64 chars; normalized lowercase; allowed chars: letters, digits,.,_,-)password: string(8-128 chars, must include uppercase/lowercase/digit)
Response:
access_tokentoken_typeuser- Sets refresh cookie
POST /auth/register¶
Self-register user (open by default; no moderation gate).
Request body:
username(same policy as bootstrap)password(same policy as bootstrap)
Response: token + user object. Also sets refresh cookie.
POST /auth/login¶
Request body:
username(same policy as bootstrap)passwordotp_code(required if MFA enabled)
Response: token + user object. Also sets refresh cookie.
Frontend client flow:
- Submit username/password first.
- If response detail indicates MFA code is required, prompt for OTP and retry the same endpoint with
otp_code.
POST /auth/refresh¶
Rotate refresh token cookie and issue a new access token.
Request:
- No JSON body
- Requires refresh cookie
Response:
access_tokentoken_typeuser- Sets rotated refresh cookie
Failure:
401if refresh token is missing/invalid/revoked/expired
POST /auth/logout¶
Revoke the current refresh token and clear refresh cookie.
Request:
- No JSON body
Response:
message
GET /auth/me¶
Returns authenticated user profile.
POST /auth/change-password¶
Change password for the authenticated user.
Request body:
current_passwordnew_password(8-128 chars, must include uppercase/lowercase/digit)
Response:
message
Behavior:
- Revokes all active refresh tokens for the current user.
POST /auth/mfa/setup¶
Creates TOTP secret and returns:
secretotpauth_url
Frontend client uses otpauth_url to generate a local QR code for authenticator app scan.
POST /auth/mfa/verify¶
Request body:
code
Enables MFA when code is valid.
POST /auth/mfa/disable¶
Request body:
code
Disables MFA (requires valid code if currently enabled).
Users (admin only)¶
GET /users¶
Returns list of users.
POST /users¶
Create user.
Request body:
username(same policy as bootstrap)password(same policy as bootstrap)is_admin: boolean
PATCH /users/{user_id}¶
Update an existing user (admin only).
Request body (at least one required):
password(8-128 chars, must include uppercase/lowercase/digit) to reset passwordis_admin: booleanto change role
Rules:
- Admin cannot edit self via this endpoint.
- API prevents removing the last admin role.
- If password is changed, all active refresh tokens for that user are revoked.
DELETE /users/{user_id}¶
Delete an existing user (admin only).
Rules:
- Admin cannot delete self via this endpoint.
- API prevents deleting the last admin account.
Parking¶
POST /parking/records¶
Create parking record with multipart form-data.
Form fields:
latitude(optional, must be sent withlongitude)longitude(optional, must be sent withlatitude)location_label(required when location pair is provided; must be a physical address, not coordinate text)parked_at(optional ISO datetime)note(optional)photos(optional repeated file field, max 3)
Validation rules:
- Provide both
latitudeandlongitude, or neither. - Record creation requires at least one of:
- location pair (
latitude+longitude) - non-empty
note - at least one
photosfile - When a location pair is sent,
location_labelmust be present and non-coordinate (physical address). - If location is omitted,
location_labelis cleared/ignored. - Text fields (
location_label,note) reject control characters. - Stored/returned
location_labelvalues are normalized as street-first address text when geocoding succeeds.
Photo upload constraints:
- max size per file =
MAX_PHOTO_SIZE_MB(default 8 MB) - allowed content types =
image/jpeg,image/jpg,image/pjpeg,image/png,image/webp,image/heic,image/heic-sequence,image/heif,image/heif-sequence,image/avif
Returns ParkingRecordOut.
GET /parking/records¶
Query params:
owner_id(admin-only UUID filter)limit(default 50, max 200)
Returns list of ParkingRecordOut ordered by newest parked time.
GET /parking/records/latest¶
Query params:
owner_id(admin-only UUID filter)
Returns latest ParkingRecordOut or null.
PATCH /parking/records/{record_id}¶
Request JSON (all optional):
latitudelongitudelocation_labelnoteparked_at
Validation rules:
- If updating location, provide both
latitudeandlongitudetogether. - To clear location, set both
latitudeandlongitudetonull. - Clearing location also clears
location_label. - When updating to non-null coordinates, include
location_labelwith a physical address (not coordinate text).
Returns updated record.
DELETE /parking/records/{record_id}¶
Deletes a record and linked photos.
POST /parking/records/{record_id}/photos¶
Add additional photos (multipart form-data field photos).
DELETE /parking/photos/{photo_id}¶
Delete a specific photo.
GET /parking/photos/{photo_id}/download¶
Download photo if caller has access.
Response objects¶
UserOut¶
idusernameis_adminmfa_enabledcreated_at
PhotoOut¶
idfile_namecontent_typefile_sizecreated_atdownload_url
ParkingRecordOut¶
idowner_idlatitude(number | null)longitude(number | null)location_label(string | null)noteparked_atcreated_atupdated_atphotos: PhotoOut[]