diff --git a/apps/chronos/openapi/chronos-openapi.json b/apps/chronos/openapi/chronos-openapi.json index 9f7fe41f..104f8d4d 100644 --- a/apps/chronos/openapi/chronos-openapi.json +++ b/apps/chronos/openapi/chronos-openapi.json @@ -6190,7 +6190,12 @@ } } } - } + }, + "security": [ + { + "sessionAuth": [] + } + ] } }, "/doorlock/devices/update": { @@ -6422,7 +6427,12 @@ } } } - } + }, + "security": [ + { + "sessionAuth": [] + } + ] } }, "/doorlock/logs/export": { @@ -29380,12 +29390,7 @@ } } } - }, - "security": [ - { - "sessionAuth": [] - } - ] + } }, "patch": { "operationId": "patchNewsSystemMessagesById", @@ -29876,224 +29881,34 @@ ] } }, - "/ping": { - "get": { - "operationId": "getPing", - "description": "Health check endpoint that returns a pong response.", + "/notifications/fcm-tokens": { + "post": { + "operationId": "postNotificationsFcmTokens", + "description": "Register a push (FCM) token for the signed-in user", "tags": [ - "Ping" + "Notifications" ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ] - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "500": { - "description": "500", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deviceInfo": { + "type": "string" }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } + "token": { + "type": "string" + } + }, + "required": [ + "token" + ] } } } - } - } - }, - "/ping/uptime": { - "get": { - "operationId": "getPingUptime", - "description": "Get the uptime.", - "tags": [ - "Ping" - ], + }, "responses": { "200": { "description": "OK", @@ -30102,16 +29917,3168 @@ "schema": { "type": "object", "properties": { - "pretty": { - "type": "string" - }, - "uptime_ms": { - "type": "number" + "ok": { + "const": true } }, "required": [ - "pretty", - "uptime_ms" + "ok" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/notifications/fcm-tokens/{token}": { + "delete": { + "operationId": "deleteNotificationsFcmTokensByToken", + "description": "Unregister a push (FCM) token", + "tags": [ + "Notifications" + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "const": true + } + }, + "required": [ + "ok" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/notifications": { + "get": { + "operationId": "getNotifications", + "description": "List the signed-in user's notifications", + "tags": [ + "Notifications" + ], + "parameters": [ + { + "name": "dateFrom", + "in": "query", + "schema": { + "type": "string" + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "dateTo", + "in": "query", + "schema": { + "type": "string" + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "number", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "number", + "minimum": 0, + "default": 0 + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + }, + "allowEmptyValue": true, + "allowReserved": true + }, + { + "name": "unread", + "in": "query", + "schema": { + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "allowEmptyValue": true, + "allowReserved": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "read": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "content", + "createdAt", + "id", + "metadata", + "read", + "title", + "type", + "userId" + ] + } + }, + "total": { + "type": "number" + } + }, + "required": [ + "items", + "total" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/notifications/read-all": { + "post": { + "operationId": "postNotificationsReadAll", + "description": "Mark every notification of the signed-in user as read", + "tags": [ + "Notifications" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "const": true + } + }, + "required": [ + "ok" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/notifications/{id}/read": { + "post": { + "operationId": "postNotificationsByIdRead", + "description": "Mark one notification as read", + "tags": [ + "Notifications" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "read": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "content", + "createdAt", + "id", + "metadata", + "read", + "title", + "type", + "userId" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/notifications/test/preview": { + "post": { + "operationId": "postNotificationsTestPreview", + "description": "Development helper: render a notification template without sending it (needs CHRONOS_ENABLE_TEST_NOTIFICATIONS)", + "tags": [ + "Notifications" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "language": { + "enum": [ + "en", + "hu" + ], + "type": "string" + }, + "subject": { + "type": "string" + }, + "type": { + "enum": [ + "substitution", + "substitution_teacher", + "moved_lesson", + "announcement", + "system_message", + "blog_post", + "doorlock_card_used", + "cohort_reselection_required", + "test" + ], + "type": "string" + } + }, + "required": [ + "type" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "html": { + "type": "string" + } + }, + "required": [ + "html" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/notifications/test/send": { + "post": { + "operationId": "postNotificationsTestSend", + "description": "Development helper: send a test message over email, in-app and/or push (needs CHRONOS_ENABLE_TEST_NOTIFICATIONS)", + "tags": [ + "Notifications" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "channels": { + "type": "object", + "properties": { + "email": { + "type": "boolean", + "default": false + }, + "inApp": { + "type": "boolean", + "default": false + }, + "push": { + "type": "boolean", + "default": false + } + } + }, + "content": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "language": { + "enum": [ + "en", + "hu" + ], + "type": "string" + }, + "subject": { + "type": "string" + }, + "type": { + "enum": [ + "substitution", + "substitution_teacher", + "moved_lesson", + "announcement", + "system_message", + "blog_post", + "doorlock_card_used", + "cohort_reselection_required", + "test" + ], + "type": "string" + } + }, + "required": [ + "channels", + "type" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "boolean" + }, + "inApp": { + "type": "boolean" + }, + "push": { + "type": "boolean" + } + }, + "required": [ + "email", + "inApp", + "push" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/notifications/settings": { + "get": { + "operationId": "getNotificationsSettings", + "description": "Read the notification preferences of the signed-in user", + "tags": [ + "Notifications" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "language": { + "type": "string" + }, + "notificationPreferences": { + "type": "object", + "properties": { + "announcement": { + "type": "boolean" + }, + "blogPost": { + "type": "boolean" + }, + "channelsEnabled": { + "type": "boolean" + }, + "doorlockCardUsed": { + "type": "boolean" + }, + "movedLesson": { + "type": "boolean" + }, + "substitution": { + "type": "boolean" + }, + "systemMessage": { + "type": "boolean" + } + }, + "required": [ + "announcement", + "blogPost", + "channelsEnabled", + "doorlockCardUsed", + "movedLesson", + "substitution", + "systemMessage" + ] + }, + "theme": { + "type": "string" + }, + "timetableClassColors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "number" + } + }, + "timetableGroupDisplay": { + "type": "string" + }, + "timetableView": { + "type": "string" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "userId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "createdAt", + "id", + "language", + "notificationPreferences", + "theme", + "timetableClassColors", + "timetableGroupDisplay", + "timetableView", + "updatedAt", + "userId" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + }, + "patch": { + "operationId": "patchNotificationsSettings", + "description": "Update the notification preferences of the signed-in user", + "tags": [ + "Notifications" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "language": { + "type": "string" + }, + "notificationPreferences": { + "type": "object", + "properties": { + "announcement": { + "type": "boolean" + }, + "blogPost": { + "type": "boolean" + }, + "channelsEnabled": { + "type": "boolean" + }, + "doorlockCardUsed": { + "type": "boolean" + }, + "movedLesson": { + "type": "boolean" + }, + "substitution": { + "type": "boolean" + }, + "systemMessage": { + "type": "boolean" + } + } + }, + "theme": { + "type": "string" + }, + "timetableClassColors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "number" + } + }, + "timetableGroupDisplay": { + "enum": [ + "highlight", + "hide" + ], + "type": "string" + }, + "timetableView": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "language": { + "type": "string" + }, + "notificationPreferences": { + "type": "object", + "properties": { + "announcement": { + "type": "boolean" + }, + "blogPost": { + "type": "boolean" + }, + "channelsEnabled": { + "type": "boolean" + }, + "doorlockCardUsed": { + "type": "boolean" + }, + "movedLesson": { + "type": "boolean" + }, + "substitution": { + "type": "boolean" + }, + "systemMessage": { + "type": "boolean" + } + }, + "required": [ + "announcement", + "blogPost", + "channelsEnabled", + "doorlockCardUsed", + "movedLesson", + "substitution", + "systemMessage" + ] + }, + "theme": { + "type": "string" + }, + "timetableClassColors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "number" + } + }, + "timetableGroupDisplay": { + "type": "string" + }, + "timetableView": { + "type": "string" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "userId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "createdAt", + "id", + "language", + "notificationPreferences", + "theme", + "timetableClassColors", + "timetableGroupDisplay", + "timetableView", + "updatedAt", + "userId" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/notifications/test": { + "post": { + "operationId": "postNotificationsTest", + "description": "Development helper: drop a test notification into the caller's own inbox (needs CHRONOS_ENABLE_TEST_NOTIFICATIONS)", + "tags": [ + "Notifications" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "read": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "content", + "createdAt", + "id", + "metadata", + "read", + "title", + "type", + "userId" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/notifications/unread-count": { + "get": { + "operationId": "getNotificationsUnreadCount", + "description": "Count the signed-in user's unread notifications", + "tags": [ + "Notifications" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "number" + } + }, + "required": [ + "count" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/ping": { + "get": { + "operationId": "getPing", + "description": "Health check endpoint that returns a pong response.", + "tags": [ + "Ping" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + } + } + }, + "/ping/uptime": { + "get": { + "operationId": "getPingUptime", + "description": "Get the uptime.", + "tags": [ + "Ping" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pretty": { + "type": "string" + }, + "uptime_ms": { + "type": "number" + } + }, + "required": [ + "pretty", + "uptime_ms" ] } } @@ -30545,7 +33512,12 @@ } } } - } + }, + "security": [ + { + "sessionAuth": [] + } + ] }, "get": { "operationId": "getRoles", @@ -30776,228 +33748,238 @@ } } } - } - } - }, - "/roles/{name}": { - "delete": { - "operationId": "deleteRolesByName", - "description": "Delete a role", - "tags": [ - "Roles" - ], - "parameters": [ + }, + "security": [ + { + "sessionAuth": [] + } + ] + } + }, + "/roles/{name}": { + "delete": { + "operationId": "deleteRolesByName", + "description": "Delete a role", + "tags": [ + "Roles" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "const": true + } + }, + "required": [ + "ok" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + } + }, + "security": [ { - "name": "name", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "ok": { - "const": true - } - }, - "required": [ - "ok" - ] - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "500": { - "description": "500", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } + "sessionAuth": [] } - } + ] }, "patch": { "operationId": "patchRolesByName", @@ -31244,7 +34226,12 @@ } } } - } + }, + "security": [ + { + "sessionAuth": [] + } + ] } }, "/roles/permissions": { @@ -31462,7 +34449,12 @@ } } } - } + }, + "security": [ + { + "sessionAuth": [] + } + ] } }, "/timetable/timetables/cleanup-orphaned-cohorts": { @@ -32464,12 +35456,7 @@ } } } - }, - "security": [ - { - "sessionAuth": [] - } - ] + } } }, "/timetable/timetables": { @@ -32734,12 +35721,7 @@ } } } - }, - "security": [ - { - "sessionAuth": [] - } - ] + } } }, "/timetable/timetables/{id}/preview-delete": { @@ -33311,12 +36293,7 @@ } } } - }, - "security": [ - { - "sessionAuth": [] - } - ] + } } }, "/timetable/import": { @@ -33587,7 +36564,12 @@ } } } - } + }, + "security": [ + { + "sessionAuth": [] + } + ] } }, "/timetable/classrooms/getAll": { @@ -34532,12 +37514,7 @@ } } } - }, - "security": [ - { - "sessionAuth": [] - } - ] + } } }, "/timetable/groups/getForCohort/{cohortId}": { @@ -34832,12 +37809,7 @@ } } } - }, - "security": [ - { - "sessionAuth": [] - } - ] + } } }, "/timetable/groups/select": { @@ -35342,259 +38314,254 @@ } } } - } - } - }, - "/timetable/subjects": { - "get": { - "operationId": "getTimetableSubjects", - "description": "Get all subjects from the database.", - "tags": [ - "Subject" - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "createdAt": { - "type": "string", - "format": "date-time", - "x-native-type": "date" - }, - "id": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "short": { - "type": "string" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "x-native-type": "date" - } - }, - "required": [ - "createdAt", - "id", - "name", - "short", - "updatedAt" - ] - } - }, - "total": { - "type": "number" - } - }, - "required": [ - "items", - "total" - ] - } - } - } - }, - "400": { - "description": "400", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "401": { - "description": "401", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "403": { - "description": "403", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "404": { - "description": "404", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "409": { - "description": "409", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "429": { - "description": "429", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - }, - "500": { - "description": "500", - "content": { - "application/json": { - "schema": { - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - }, - "required": [ - "code", - "message", - "status" - ], - "type": "object" - } - } - } - } - }, - "security": [ - { - "sessionAuth": [] + } + } + }, + "/timetable/subjects": { + "get": { + "operationId": "getTimetableSubjects", + "description": "Get all subjects from the database.", + "tags": [ + "Subject" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "short": { + "type": "string" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "x-native-type": "date" + } + }, + "required": [ + "createdAt", + "id", + "name", + "short", + "updatedAt" + ] + } + }, + "total": { + "type": "number" + } + }, + "required": [ + "items", + "total" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "integer" + } + }, + "required": [ + "code", + "message", + "status" + ], + "type": "object" + } + } + } } - ] + } } }, "/timetable/teachers/getAll": { @@ -41095,12 +44062,7 @@ } } } - }, - "security": [ - { - "sessionAuth": [] - } - ] + } } }, "/timetable/substitutions/{id}": { @@ -42524,12 +45486,7 @@ } } } - }, - "security": [ - { - "sessionAuth": [] - } - ] + } } }, "/timetable/substitutions/cohort/{cohortId}": { @@ -42881,12 +45838,7 @@ } } } - }, - "security": [ - { - "sessionAuth": [] - } - ] + } } }, "/timetable/movedLessons": { diff --git a/apps/chronos/src/utils/openapi-spec.ts b/apps/chronos/src/utils/openapi-spec.ts index c4805ff7..5f1a92dd 100644 --- a/apps/chronos/src/utils/openapi-spec.ts +++ b/apps/chronos/src/utils/openapi-spec.ts @@ -33,10 +33,6 @@ export const openApiSpecOptions = { required: ['code', 'message', 'status'], type: 'object', }), - // The notifications endpoints are internal to the apps: they predate the - // OpenAPI metadata convention, so they are procedures without a public - // description. Drop the `filter` to publish them. - filter: ({ path }) => path[0] !== 'notifications', info: { description: 'API for consumption by the Filc app family.', title: 'Chronos backend API', diff --git a/apps/iris/src/utils/orpc.ts b/apps/iris/src/utils/orpc.ts index 08087352..9e4c1149 100644 --- a/apps/iris/src/utils/orpc.ts +++ b/apps/iris/src/utils/orpc.ts @@ -6,24 +6,11 @@ import type { } from '@tanstack/react-query'; import { createIsomorphicFn } from '@tanstack/react-start'; import { getRequestHeaders } from '@tanstack/react-start/server'; -import { requestOrigin } from '@/utils/request'; +import { fallbackApiOrigin, requestOrigin } from '@/utils/request'; /** The API base as a URL prefix: image URLs are built from it by hand. */ export const apiBaseUrl = '/api'; -/** - * Absolute origin for a server render whose own origin could not be read. - * - * oRPC builds a `URL` from this value, so a relative one is fatal: the call - * throws `Failed to construct 'URL': Invalid URL` before anything is sent. - * That is why the fallback has to be absolute and not `apiBaseUrl`. In - * development it points straight at Chronos, which is also where the Vite - * proxy sends `/api`; in production the request origin is always available and - * this is only a safety net. - */ -const fallbackApiOrigin = - import.meta.env.VITE_API_ORIGIN ?? 'http://localhost:3001'; - const client = createIsomorphicFn() .client(() => createApiClient({ diff --git a/apps/iris/src/utils/request.ts b/apps/iris/src/utils/request.ts index 60ccdf1f..40592045 100644 --- a/apps/iris/src/utils/request.ts +++ b/apps/iris/src/utils/request.ts @@ -6,6 +6,19 @@ import { getRequestUrl, } from '@tanstack/react-start/server'; +/** + * Absolute origin for a server render whose own origin could not be read. + * + * A relative origin is fatal for the callers below — oRPC builds a `URL` from + * it, and the browser constructor rejects a relative one outright. The + * development server is exactly that case: `getRequestUrl` answers nothing + * there, so a server render has no origin of its own to use. Development points + * straight at Chronos, which is also where the Vite proxy sends `/api`; behind + * the platform proxy the request origin is always available and this is unused. + */ +export const fallbackApiOrigin = + import.meta.env.VITE_API_ORIGIN ?? 'http://localhost:3001'; + /** * The origin the request being rendered came in on, e.g. * `https://filc.petrik.hu`. @@ -17,19 +30,17 @@ import { * origin, so parsing it would send a server render's API calls back to * localhost instead of out to Chronos. * - * Returns `null` where there is no request to read — outside a server render, - * such as during the build. + * Returns `null` where there is no request origin to read — the development + * server and the build both qualify — so callers use `fallbackApiOrigin`. */ export const requestOrigin = () => createIsomorphicFn() .server(() => { try { - // `xForwardedHost` is off by default in the accessor, and the host is - // the half that matters here: behind the platform proxy it is the - // public origin, while the request object still carries the internal - // one. `null` outside a request — during the build, say. - return getRequestUrl({ xForwardedHost: true, xForwardedProto: true }) - .origin; + return getRequestUrl({ + xForwardedHost: true, + xForwardedProto: true, + }).origin; } catch { return null; } @@ -53,10 +64,10 @@ export const fetchLanguage = () => export const fetchSession = (): Promise => createIsomorphicFn() .server(async () => { - const origin = requestOrigin(); - if (!origin) { - return null; - } + // Without an origin to read — the development server — the read used to + // bail out, which left every server render looking signed out and sent the + // browser's guard to /auth/login on the next refresh. + const origin = requestOrigin() ?? fallbackApiOrigin; try { const response = await fetch(`${origin}/api/auth/get-session`, { diff --git a/apps/iris/vite.config.ts b/apps/iris/vite.config.ts index ed53718f..8008d6c1 100644 --- a/apps/iris/vite.config.ts +++ b/apps/iris/vite.config.ts @@ -19,7 +19,14 @@ export default defineConfig({ tanstackStart(), viteReact(), tailwindcss(), - devtools(), + // `consolePiping` is off because the pipe patches `console` in *both* realms + // and feeds each side's output to the other: a server log printed in the + // browser console is then piped back as a client log, and each round embeds + // the previous message — tens of thousands of lines a minute, which also + // stalls hydration. 0.8.5 is the latest release, so re-enable it when + // upstream stops echoing. Server logs still reach the terminal, browser + // logs still reach the browser console. + devtools({ consolePiping: { enabled: false } }), sentryVitePlugin({ org: 'filc', project: Bun.env.PROD_DEPLOY ? 'iris' : 'iris-preview', diff --git a/packages/api/src/contract/doorlock.ts b/packages/api/src/contract/doorlock.ts index 6b1c0cae..82b1bfd1 100644 --- a/packages/api/src/contract/doorlock.ts +++ b/packages/api/src/contract/doorlock.ts @@ -156,6 +156,7 @@ export const doorlockContract = { triggerOta: oc .route( filcRoute({ + auth: true, description: 'Trigger an OTA update on a specific device', method: 'POST', operationId: 'postDoorlockDevicesByIdUpdate', @@ -181,6 +182,7 @@ export const doorlockContract = { updateAll: oc .route( filcRoute({ + auth: true, description: 'Trigger an OTA update on all devices', method: 'POST', operationId: 'postDoorlockDevicesUpdate', diff --git a/packages/api/src/contract/news.ts b/packages/api/src/contract/news.ts index 9dfa51f2..fca65545 100644 --- a/packages/api/src/contract/news.ts +++ b/packages/api/src/contract/news.ts @@ -285,7 +285,6 @@ export const newsContract = { get: oc .route( filcRoute({ - auth: true, description: 'Get a single system message by ID', method: 'GET', operationId: 'getNewsSystemMessagesById', diff --git a/packages/api/src/contract/notifications.ts b/packages/api/src/contract/notifications.ts index c4b95691..99663b12 100644 --- a/packages/api/src/contract/notifications.ts +++ b/packages/api/src/contract/notifications.ts @@ -15,42 +15,169 @@ import { updateSettingsSchema, userPreferencesSchema, } from '../domains/notifications'; +import { filcRoute } from './route'; /** - * The notification inbox and per-user preferences. + * The notification inbox, the per-user preferences and push-token registration. * - * These procedures deliberately have no `filcRoute` metadata, so the generated - * document filters this subtree out and the endpoints exist only for the typed - * RPC client. The original reason was keeping the document identical to the - * pre-migration one; that document and the `x-filc_*` extensions are gone, so - * this is now a standing choice rather than a constraint — worth revisiting if - * an external client should ever reach the inbox. `GET|POST - * /api/notifications/unsubscribe` is not part of the contract either — it + * Every procedure here is documented, because they are an external-client + * surface: the mobile app registers its FCM token, reads the inbox and edits + * its preferences through them. + * + * The three `*Test*` procedures are development helpers, each refused unless + * `CHRONOS_ENABLE_TEST_NOTIFICATIONS` is set (it defaults to off, and + * `sendTest` takes a free-form address to send real mail to), which is why + * their descriptions say so. + * + * `GET|POST /api/notifications/unsubscribe` is not part of the contract: it * renders HTML from a plain handler in - * `apps/chronos/src/routes/notifications/unsubscribe-html.ts`. + * `apps/chronos/src/modules/notifications/unsubscribe-html.ts`. */ export const notificationsContract = { fcmTokens: { - register: oc.input(fcmTokenSchema).output(notificationsOkResponseSchema), + register: oc + .route( + filcRoute({ + auth: true, + description: 'Register a push (FCM) token for the signed-in user', + method: 'POST', + operationId: 'postNotificationsFcmTokens', + path: '/notifications/fcm-tokens', + tags: ['Notifications'], + }) + ) + .input(fcmTokenSchema) + .output(notificationsOkResponseSchema), unregister: oc + .route( + filcRoute({ + auth: true, + description: 'Unregister a push (FCM) token', + method: 'DELETE', + operationId: 'deleteNotificationsFcmTokensByToken', + path: '/notifications/fcm-tokens/{token}', + tags: ['Notifications'], + }) + ) .input(tokenDeleteSchema) .output(notificationsOkResponseSchema), }, /** Paged history for the signed-in user, filtered by type/unread/date. */ - list: oc.input(paginationSchema).output(notificationListResponseSchema), - markAllAsRead: oc.output(notificationsOkResponseSchema), + list: oc + .route( + filcRoute({ + auth: true, + description: "List the signed-in user's notifications", + method: 'GET', + operationId: 'getNotifications', + path: '/notifications', + tags: ['Notifications'], + }) + ) + .input(paginationSchema) + .output(notificationListResponseSchema), + markAllAsRead: oc + .route( + filcRoute({ + auth: true, + description: 'Mark every notification of the signed-in user as read', + method: 'POST', + operationId: 'postNotificationsReadAll', + path: '/notifications/read-all', + tags: ['Notifications'], + }) + ) + .output(notificationsOkResponseSchema), markAsRead: oc + .route( + filcRoute({ + auth: true, + description: 'Mark one notification as read', + method: 'POST', + operationId: 'postNotificationsByIdRead', + path: '/notifications/{id}/read', + tags: ['Notifications'], + }) + ) .input(notificationIdParamsSchema) .output(notificationRowSchema), - /** Dev-only: render a notification template without sending it. */ previewTest: oc + .route( + filcRoute({ + auth: true, + description: + 'Development helper: render a notification template without sending it (needs CHRONOS_ENABLE_TEST_NOTIFICATIONS)', + method: 'POST', + operationId: 'postNotificationsTestPreview', + path: '/notifications/test/preview', + tags: ['Notifications'], + }) + ) .input(previewTestNotificationSchema) .output(previewTestResponseSchema), - /** Dev-only: send a test message over email, in-app and/or push. */ - sendTest: oc.input(sendTestNotificationSchema).output(sendTestResultSchema), - settings: oc.output(userPreferencesSchema), - /** Dev-only: drop a test notification into the caller's own inbox. */ - test: oc.output(notificationRowSchema), - unreadCount: oc.output(unreadCountResponseSchema), - updateSettings: oc.input(updateSettingsSchema).output(userPreferencesSchema), + sendTest: oc + .route( + filcRoute({ + auth: true, + description: + 'Development helper: send a test message over email, in-app and/or push (needs CHRONOS_ENABLE_TEST_NOTIFICATIONS)', + method: 'POST', + operationId: 'postNotificationsTestSend', + path: '/notifications/test/send', + tags: ['Notifications'], + }) + ) + .input(sendTestNotificationSchema) + .output(sendTestResultSchema), + settings: oc + .route( + filcRoute({ + auth: true, + description: 'Read the notification preferences of the signed-in user', + method: 'GET', + operationId: 'getNotificationsSettings', + path: '/notifications/settings', + tags: ['Notifications'], + }) + ) + .output(userPreferencesSchema), + test: oc + .route( + filcRoute({ + auth: true, + description: + "Development helper: drop a test notification into the caller's own inbox (needs CHRONOS_ENABLE_TEST_NOTIFICATIONS)", + method: 'POST', + operationId: 'postNotificationsTest', + path: '/notifications/test', + tags: ['Notifications'], + }) + ) + .output(notificationRowSchema), + unreadCount: oc + .route( + filcRoute({ + auth: true, + description: "Count the signed-in user's unread notifications", + method: 'GET', + operationId: 'getNotificationsUnreadCount', + path: '/notifications/unread-count', + tags: ['Notifications'], + }) + ) + .output(unreadCountResponseSchema), + updateSettings: oc + .route( + filcRoute({ + auth: true, + description: + 'Update the notification preferences of the signed-in user', + method: 'PATCH', + operationId: 'patchNotificationsSettings', + path: '/notifications/settings', + tags: ['Notifications'], + }) + ) + .input(updateSettingsSchema) + .output(userPreferencesSchema), }; diff --git a/packages/api/src/contract/roles.ts b/packages/api/src/contract/roles.ts index 59f3edab..ded5d2a2 100644 --- a/packages/api/src/contract/roles.ts +++ b/packages/api/src/contract/roles.ts @@ -14,6 +14,7 @@ export const rolesContract = { create: oc .route( filcRoute({ + auth: true, description: 'Create a new role', method: 'POST', operationId: 'postRoles', @@ -27,6 +28,7 @@ export const rolesContract = { delete: oc .route( filcRoute({ + auth: true, description: 'Delete a role', method: 'DELETE', operationId: 'deleteRolesByName', @@ -39,6 +41,7 @@ export const rolesContract = { list: oc .route( filcRoute({ + auth: true, description: 'List all roles with their permissions', method: 'GET', operationId: 'getRoles', @@ -50,6 +53,7 @@ export const rolesContract = { permissions: oc .route( filcRoute({ + auth: true, description: 'List all known permissions registered by the application', method: 'GET', operationId: 'getRolesPermissions', @@ -61,6 +65,7 @@ export const rolesContract = { update: oc .route( filcRoute({ + auth: true, description: 'Update permissions for a role', method: 'PATCH', operationId: 'patchRolesByName', diff --git a/packages/api/src/contract/timetable/import.ts b/packages/api/src/contract/timetable/import.ts index 961dbf7f..6fa1614c 100644 --- a/packages/api/src/contract/timetable/import.ts +++ b/packages/api/src/contract/timetable/import.ts @@ -10,6 +10,7 @@ export const importContract = { import: oc .route( filcRoute({ + auth: true, description: 'Import a timetable from an Oman or aSc 2012 XML file.', method: 'POST', operationId: 'postTimetableImport', diff --git a/packages/api/src/contract/timetable/people.ts b/packages/api/src/contract/timetable/people.ts index 6bd9167a..2010dab1 100644 --- a/packages/api/src/contract/timetable/people.ts +++ b/packages/api/src/contract/timetable/people.ts @@ -57,7 +57,6 @@ export const peopleContract = { getAllForTimetable: oc .route( filcRoute({ - auth: true, description: 'Get cohorts for a given timetable from the database.', method: 'GET', operationId: 'getTimetableCohortsGetAllForTimetableByTimetableId', @@ -72,7 +71,6 @@ export const peopleContract = { getForCohort: oc .route( filcRoute({ - auth: true, description: 'Get the groups of a cohort, marking the current user selection.', method: 'GET', @@ -116,7 +114,6 @@ export const peopleContract = { subjects: oc .route( filcRoute({ - auth: true, description: 'Get all subjects from the database.', method: 'GET', operationId: 'getTimetableSubjects', diff --git a/packages/api/src/contract/timetable/substitutions.ts b/packages/api/src/contract/timetable/substitutions.ts index d217ea04..b21f8d56 100644 --- a/packages/api/src/contract/timetable/substitutions.ts +++ b/packages/api/src/contract/timetable/substitutions.ts @@ -60,7 +60,6 @@ export const substitutionsContract = { list: oc .route( filcRoute({ - auth: true, description: 'Get all substitutions from the database.', method: 'GET', operationId: 'getTimetableSubstitutions', @@ -86,7 +85,6 @@ export const substitutionsContract = { relevant: oc .route( filcRoute({ - auth: true, description: 'Get relevant substitutions from the database.', method: 'GET', operationId: 'getTimetableSubstitutionsRelevant', @@ -98,7 +96,6 @@ export const substitutionsContract = { relevantForCohort: oc .route( filcRoute({ - auth: true, description: 'Get relevant substitutions for a given cohort from the database.', method: 'GET', diff --git a/packages/api/src/contract/timetable/timetables.ts b/packages/api/src/contract/timetable/timetables.ts index e92750eb..a5018b5b 100644 --- a/packages/api/src/contract/timetable/timetables.ts +++ b/packages/api/src/contract/timetable/timetables.ts @@ -43,7 +43,6 @@ export const timetablesContract = { latestValid: oc .route( filcRoute({ - auth: true, description: 'Get the latest valid timetable.', method: 'GET', operationId: 'getTimetableTimetablesLatestValid', @@ -55,7 +54,6 @@ export const timetablesContract = { list: oc .route( filcRoute({ - auth: true, description: 'Get all timetables from the database.', method: 'GET', operationId: 'getTimetableTimetables', @@ -94,7 +92,6 @@ export const timetablesContract = { valid: oc .route( filcRoute({ - auth: true, description: 'Get all the latest valid timetables.', method: 'GET', operationId: 'getTimetableTimetablesValid',