feat: add API documentation and Swagger config
- Add comprehensive Swagger/OpenAPI documentation - Include all API endpoints with request/response schemas - Add authentication and authorization documentation - Include example requests and responses - Configure Swagger UI integration for easy API testing
This commit is contained in:
887
docs/swagger.json
Normal file
887
docs/swagger.json
Normal file
@@ -0,0 +1,887 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "SecNex IDP API",
|
||||
"version": "0.0.1",
|
||||
"description": "OpenAPI specification for SecNex Identity Provider API"
|
||||
},
|
||||
"paths": {
|
||||
"/readyz": {
|
||||
"get": {
|
||||
"tags": ["Healthcheck"],
|
||||
"summary": "Readiness probe",
|
||||
"description": "Check if the service is ready to accept traffic.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Service is ready"
|
||||
},
|
||||
"500": {
|
||||
"description": "Service is not ready"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/livez": {
|
||||
"get": {
|
||||
"tags": ["Healthcheck"],
|
||||
"summary": "Liveness probe",
|
||||
"description": "Check if the service is alive.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Service is alive"
|
||||
},
|
||||
"500": {
|
||||
"description": "Service is not alive"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/login": {
|
||||
"post": {
|
||||
"tags": ["Authentication"],
|
||||
"summary": "Login",
|
||||
"description": "Logs in a user.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string",
|
||||
"example": "john.doe"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"example": "password"
|
||||
}
|
||||
},
|
||||
"required": ["username", "password"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful login",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session": {
|
||||
"type": "string",
|
||||
"example": "session:123e4567-e89b-12d3-a456-426614174000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request body"
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid username or password"
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to verify password or create session"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/logout": {
|
||||
"post": {
|
||||
"tags": ["Authentication"],
|
||||
"summary": "Logout",
|
||||
"description": "Logs out a user by invalidating their session.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session": {
|
||||
"type": "string",
|
||||
"example": "session:123e4567-e89b-12d3-a456-426614174000"
|
||||
}
|
||||
},
|
||||
"required": ["session"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successfully logged out",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "null"
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request body"
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to logout session"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/session": {
|
||||
"post": {
|
||||
"tags": ["Authentication"],
|
||||
"summary": "Get session info",
|
||||
"description": "Returns information about a session.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session": {
|
||||
"type": "string",
|
||||
"example": "session:123e4567-e89b-12d3-a456-426614174000"
|
||||
}
|
||||
},
|
||||
"required": ["session"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Session information",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/Session"
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request body"
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid session"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/user/": {
|
||||
"get": {
|
||||
"tags": ["User Management"],
|
||||
"summary": "Get all users",
|
||||
"description": "Returns a list of all users. Requires authentication.",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of users",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to get users"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/": {
|
||||
"get": {
|
||||
"tags": ["Session Management"],
|
||||
"summary": "Get sessions",
|
||||
"description": "Returns a paginated list of sessions. Requires authentication.",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"description": "Page number",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "Number of items per page",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"in": "query",
|
||||
"description": "Filter by user ID",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Paginated list of sessions",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Session"
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"pagination": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_pages": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"_links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"first": {
|
||||
"type": "string"
|
||||
},
|
||||
"last": {
|
||||
"type": "string"
|
||||
},
|
||||
"prev": {
|
||||
"type": "string"
|
||||
},
|
||||
"next": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid page or limit"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to get sessions"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/info": {
|
||||
"get": {
|
||||
"tags": ["Session Management"],
|
||||
"summary": "Get session by session token",
|
||||
"description": "Returns session information by session token. Requires authentication.",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "session",
|
||||
"in": "query",
|
||||
"description": "Base64 encoded session token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Session information",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/Session"
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid session token"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/revoke": {
|
||||
"delete": {
|
||||
"tags": ["Session Management"],
|
||||
"summary": "Revoke session by session token",
|
||||
"description": "Revokes a session using the session token. Requires authentication.",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "session",
|
||||
"in": "query",
|
||||
"description": "Base64 encoded session token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Session revoked successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Session 123e4567-e89b-12d3-a456-426614174000 revoked"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "REVOKED"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid session token"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to revoke session"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/{session_id}/revoke": {
|
||||
"delete": {
|
||||
"tags": ["Session Management"],
|
||||
"summary": "Revoke session by ID",
|
||||
"description": "Revokes a session using the session ID. Requires authentication.",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"description": "Session ID",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Session revoked successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Session 123e4567-e89b-12d3-a456-426614174000 revoked"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "REVOKED"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to revoke session"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/revoke/all": {
|
||||
"delete": {
|
||||
"tags": ["Session Management"],
|
||||
"summary": "Revoke all sessions",
|
||||
"description": "Revokes all sessions for all users. Requires authentication.",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All sessions revoked successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "All sessions revoked for all users"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "REVOKED"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to revoke all sessions"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/revoke/all/user": {
|
||||
"delete": {
|
||||
"tags": ["Session Management"],
|
||||
"summary": "Revoke all sessions for user",
|
||||
"description": "Revokes all sessions for a specific user. Requires authentication.",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "user",
|
||||
"in": "query",
|
||||
"description": "User ID",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All sessions for user revoked successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "All sessions revoked for user 123e4567-e89b-12d3-a456-426614174000"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "REVOKED"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"500": {
|
||||
"description": "Failed to revoke all sessions"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/test/": {
|
||||
"get": {
|
||||
"tags": ["Test"],
|
||||
"summary": "Test endpoint",
|
||||
"description": "Test endpoint that returns user information and IP address. Requires authentication.",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Test response with user info and IP",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ip": {
|
||||
"type": "string",
|
||||
"example": "192.168.1.1"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"BearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "session:base64encodedtoken"
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"example": "123e4567-e89b-12d3-a456-426614174000"
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string",
|
||||
"example": "John"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string",
|
||||
"example": "Doe"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"example": "john.doe"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"example": "john.doe@example.com"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"verified": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"super_admin": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2021-01-01T00:00:00Z"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2021-01-01T00:00:00Z"
|
||||
},
|
||||
"deleted_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Session": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"example": "123e4567-e89b-12d3-a456-426614174000"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"example": "123e4567-e89b-12d3-a456-426614174000"
|
||||
},
|
||||
"revoked": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"logged_out": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"expires_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2021-01-02T00:00:00Z"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2021-01-01T00:00:00Z"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2021-01-01T00:00:00Z"
|
||||
},
|
||||
"deleted_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiKey": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"example": "123e4567-e89b-12d3-a456-426614174000"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"example": "ak_1234567890abcdef"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"nullable": true,
|
||||
"example": "123e4567-e89b-12d3-a456-426614174000"
|
||||
},
|
||||
"revoked": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"expires_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"example": "2021-01-02T00:00:00Z"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2021-01-01T00:00:00Z"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"example": "2021-01-01T00:00:00Z"
|
||||
},
|
||||
"deleted_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user