diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..e6d2c12 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,686 @@ +openapi: 3.1.0 +info: + title: HelpYourNeighbour API + version: 0.2.0 + description: API contract for authentication, requests, offers, contacts, profile, addresses, and reviews. +servers: + - url: http://localhost:3000 + description: Local development +security: + - bearerAuth: [] +paths: + /health: + get: + tags: [system] + summary: Health check + security: [] + responses: + '200': + description: Service healthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + + /auth/register: + post: + tags: [auth] + summary: Register a new user + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + responses: + '201': + description: User created + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponse' + '400': + $ref: '#/components/responses/BadRequest' + '409': + description: Email already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Registration failed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /auth/login: + post: + tags: [auth] + summary: Login with email/password + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: Login success + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + description: Invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /requests: + get: + tags: [requests] + summary: List help requests + security: [] + responses: + '200': + description: Request list + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/HelpRequest' + post: + tags: [requests] + summary: Create help request + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateHelpRequest' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /offers/{requestId}: + post: + tags: [offers] + summary: Create offer for help request + parameters: + - $ref: '#/components/parameters/RequestId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOfferRequest' + responses: + '201': + description: Offer created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequestSimple' + '401': + $ref: '#/components/responses/Unauthorized' + + /offers/negotiation/{offerId}: + post: + tags: [offers] + summary: Create counter-offer/negotiation message + parameters: + - $ref: '#/components/parameters/OfferId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOfferRequest' + responses: + '201': + description: Negotiation entry created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequestSimple' + '401': + $ref: '#/components/responses/Unauthorized' + + /offers/accept/{offerId}: + post: + tags: [offers] + summary: Accept offer and create deal + parameters: + - $ref: '#/components/parameters/OfferId' + responses: + '201': + description: Deal created + content: + application/json: + schema: + $ref: '#/components/schemas/DealCreatedResponse' + '400': + description: Invalid offer id + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: Offer not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /contacts/request: + post: + tags: [contacts] + summary: Request contact exchange for a deal + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContactRequestCreate' + responses: + '201': + description: Request created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Deal not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Request already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /contacts/respond: + post: + tags: [contacts] + summary: Accept or reject contact exchange request + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ContactRequestRespond' + responses: + '200': + description: Response processed + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Request not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /contacts/deal/{dealId}: + get: + tags: [contacts] + summary: List accepted contact exchanges for deal + parameters: + - $ref: '#/components/parameters/DealId' + responses: + '200': + description: Accepted contact exchange rows + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ContactExchangeRow' + '400': + description: Invalid dealId + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Deal not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /addresses/change-request: + post: + tags: [addresses] + summary: Request address change with postal verification code + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddressChangeRequest' + responses: + '201': + description: Change request created + content: + application/json: + schema: + $ref: '#/components/schemas/AddressChangeCreatedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /addresses/verify: + post: + tags: [addresses] + summary: Verify address change with 6-digit code + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddressVerifyRequest' + responses: + '200': + description: Address verified + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + description: Invalid payload or code + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + description: Request not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: Request not pending + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /profile/phone: + post: + tags: [profile] + summary: Update encrypted phone number + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PhoneUpdateRequest' + responses: + '200': + description: Phone updated + content: + application/json: + schema: + $ref: '#/components/schemas/StatusResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /reviews/{dealId}: + post: + tags: [reviews] + summary: Create review for deal + parameters: + - $ref: '#/components/parameters/DealId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateReviewRequest' + responses: + '201': + description: Review created + content: + application/json: + schema: + $ref: '#/components/schemas/IdResponse' + '400': + $ref: '#/components/responses/BadRequestSimple' + '401': + $ref: '#/components/responses/Unauthorized' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + parameters: + RequestId: + name: requestId + in: path + required: true + schema: + type: integer + minimum: 1 + OfferId: + name: offerId + in: path + required: true + schema: + type: integer + minimum: 1 + DealId: + name: dealId + in: path + required: true + schema: + type: integer + minimum: 1 + + responses: + BadRequest: + description: Validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + BadRequestSimple: + description: Invalid payload + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + Unauthorized: + description: Missing or invalid token + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + Forbidden: + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + schemas: + HealthResponse: + type: object + additionalProperties: false + properties: + status: + type: string + example: ok + required: [status] + + ErrorResponse: + type: object + properties: + error: + oneOf: + - type: string + - type: object + + StatusResponse: + type: object + additionalProperties: false + properties: + status: + type: string + required: [status] + + IdResponse: + type: object + additionalProperties: false + properties: + id: + type: integer + minimum: 1 + required: [id] + + DealCreatedResponse: + type: object + additionalProperties: false + properties: + dealId: + type: integer + minimum: 1 + required: [dealId] + + AuthTokenResponse: + type: object + additionalProperties: false + properties: + token: + type: string + required: [token] + + RegisterRequest: + type: object + additionalProperties: false + properties: + email: + type: string + format: email + password: + type: string + minLength: 8 + displayName: + type: string + minLength: 2 + maxLength: 120 + required: [email, password, displayName] + + LoginRequest: + type: object + additionalProperties: false + properties: + email: + type: string + format: email + password: + type: string + minLength: 1 + required: [email, password] + + HelpRequest: + type: object + properties: + id: + type: integer + title: + type: string + description: + type: string + value_chf: + type: number + status: + type: string + created_at: + type: string + format: date-time + requester_name: + type: string + + CreateHelpRequest: + type: object + additionalProperties: false + properties: + title: + type: string + minLength: 3 + maxLength: 180 + description: + type: string + minLength: 5 + valueChf: + type: number + exclusiveMinimum: 0 + required: [title, description, valueChf] + + CreateOfferRequest: + type: object + additionalProperties: false + properties: + amountChf: + type: number + exclusiveMinimum: 0 + message: + type: string + maxLength: 2000 + required: [amountChf] + + ContactRequestCreate: + type: object + additionalProperties: false + properties: + dealId: + type: integer + minimum: 1 + targetUserId: + type: integer + minimum: 1 + required: [dealId, targetUserId] + + ContactRequestRespond: + type: object + additionalProperties: false + properties: + requestId: + type: integer + minimum: 1 + accept: + type: boolean + required: [requestId, accept] + + ContactExchangeRow: + type: object + properties: + id: + type: integer + requester_id: + type: integer + target_id: + type: integer + accepted: + type: boolean + requester_phone_encrypted: + type: string + target_phone_encrypted: + type: string + + AddressChangeRequest: + type: object + additionalProperties: false + properties: + newAddress: + type: string + minLength: 10 + required: [newAddress] + + AddressChangeCreatedResponse: + type: object + additionalProperties: false + properties: + requestId: + type: integer + minimum: 1 + postalDispatch: + type: string + example: pending_letter + note: + type: string + verificationCode: + type: string + pattern: '^\\d{6}$' + required: [requestId, postalDispatch, note, verificationCode] + + AddressVerifyRequest: + type: object + additionalProperties: false + properties: + requestId: + type: integer + minimum: 1 + code: + type: string + pattern: '^\\d{6}$' + required: [requestId, code] + + PhoneUpdateRequest: + type: object + additionalProperties: false + properties: + phone: + type: string + minLength: 6 + maxLength: 40 + required: [phone] + + CreateReviewRequest: + type: object + additionalProperties: false + properties: + revieweeId: + type: integer + minimum: 1 + rating: + type: integer + minimum: 1 + maximum: 5 + comment: + type: string + maxLength: 2000 + required: [revieweeId, rating]