Add passkeysRoute for managing user passkeys and extend OpenAPI documentation
- Introduce `passkeysRoute` with operations to list, create, rename, and delete passkeys for authenticated users. - Update OpenAPI with endpoints and request/response schemas for passkey management.
This commit is contained in:
+136
-91
@@ -1,93 +1,3 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Copenhagen Truck Wash API
|
||||
description: |
|
||||
This API provides access to the Copenhagen Truck Wash system, managing orders, bookings,
|
||||
departments, products, customers, and various integrations including e-conomic, Stripe,
|
||||
XLVask, and more.
|
||||
|
||||
## Authentication
|
||||
Most endpoints require authentication using a Bearer token obtained from the `/auth/login`
|
||||
or `/auth/employee/login` endpoints.
|
||||
|
||||
## Permissions
|
||||
Many endpoints require specific permissions that are assigned to user groups/roles.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Copenhagen Truck Wash
|
||||
email: support@truckwash.dk
|
||||
servers:
|
||||
- url: https://api.truckwash.dk
|
||||
description: Production server
|
||||
- url: http://localhost
|
||||
description: Local development server
|
||||
|
||||
security:
|
||||
- BearerAuth: []
|
||||
|
||||
tags:
|
||||
- name: Authentication
|
||||
description: User and employee authentication endpoints
|
||||
- name: Users
|
||||
description: User management and customer operations
|
||||
- name: Orders
|
||||
description: Order creation, management, and retrieval
|
||||
- name: Order Items
|
||||
description: Managing items within orders
|
||||
- name: Bookings
|
||||
description: Booking management for wash services
|
||||
- name: Departments
|
||||
description: Department and location management
|
||||
- name: Products
|
||||
description: Product catalog and pricing
|
||||
- name: Categories
|
||||
description: Product category management
|
||||
- name: Invoices
|
||||
description: Invoice generation and management
|
||||
- name: Payments
|
||||
description: Payment processing and collection
|
||||
- name: Vehicles
|
||||
description: Vehicle registration and management
|
||||
- name: Notifications
|
||||
description: System notifications and alerts
|
||||
- name: Statistics
|
||||
description: Business analytics and reporting
|
||||
- name: Modules
|
||||
description: Third-party integrations and modules
|
||||
- name: Attachments
|
||||
description: File upload and attachment management
|
||||
- name: Forms
|
||||
description: Form submissions and management
|
||||
- name: Worker
|
||||
description: System worker status and maintenance
|
||||
- name: Plate Scans
|
||||
description: License plate scanning operations
|
||||
- name: Config
|
||||
description: Module configuration management
|
||||
- name: Branding
|
||||
description: Branding options management
|
||||
- name: Roles
|
||||
description: Role and permission management
|
||||
- name: Self-Serve
|
||||
description: Self-serve lane operations and questions
|
||||
- name: Goals
|
||||
description: Department goals management
|
||||
- name: Subusers
|
||||
description: Subuser registration and setup
|
||||
|
||||
paths:
|
||||
# Subusers (public registration + setup)
|
||||
/subusers:
|
||||
post:
|
||||
tags:
|
||||
- Subusers
|
||||
summary: Create a subuser registration
|
||||
description: |
|
||||
Creates a subuser (driver) account using a company's CVR and a phone number. Validates the
|
||||
CVR via e-conomic, ensures the phone number is not already in use, and if SMS is enabled
|
||||
sends a setup link by SMS for the user to complete registration.
|
||||
operationId: createSubuser
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -5601,6 +5511,88 @@ paths:
|
||||
'200':
|
||||
description: Success
|
||||
|
||||
# Account Security - Passkeys
|
||||
/account/security/passkeys:
|
||||
get:
|
||||
tags:
|
||||
- Security
|
||||
summary: List passkeys for the authenticated user
|
||||
operationId: listPasskeys
|
||||
responses:
|
||||
'200':
|
||||
description: A list of passkeys
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Passkey'
|
||||
'400':
|
||||
description: Invalid session or request
|
||||
post:
|
||||
tags:
|
||||
- Security
|
||||
summary: Create/add a passkey for the authenticated user
|
||||
operationId: createPasskey
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PasskeyCreateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Passkey created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
'400':
|
||||
description: Invalid session or request
|
||||
|
||||
/account/security/passkeys/{id}:
|
||||
patch:
|
||||
tags:
|
||||
- Security
|
||||
summary: Rename a passkey
|
||||
operationId: renamePasskey
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PasskeyRenameRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Passkey renamed
|
||||
'404':
|
||||
description: Not found
|
||||
delete:
|
||||
tags:
|
||||
- Security
|
||||
summary: Delete a passkey
|
||||
operationId: deletePasskey
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Passkey deleted
|
||||
'404':
|
||||
description: Not found
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
@@ -6654,4 +6646,57 @@ components:
|
||||
type: array
|
||||
items: { type: integer }
|
||||
criteria:
|
||||
$ref: '#/components/schemas/GoalsCriteria'
|
||||
$ref: '#/components/schemas/GoalsCriteria'
|
||||
Passkey:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
credential_id:
|
||||
type: string
|
||||
description: Base64URL-encoded credential ID
|
||||
name:
|
||||
type: string
|
||||
nullable: true
|
||||
algorithm:
|
||||
type: string
|
||||
example: ES256
|
||||
transports:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ["usb", "nfc", "ble", "internal"]
|
||||
sign_count:
|
||||
type: integer
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
PasskeyCreateRequest:
|
||||
type: object
|
||||
required: [credential_id, public_key, algorithm, transports]
|
||||
properties:
|
||||
credential_id:
|
||||
type: string
|
||||
description: Base64URL-encoded credential ID returned from WebAuthn
|
||||
public_key:
|
||||
type: string
|
||||
description: Base64URL-encoded public key (COSE or PEM as stored)
|
||||
algorithm:
|
||||
type: string
|
||||
example: ES256
|
||||
transports:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
nullable: true
|
||||
PasskeyRenameRequest:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use objects\logs_o;
|
||||
use objects\passkeys_o;
|
||||
use traits\route_t;
|
||||
|
||||
class passkeysRoute
|
||||
{
|
||||
use route_t;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// List passkeys for current authenticated user
|
||||
$this->get('/account/security/passkeys', function () {
|
||||
global $response;
|
||||
self::requirePermission('user_security_passkeys_list');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
(new logs_o())->add('user_security', 'global', 0, 0, 'USER_SECURITY_PASSKEYS_LIST', 'User not logged in');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
$passkeys = new passkeys_o();
|
||||
// Restrict to current user (customer) and non-subuser records
|
||||
$passkeys->setAdditionalWhereClause('WHERE user_id = ' . (int)$user->id . ' AND is_subuser = 0 AND deleted_at IS NULL');
|
||||
$list = $passkeys->listObjects(function ($o) {
|
||||
return [
|
||||
'id' => $o->id,
|
||||
'credential_id' => $o->credential_id->value(),
|
||||
'name' => $o->name->value(),
|
||||
'algorithm' => $o->algorithm->value(),
|
||||
'transports' => $o->transports->value(),
|
||||
'sign_count' => $o->sign_count->value(),
|
||||
'created_at' => $o->created_at->value(),
|
||||
'updated_at' => $o->updated_at->value(),
|
||||
];
|
||||
});
|
||||
|
||||
(new logs_o())->add('user_security', 'global', 1, $user->id, 'USER_SECURITY_PASSKEYS_LIST', 'Listed passkeys');
|
||||
$response->success($list);
|
||||
}, [
|
||||
'user_security_passkeys_list' => 'List passkeys for the authenticated user',
|
||||
]);
|
||||
|
||||
// Create/add a passkey (store after client-side WebAuthn attestation)
|
||||
$this->post('/account/security/passkeys', function () {
|
||||
global $response;
|
||||
self::requirePermission('user_security_passkeys_create');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
(new logs_o())->add('user_security', 'global', 0, 0, 'USER_SECURITY_PASSKEYS_CREATE', 'User not logged in');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
self::requireParameters(['credential_id', 'public_key', 'algorithm', 'transports']);
|
||||
|
||||
$credential_id = (string)self::getParameter('credential_id');
|
||||
self::requireType($credential_id, self::type_string());
|
||||
self::requireMinLength('credential_id', 16);
|
||||
self::requireMaxLength('credential_id', 4096);
|
||||
|
||||
$public_key = (string)self::getParameter('public_key');
|
||||
self::requireType($public_key, self::type_string());
|
||||
self::requireMinLength('public_key', 32);
|
||||
self::requireMaxLength('public_key', 8192);
|
||||
|
||||
$algorithm = (string)self::getParameter('algorithm');
|
||||
self::requireType($algorithm, self::type_string());
|
||||
self::requireMinLength('algorithm', 3);
|
||||
self::requireMaxLength('algorithm', 32);
|
||||
|
||||
$transports = self::getParameter('transports');
|
||||
self::requireType($transports, self::TYPE_ARRAY());
|
||||
|
||||
$name = self::getParameter('name');
|
||||
if ($name !== null) {
|
||||
$name = (string)$name;
|
||||
self::requireType($name, self::type_string());
|
||||
self::requireMinLength('name', 1);
|
||||
self::requireMaxLength('name', 255);
|
||||
}
|
||||
|
||||
$obj = new passkeys_o();
|
||||
$obj->add((int)$user->id, false, $credential_id, $public_key, $algorithm, (array)$transports, $name);
|
||||
|
||||
(new logs_o())->add('user_security', 'global', 1, $user->id, 'USER_SECURITY_PASSKEYS_CREATE', 'Created passkey: ' . $obj->id);
|
||||
$response->success(['id' => $obj->id]);
|
||||
}, [
|
||||
'user_security_passkeys_create' => 'Create/add a new passkey for the authenticated user',
|
||||
]);
|
||||
|
||||
// Rename a passkey
|
||||
$this->patch('/account/security/passkeys/{id}', function () {
|
||||
global $response;
|
||||
self::requirePermission('user_security_passkeys_rename');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
(new logs_o())->add('user_security', 'global', 0, 0, 'USER_SECURITY_PASSKEYS_RENAME', 'User not logged in');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
$id = (int)self::fromRoute('id');
|
||||
self::requireParameterIntPositive($id, 'id');
|
||||
self::requireParameters(['name']);
|
||||
$name = (string)self::getParameter('name');
|
||||
self::requireType($name, self::type_string());
|
||||
self::requireMinLength('name', 1);
|
||||
self::requireMaxLength('name', 255);
|
||||
|
||||
$obj = (new passkeys_o())->select($id);
|
||||
if (!$obj->exists() || (int)$obj->user_id->value() !== (int)$user->id || (int)$obj->is_subuser->value() !== 0) {
|
||||
(new logs_o())->add('user_security', 'global', 0, $user->id, 'USER_SECURITY_PASSKEYS_RENAME', 'Passkey not found or not owned');
|
||||
$response->error('Not found', 404);
|
||||
}
|
||||
|
||||
$obj->update(['name' => $name]);
|
||||
(new logs_o())->add('user_security', 'global', 1, $user->id, 'USER_SECURITY_PASSKEYS_RENAME', 'Renamed passkey ' . $id);
|
||||
$response->success(['message' => 'Renamed', 'id' => $id]);
|
||||
}, [
|
||||
'user_security_passkeys_rename' => 'Rename a passkey that belongs to the authenticated user',
|
||||
]);
|
||||
|
||||
// Delete a passkey (soft delete)
|
||||
$this->delete('/account/security/passkeys/{id}', function () {
|
||||
global $response;
|
||||
self::requirePermission('user_security_passkeys_delete');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
(new logs_o())->add('user_security', 'global', 0, 0, 'USER_SECURITY_PASSKEYS_DELETE', 'User not logged in');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
$id = (int)self::fromRoute('id');
|
||||
self::requireParameterIntPositive($id, 'id');
|
||||
|
||||
$obj = (new passkeys_o())->select($id);
|
||||
if (!$obj->exists() || (int)$obj->user_id->value() !== (int)$user->id || (int)$obj->is_subuser->value() !== 0) {
|
||||
(new logs_o())->add('user_security', 'global', 0, $user->id, 'USER_SECURITY_PASSKEYS_DELETE', 'Passkey not found or not owned');
|
||||
$response->error('Not found', 404);
|
||||
}
|
||||
|
||||
$obj->delete();
|
||||
(new logs_o())->add('user_security', 'global', 1, $user->id, 'USER_SECURITY_PASSKEYS_DELETE', 'Deleted passkey ' . $id);
|
||||
$response->success(['message' => 'Deleted', 'id' => $id]);
|
||||
}, [
|
||||
'user_security_passkeys_delete' => 'Delete a passkey that belongs to the authenticated user',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user