From b6a02e9eaae0bcf8a3596722eb2f262dbcbc4e76 Mon Sep 17 00:00:00 2001 From: Igor Polyakov Date: Wed, 19 Jun 2024 23:02:50 +0300 Subject: [PATCH] Add login to users - #15 --- api/openapi.yaml | 1662 +++++++++-------- go.mod | 1 + go.sum | 2 + internal/app/database/struct_updater.go | 2 + .../app/database/update0012_update0013.go | 30 + .../database/update0013_update0013testdata.go | 46 + internal/app/db/user.go | 1 + internal/app/handlers/sessions.go | 10 +- internal/app/repository/user.go | 17 +- internal/app/server/server.gen.go | 14 +- internal/app/view/user.go | 22 +- 11 files changed, 952 insertions(+), 855 deletions(-) create mode 100644 internal/app/database/update0012_update0013.go create mode 100644 internal/app/database/update0013_update0013testdata.go diff --git a/api/openapi.yaml b/api/openapi.yaml index 3ecd644..487d284 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1,882 +1,888 @@ -openapi: 3.0.0 -info: - title: CTF Management API - description: 'API for managing CTF (Capture The Flag) games, teams, users, and services.' - contact: {} - version: 1.0.0 -servers: - - url: 'https://ctf01d.com' - description: Production server - - url: 'https://staging.ctf01d.com' - description: Staging server - - url: 'http://localhost:4102' - description: Local server -paths: - /api/v1/auth/signin: - post: - tags: - - Sessions - summary: Login user - description: >- - Authenticates a user by user_name and password, starts a new session, - and returns a session cookie. - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - user_name: - type: string - example: exampleUser - password: - type: string - format: password - example: examplePass - responses: - '200': - description: User logged in successfully. A session cookie is set. - headers: - Set-Cookie: - description: >- - Session cookie which needs to be included in subsequent - requests. - schema: - type: string - example: session_id=abc123; Path=/; Max-Age=345600; HttpOnly - content: - application/json: - schema: - type: object - properties: - data: - type: string - example: User logged in - '400': - description: Invalid request body - '401': - description: Invalid user_name or password - '500': - description: Internal Server Error - parameters: [] - /api/v1/auth/signout: - post: - tags: - - Sessions - summary: Logout user - description: >- - Authenticates a user by user_name and password, starts a new session, - and returns a session cookie. - responses: - '200': - description: User logout successfully. A session cookie is remove. - headers: - Set-Cookie: - description: >- - Session cookie which needs to be included in subsequent - requests. - schema: - type: string - example: session_id=abc123; Path=/; Max-Age=345600; HttpOnly - content: - application/json: - schema: - type: object - properties: - data: - type: string - example: User logged in - '400': - description: Invalid request body - '401': - description: Invalid user_name or password - '500': - description: Internal Server Error - parameters: [] - /api/v1/auth/session: - get: - summary: Validate current session and return user role - description: Check if the current session is valid and return the user's role. - operationId: validateSession - tags: - - Sessions - responses: - '200': - description: Session validation result + openapi: 3.0.0 + info: + title: CTF Management API + description: 'API for managing CTF (Capture The Flag) games, teams, users, and services.' + contact: {} + version: 1.0.0 + servers: + - url: 'https://ctf01d.com' + description: Production server + - url: 'https://staging.ctf01d.com' + description: Staging server + - url: 'http://localhost:4102' + description: Local server + paths: + /api/v1/auth/signin: + post: + tags: + - Sessions + summary: Sign in user + description: >- + Authenticates a user by user_name and password, starts a new session, + and returns a session cookie. + requestBody: + required: true content: application/json: schema: type: object properties: - valid: - type: boolean - description: Indicates if the current session is valid - role: + user_name: type: string - example: "admin" - description: The role of the current user - nullable: true - name: + example: exampleUser + password: type: string - example: "r00t" - description: The name of the current user - nullable: true - /api/v1/users: - get: - tags: - - Users - summary: List all users - operationId: listUsers - parameters: [] - responses: - '200': - description: A list of users + format: password + example: examplePass + responses: + '200': + description: User logged in successfully. A session cookie is set. + headers: + Set-Cookie: + description: >- + Session cookie which needs to be included in subsequent + requests. + schema: + type: string + example: session_id=abc123; Path=/; Max-Age=345600; HttpOnly + content: + application/json: + schema: + type: object + properties: + data: + type: string + example: User logged in + '400': + description: Invalid request body + '401': + description: Invalid user_name or password + '500': + description: Internal Server Error + parameters: [] + /api/v1/auth/signout: + post: + tags: + - Sessions + summary: Logout user + description: >- + Authenticates a user by user_name and password, starts a new session, + and returns a session cookie. + responses: + '200': + description: User logout successfully. A session cookie is remove. + headers: + Set-Cookie: + description: >- + Session cookie which needs to be included in subsequent + requests. + schema: + type: string + example: session_id=abc123; Path=/; Max-Age=345600; HttpOnly + content: + application/json: + schema: + type: object + properties: + data: + type: string + example: User logged in + '400': + description: Invalid request body + '401': + description: Invalid user_name or password + '500': + description: Internal Server Error + parameters: [] + /api/v1/auth/session: + get: + summary: Validate current session and return user role + description: Check if the current session is valid and return the user's role. + operationId: validateSession + tags: + - Sessions + responses: + '200': + description: Session validation result + content: + application/json: + schema: + type: object + properties: + valid: + type: boolean + description: Indicates if the current session is valid + role: + type: string + example: "admin" + description: The role of the current user + nullable: true + name: + type: string + example: "r00t" + description: The name of the current user + nullable: true + /api/v1/users: + get: + tags: + - Users + summary: List all users + operationId: listUsers + parameters: [] + responses: + '200': + description: A list of users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UserResponse' + x-content-type: application/json + '500': + description: Failed response + post: + tags: + - Users + summary: Create a new user + operationId: createUser + parameters: [] + requestBody: content: application/json: schema: - type: array - items: + $ref: '#/components/schemas/UserRequest' + required: true + responses: + '200': + description: User created successfully + '400': + description: Bad request + '500': + description: Failed response + '/api/v1/users/{id}': + get: + tags: + - Users + summary: Get a user by ID + operationId: getUserById + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: integer + responses: + '200': + description: Detailed information of a user + content: + application/json: + schema: $ref: '#/components/schemas/UserResponse' - x-content-type: application/json - '500': - description: Failed response - post: - tags: - - Users - summary: Create a new user - operationId: createUser - parameters: [] - requestBody: - content: - application/json: + '400': + description: Bad request + '500': + description: Failed response + put: + tags: + - Users + summary: Update a user + operationId: updateUser + parameters: + - name: id + in: path + required: true + style: simple + explode: false schema: - $ref: '#/components/schemas/UserRequest' - required: true - responses: - '200': - description: User created successfully - '400': - description: Bad request - '500': - description: Failed response - '/api/v1/users/{id}': - get: - tags: - - Users - summary: Get a user by ID - operationId: getUserById - parameters: - - name: id - in: path - required: true - style: simple - explode: false - schema: - type: integer - responses: - '200': - description: Detailed information of a user + type: integer + requestBody: content: application/json: schema: - $ref: '#/components/schemas/UserResponse' - '400': - description: Bad request - '500': - description: Failed response - put: - tags: - - Users - summary: Update a user - operationId: updateUser - parameters: - - name: id - in: path + $ref: '#/components/schemas/UserRequest' required: true - style: simple - explode: false - schema: - type: integer - requestBody: - content: - application/json: + responses: + '200': + description: User updated successfully + '400': + description: Bad request + '500': + description: Failed response + delete: + tags: + - Users + summary: Delete a user + operationId: deleteUser + parameters: + - name: id + in: path + required: true + style: simple + explode: false schema: - $ref: '#/components/schemas/UserRequest' - required: true - responses: - '200': - description: User updated successfully - '400': - description: Bad request - '500': - description: Failed response - delete: - tags: - - Users - summary: Delete a user - operationId: deleteUser - parameters: - - name: id - in: path - required: true - style: simple - explode: false - schema: - type: integer - responses: - '200': - description: User deleted successfully - '500': - description: Failed response - content: - application/json: - schema: {} - /api/v1/games: - get: - tags: - - Games - summary: List all games - operationId: listGames - parameters: [] - responses: - '200': - description: A list of games + type: integer + responses: + '200': + description: User deleted successfully + '500': + description: Failed response + content: + application/json: + schema: {} + /api/v1/games: + get: + tags: + - Games + summary: List all games + operationId: listGames + parameters: [] + responses: + '200': + description: A list of games + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/GameResponse' + x-content-type: application/json + '400': + description: Bad request + post: + tags: + - Games + summary: Create a new game + operationId: createGame + parameters: [] + requestBody: content: application/json: schema: - type: array - items: + $ref: '#/components/schemas/GameRequest' + required: true + responses: + '200': + description: Game created successfully + '400': + description: Bad request + '500': + description: Failed response + '/api/v1/games/{id}': + get: + tags: + - Games + summary: Get a game by ID + operationId: getGameById + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: integer + responses: + '200': + description: Detailed information of a game + content: + application/json: + schema: $ref: '#/components/schemas/GameResponse' - x-content-type: application/json - '400': - description: Bad request - post: - tags: - - Games - summary: Create a new game - operationId: createGame - parameters: [] - requestBody: - content: - application/json: + '400': + description: Bad request + '500': + description: Failed response + put: + tags: + - Games + summary: Update a game + operationId: updateGame + parameters: + - name: id + in: path + required: true + style: simple + explode: false schema: - $ref: '#/components/schemas/GameRequest' - required: true - responses: - '200': - description: Game created successfully - '400': - description: Bad request - '500': - description: Failed response - '/api/v1/games/{id}': - get: - tags: - - Games - summary: Get a game by ID - operationId: getGameById - parameters: - - name: id - in: path - required: true - style: simple - explode: false - schema: - type: integer - responses: - '200': - description: Detailed information of a game + type: integer + requestBody: content: application/json: schema: - $ref: '#/components/schemas/GameResponse' - '400': - description: Bad request - '500': - description: Failed response - put: - tags: - - Games - summary: Update a game - operationId: updateGame - parameters: - - name: id - in: path + $ref: '#/components/schemas/GameRequest' required: true - style: simple - explode: false - schema: - type: integer - requestBody: - content: - application/json: + responses: + '200': + description: Game updated successfully + '400': + description: Failed request + content: + application/json: + schema: {} + '500': + description: Failed response + content: + application/json: + schema: {} + delete: + tags: + - Games + summary: Delete a game + operationId: deleteGame + parameters: + - name: id + in: path + required: true + style: simple + explode: false schema: - $ref: '#/components/schemas/GameRequest' - required: true - responses: - '200': - description: Game updated successfully - '400': - description: Failed request - content: - application/json: - schema: {} - '500': - description: Failed response - content: - application/json: - schema: {} - delete: - tags: - - Games - summary: Delete a game - operationId: deleteGame - parameters: - - name: id - in: path - required: true - style: simple - explode: false - schema: - type: integer - responses: - '200': - description: Game deleted successfully - '400': - description: Bad request - '500': - description: Failed response - /api/v1/teams: - get: - tags: - - Teams - summary: List all teams - operationId: listTeams - parameters: [] - responses: - '200': - description: A list of teams + type: integer + responses: + '200': + description: Game deleted successfully + '400': + description: Bad request + '500': + description: Failed response + /api/v1/teams: + get: + tags: + - Teams + summary: List all teams + operationId: listTeams + parameters: [] + responses: + '200': + description: A list of teams + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TeamResponse' + x-content-type: application/json + '400': + description: Failed request + content: + application/json: + schema: {} + '500': + description: Failed response + content: + application/json: + schema: {} + post: + tags: + - Teams + summary: Create a new team + operationId: createTeam + parameters: [] + requestBody: content: application/json: schema: - type: array - items: + $ref: '#/components/schemas/TeamRequest' + required: true + responses: + '200': + description: Team created successfully + '400': + description: Failed request + content: + application/json: + schema: {} + '500': + description: Failed response + content: + application/json: + schema: {} + '/api/v1/teams/{id}': + get: + tags: + - Teams + summary: Get a team by ID + operationId: getTeamById + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: integer + responses: + '200': + description: Detailed information of a team + content: + application/json: + schema: $ref: '#/components/schemas/TeamResponse' - x-content-type: application/json - '400': - description: Failed request - content: - application/json: - schema: {} - '500': - description: Failed response - content: - application/json: - schema: {} - post: - tags: - - Teams - summary: Create a new team - operationId: createTeam - parameters: [] - requestBody: - content: - application/json: + put: + tags: + - Teams + summary: Update a team + operationId: updateTeam + parameters: + - name: id + in: path + required: true + style: simple + explode: false schema: - $ref: '#/components/schemas/TeamRequest' - required: true - responses: - '200': - description: Team created successfully - '400': - description: Failed request - content: - application/json: - schema: {} - '500': - description: Failed response - content: - application/json: - schema: {} - '/api/v1/teams/{id}': - get: - tags: - - Teams - summary: Get a team by ID - operationId: getTeamById - parameters: - - name: id - in: path - required: true - style: simple - explode: false - schema: - type: integer - responses: - '200': - description: Detailed information of a team + type: integer + requestBody: content: application/json: schema: - $ref: '#/components/schemas/TeamResponse' - put: - tags: - - Teams - summary: Update a team - operationId: updateTeam - parameters: - - name: id - in: path + $ref: '#/components/schemas/TeamRequest' required: true - style: simple - explode: false - schema: - type: integer - requestBody: - content: - application/json: + responses: + '200': + description: Team updated successfully + delete: + tags: + - Teams + summary: Delete a team + operationId: deleteTeam + parameters: + - name: id + in: path + required: true + style: simple + explode: false schema: - $ref: '#/components/schemas/TeamRequest' - required: true - responses: - '200': - description: Team updated successfully - delete: - tags: - - Teams - summary: Delete a team - operationId: deleteTeam - parameters: - - name: id - in: path - required: true - style: simple - explode: false - schema: - type: integer - responses: - '200': - description: Team deleted successfully - /api/v1/results: - get: - tags: - - Results - summary: List all results - operationId: listResults - parameters: [] - responses: - '200': - description: A list of game results + type: integer + responses: + '200': + description: Team deleted successfully + /api/v1/results: + get: + tags: + - Results + summary: List all results + operationId: listResults + parameters: [] + responses: + '200': + description: A list of game results + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ResultResponse' + x-content-type: application/json + post: + tags: + - Results + summary: Create a new result + operationId: createResult + parameters: [] + requestBody: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/ResultResponse' - x-content-type: application/json - post: - tags: - - Results - summary: Create a new result - operationId: createResult - parameters: [] - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ResultRequest' - required: true - responses: - '200': - description: Result created successfully - '/api/v1/results/{id}': - get: - tags: - - Results - summary: Get a result by ID - operationId: getResultById - parameters: - - name: id - in: path + $ref: '#/components/schemas/ResultRequest' required: true - style: simple - explode: false - schema: - type: integer - responses: - '200': - description: Detailed information of a result - content: - application/json: - schema: - $ref: '#/components/schemas/ResultResponse' - /api/v1/services: - get: - tags: - - Services - summary: List all services - operationId: listServices - parameters: [] - responses: - '200': - description: A list of services + responses: + '200': + description: Result created successfully + '/api/v1/results/{id}': + get: + tags: + - Results + summary: Get a result by ID + operationId: getResultById + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: integer + responses: + '200': + description: Detailed information of a result + content: + application/json: + schema: + $ref: '#/components/schemas/ResultResponse' + /api/v1/services: + get: + tags: + - Services + summary: List all services + operationId: listServices + parameters: [] + responses: + '200': + description: A list of services + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ServiceResponse' + x-content-type: application/json + post: + tags: + - Services + summary: Create a new service + operationId: createService + parameters: [] + requestBody: content: application/json: schema: - type: array - items: + $ref: '#/components/schemas/ServiceRequest' + required: true + responses: + '200': + description: Service created successfully + '/api/v1/services/{id}': + get: + tags: + - Services + summary: Get a service by ID + operationId: getServiceById + parameters: + - name: id + in: path + required: true + style: simple + explode: false + schema: + type: integer + responses: + '200': + description: Detailed information of a service + content: + application/json: + schema: $ref: '#/components/schemas/ServiceResponse' - x-content-type: application/json - post: - tags: - - Services - summary: Create a new service - operationId: createService - parameters: [] - requestBody: - content: - application/json: + put: + tags: + - Services + summary: Update a service + operationId: updateService + parameters: + - name: id + in: path + required: true + style: simple + explode: false schema: - $ref: '#/components/schemas/ServiceRequest' - required: true - responses: - '200': - description: Service created successfully - '/api/v1/services/{id}': - get: - tags: - - Services - summary: Get a service by ID - operationId: getServiceById - parameters: - - name: id - in: path - required: true - style: simple - explode: false - schema: - type: integer - responses: - '200': - description: Detailed information of a service + type: integer + requestBody: content: application/json: schema: - $ref: '#/components/schemas/ServiceResponse' - put: - tags: - - Services - summary: Update a service - operationId: updateService - parameters: - - name: id - in: path + $ref: '#/components/schemas/ServiceRequest' required: true - style: simple - explode: false - schema: - type: integer - requestBody: - content: - application/json: + responses: + '200': + description: Service updated successfully + delete: + tags: + - Services + summary: Delete a service + operationId: deleteService + parameters: + - name: id + in: path + required: true + style: simple + explode: false schema: - $ref: '#/components/schemas/ServiceRequest' - required: true - responses: - '200': - description: Service updated successfully - delete: - tags: - - Services - summary: Delete a service - operationId: deleteService - parameters: - - name: id - in: path - required: true - style: simple - explode: false - schema: + type: integer + responses: + '200': + description: Service deleted successfully + /api/v1/universities: + get: + summary: Retrieves a list of universities + description: > + This endpoint retrieves universities. It can optionally filter + universities that match a specific term. + tags: + - University + parameters: + - in: query + name: term + schema: + type: string + description: Optional search term to filter universities by name. + required: false + responses: + '200': + description: A JSON array of universities + content: + application/json: + schema: + $ref: '#/components/schemas/UniversitiesResponse' + components: + schemas: + UserRequest: + type: object + properties: + display_name: + type: string + description: The name of the user + user_name: + type: string + description: The login of the user + role: + type: string + enum: + - admin + - player + - guest + example: player + description: 'The role of the user (admin, player or guest)' + avatar_url: + type: string + description: URL to the user's avatar + status: + type: string + description: 'Status of the user (active, disabled)' + password: + type: string + description: User password + team_ids: + type: array + items: + type: integer + example: 1 + description: Unique identifier for the result entry + UserResponse: + type: object + properties: + id: type: integer - responses: - '200': - description: Service deleted successfully - /api/v1/universities: - get: - summary: Retrieves a list of universities - description: > - This endpoint retrieves universities. It can optionally filter - universities that match a specific term. - tags: - - University - parameters: - - in: query - name: term - schema: - type: string - description: Optional search term to filter universities by name. - required: false - responses: - '200': - description: A JSON array of universities - content: - application/json: - schema: - $ref: '#/components/schemas/UniversitiesResponse' -components: - schemas: - UserRequest: - type: object - properties: - user_name: - type: string - description: The name of the user - role: - type: string - enum: - - admin - - player - - guest - example: player - description: 'The role of the user (admin, player or guest)' - avatar_url: - type: string - description: URL to the user's avatar - status: - type: string - description: 'Status of the user (active, disabled)' - password: - type: string - description: User password - team_ids: - type: array - items: + description: The unique identifier for the user + display_name: + type: string + description: The name of the user + user_name: + type: string + description: The login of the user + role: + type: string + enum: + - admin + - player + - guest + example: player + description: 'The role of the user (admin, player or guest)' + avatar_url: + type: string + description: URL to the user's avatar + status: + type: string + description: 'Status of the user (active, disabled)' + GameRequest: + required: + - end_time + - start_time + type: object + properties: + start_time: + type: string + description: The start time of the game + format: date-time + example: '2000-01-23T04:56:07.000Z' + end_time: + type: string + description: The end time of the game + format: date-time + example: '2000-01-24T04:56:07.000Z' + description: + type: string + description: A brief description of the game + GameResponse: + required: + - end_time + - id + - start_time + type: object + properties: + id: + type: integer + description: Unique identifier for the game + start_time: + type: string + description: The start time of the game + format: date-time + example: '2000-01-23T04:56:07.000Z' + end_time: + type: string + description: The end time of the game + format: date-time + example: '2000-01-24T04:56:07.000Z' + description: + type: string + description: A brief description of the game + ResultRequest: + required: + - game_id + - rank + - score + - team_id + type: object + properties: + team_id: + type: string + description: Identifier of the team this result belongs to + game_id: + type: string + description: Identifier of the game this result is for + score: + type: integer + description: The score achieved by the team + rank: + type: integer + description: The rank achieved by the team in this game + ResultResponse: + required: + - id + - game_id + - rank + - score + - team_id + type: object + properties: + id: type: integer - example: 1 description: Unique identifier for the result entry - UserResponse: - type: object - properties: - id: - type: integer - description: The unique identifier for the user - user_name: - type: string - description: The name of the user - role: - type: string - enum: - - admin - - player - - guest - example: player - description: 'The role of the user (admin, player or guest)' - avatar_url: - type: string - description: URL to the user's avatar - status: - type: string - description: 'Status of the user (active, disabled)' - GameRequest: - required: - - end_time - - start_time - type: object - properties: - start_time: - type: string - description: The start time of the game - format: date-time - example: '2000-01-23T04:56:07.000Z' - end_time: - type: string - description: The end time of the game - format: date-time - example: '2000-01-24T04:56:07.000Z' - description: - type: string - description: A brief description of the game - GameResponse: - required: - - end_time - - id - - start_time - type: object - properties: - id: - type: integer - description: Unique identifier for the game - start_time: - type: string - description: The start time of the game - format: date-time - example: '2000-01-23T04:56:07.000Z' - end_time: - type: string - description: The end time of the game - format: date-time - example: '2000-01-24T04:56:07.000Z' - description: - type: string - description: A brief description of the game - ResultRequest: - required: - - game_id - - rank - - score - - team_id - type: object - properties: - team_id: - type: string - description: Identifier of the team this result belongs to - game_id: - type: string - description: Identifier of the game this result is for - score: - type: integer - description: The score achieved by the team - rank: - type: integer - description: The rank achieved by the team in this game - ResultResponse: - required: - - id - - game_id - - rank - - score - - team_id - type: object - properties: - id: - type: integer - description: Unique identifier for the result entry - team_id: - type: string - description: Identifier of the team this result belongs to - game_id: - type: string - description: Identifier of the game this result is for - score: - type: integer - description: The score achieved by the team - rank: - type: integer - description: The rank achieved by the team in this game - ServiceRequest: - required: - - author - - is_public - - name - type: object - properties: - name: - type: string - description: Name of the service - author: - type: string - description: Author of the service - logo_url: - type: string - description: URL to the logo of the service - description: - type: string - description: A brief description of the service - is_public: - type: boolean - description: Boolean indicating if the service is public - ServiceResponse: - required: - - author - - id - - is_public - - name - type: object - properties: - id: - type: integer - description: Unique identifier for the service - name: - type: string - description: Name of the service - author: - type: string - description: Author of the service - logo_url: - type: string - description: URL to the logo of the service - description: - type: string - description: A brief description of the service - is_public: - type: boolean - description: Boolean indicating if the service is public - TeamRequest: - required: - - name - - university_id - type: object - properties: - name: - type: string - description: Name of the team - description: - type: string - description: A brief description of the team - university_id: - type: integer - description: University or institution the team is associated with - social_links: - type: string - description: JSON string containing social media links of the team - avatar_url: - type: string - description: URL to the team's avatar - TeamResponse: - required: - - id - - name - type: object - properties: - id: - type: integer - description: Unique identifier for the team - name: - type: string - description: Name of the team - description: - type: string - description: A brief description of the team - university: - type: string - description: University or institution the team is associated with - social_links: - type: string - description: JSON string containing social media links of the team - avatar_url: - type: string - description: URL to the team's avatar - UniversityResponse: - type: object - required: - - id - - name - properties: - id: - type: integer - description: The unique identifier of the university - example: 1 - name: - type: string - description: The name of the university - example: >- - Анапский филиал Кубанского государственного аграрного - университета - UniversitiesResponse: - type: array - items: - $ref: '#/components/schemas/UniversityResponse' - links: {} - callbacks: {} -security: [] + team_id: + type: string + description: Identifier of the team this result belongs to + game_id: + type: string + description: Identifier of the game this result is for + score: + type: integer + description: The score achieved by the team + rank: + type: integer + description: The rank achieved by the team in this game + ServiceRequest: + required: + - author + - is_public + - name + type: object + properties: + name: + type: string + description: Name of the service + author: + type: string + description: Author of the service + logo_url: + type: string + description: URL to the logo of the service + description: + type: string + description: A brief description of the service + is_public: + type: boolean + description: Boolean indicating if the service is public + ServiceResponse: + required: + - author + - id + - is_public + - name + type: object + properties: + id: + type: integer + description: Unique identifier for the service + name: + type: string + description: Name of the service + author: + type: string + description: Author of the service + logo_url: + type: string + description: URL to the logo of the service + description: + type: string + description: A brief description of the service + is_public: + type: boolean + description: Boolean indicating if the service is public + TeamRequest: + required: + - name + - university_id + type: object + properties: + name: + type: string + description: Name of the team + description: + type: string + description: A brief description of the team + university_id: + type: integer + description: University or institution the team is associated with + social_links: + type: string + description: JSON string containing social media links of the team + avatar_url: + type: string + description: URL to the team's avatar + TeamResponse: + required: + - id + - name + type: object + properties: + id: + type: integer + description: Unique identifier for the team + name: + type: string + description: Name of the team + description: + type: string + description: A brief description of the team + university: + type: string + description: University or institution the team is associated with + social_links: + type: string + description: JSON string containing social media links of the team + avatar_url: + type: string + description: URL to the team's avatar + UniversityResponse: + type: object + required: + - id + - name + properties: + id: + type: integer + description: The unique identifier of the university + example: 1 + name: + type: string + description: The name of the university + example: >- + Анапский филиал Кубанского государственного аграрного + университета + UniversitiesResponse: + type: array + items: + $ref: '#/components/schemas/UniversityResponse' + links: {} + callbacks: {} + security: [] diff --git a/go.mod b/go.mod index 6fd67fb..9a3de61 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/go-chi/chi v1.5.5 // indirect github.com/google/uuid v1.5.0 // indirect + github.com/jaswdr/faker/v2 v2.3.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/stretchr/testify v1.9.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 3a7e6f5..473d7a5 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/jaswdr/faker/v2 v2.3.0 h1:jgQ9UmU2Eb5tSQ8JkUS4tPoyTM2OtThQpOpwk7Fa9RY= +github.com/jaswdr/faker/v2 v2.3.0/go.mod h1:ROK8xwQV0hYOLDUtxCQgHGcl10jbVzIvqHxcIDdwY2Q= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= diff --git a/internal/app/database/struct_updater.go b/internal/app/database/struct_updater.go index 06a7cd6..65cde86 100644 --- a/internal/app/database/struct_updater.go +++ b/internal/app/database/struct_updater.go @@ -35,6 +35,8 @@ func RegisterAllUpdates() map[string][]DatabaseUpdateFunc { allUpdates = RegisterDatabaseUpdate(allUpdates, DatabaseUpdate_update0011_update0011testdata) allUpdates = RegisterDatabaseUpdate(allUpdates, DatabaseUpdate_update0011_update0012) allUpdates = RegisterDatabaseUpdate(allUpdates, DatabaseUpdate_update0012_update0012testdata) + allUpdates = RegisterDatabaseUpdate(allUpdates, DatabaseUpdate_update0012_update0013) + allUpdates = RegisterDatabaseUpdate(allUpdates, DatabaseUpdate_update0013_update0013testdata) return allUpdates } diff --git a/internal/app/database/update0012_update0013.go b/internal/app/database/update0012_update0013.go new file mode 100644 index 0000000..f38682d --- /dev/null +++ b/internal/app/database/update0012_update0013.go @@ -0,0 +1,30 @@ +package database + +import ( + "database/sql" + "log/slog" + "runtime" +) + +func DatabaseUpdate_update0012_update0013(db *sql.DB, getInfo bool) (string, string, string, error) { + + // WARNING!!! + // Do not change the update if it has already been installed by other developers or in production. + // To correct the database, create a new update and register it in the list of updates. + + fromUpdateId, toUpdateId := ParseNameFuncUpdate(runtime.Caller(0)) + description := "Added column display_name to users" + if getInfo { + return fromUpdateId, toUpdateId, description, nil + } + query := ` + ALTER TABLE users + ADD COLUMN display_name varchar(255); + ` + _, err := db.Exec(query) + if err != nil { + slog.Error("Problem with update, query: " + query + "\n error:" + err.Error()) + return fromUpdateId, toUpdateId, description, err + } + return fromUpdateId, toUpdateId, description, nil +} diff --git a/internal/app/database/update0013_update0013testdata.go b/internal/app/database/update0013_update0013testdata.go new file mode 100644 index 0000000..6f26f02 --- /dev/null +++ b/internal/app/database/update0013_update0013testdata.go @@ -0,0 +1,46 @@ +package database + +import ( + "database/sql" + "fmt" + "log" + "log/slog" + "runtime" + + "github.com/jaswdr/faker/v2" +) + +func DatabaseUpdate_update0013_update0013testdata(db *sql.DB, getInfo bool) (string, string, string, error) { + + // WARNING!!! + // Do not change the update if it has already been installed by other developers or in production. + // To correct the database, create a new update and register it in the list of updates. + + fromUpdateId, toUpdateId := ParseNameFuncUpdate(runtime.Caller(0)) + description := "Insert test data to game_services" + if getInfo { + return fromUpdateId, toUpdateId, description, nil + } + query := ` + SELECT id from users + ` + rows, err := db.Query(query) + if err != nil { + slog.Error("Problem with select, query: " + query + "\n error:" + err.Error()) + return fromUpdateId, toUpdateId, description, err + } + fake := faker.New() + for rows.Next() { + var id int + if err := rows.Scan(&id); err != nil { + log.Fatalf("Error scanning row: %v", err) + } + query := fmt.Sprintf("UPDATE users SET display_name = '%s' WHERE id = %d", fake.Person().Name(), id) + _, err := db.Exec(query) + if err != nil { + slog.Error("Problem with update, query: " + query + "\n error:" + err.Error()) + return fromUpdateId, toUpdateId, description, err + } + } + return fromUpdateId, toUpdateId, description, nil +} diff --git a/internal/app/db/user.go b/internal/app/db/user.go index b95c661..9ef4aff 100644 --- a/internal/app/db/user.go +++ b/internal/app/db/user.go @@ -4,6 +4,7 @@ import "ctf01d/internal/app/server" type User struct { Id int `db:"id"` + DisplayName string `db:"display_name"` Username string `db:"user_name"` Role server.UserRequestRole `db:"role"` AvatarUrl string `db:"avatar_url"` diff --git a/internal/app/handlers/sessions.go b/internal/app/handlers/sessions.go index 3c0301d..903cd43 100644 --- a/internal/app/handlers/sessions.go +++ b/internal/app/handlers/sessions.go @@ -13,14 +13,14 @@ import ( func (h *Handlers) PostApiV1AuthSignin(w http.ResponseWriter, r *http.Request) { var req server.PostApiV1AuthSigninJSONBody if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - slog.Warn(err.Error(), "handler", "LoginSessionHandler") + slog.Warn(err.Error(), "handler", "PostApiV1AuthSignin") http.Error(w, "Invalid request body", http.StatusBadRequest) return } userRepo := repository.NewUserRepository(h.DB) user, err := userRepo.GetByUserName(r.Context(), *req.UserName) if err != nil || !api_helpers.CheckPasswordHash(*req.Password, user.PasswordHash) { - slog.Warn(err.Error(), "handler", "LoginSessionHandler") + slog.Warn(err.Error(), "handler", "PostApiV1AuthSignin") api_helpers.RespondWithJSON(w, http.StatusUnauthorized, map[string]string{"error": "Invalid password or user"}) return } @@ -28,7 +28,7 @@ func (h *Handlers) PostApiV1AuthSignin(w http.ResponseWriter, r *http.Request) { repo := repository.NewSessionRepository(h.DB) sessionId, err := repo.StoreSessionInDB(r.Context(), user.Id) if err != nil { - slog.Warn(err.Error(), "handler", "LoginSessionHandler") + slog.Warn(err.Error(), "handler", "PostApiV1AuthSignin") api_helpers.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to store session"}) return } @@ -47,14 +47,14 @@ func (h *Handlers) PostApiV1AuthSignin(w http.ResponseWriter, r *http.Request) { func (h *Handlers) PostApiV1AuthSignout(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session_id") if err != nil { - slog.Warn(err.Error(), "handler", "LogoutSessionHandler") + slog.Warn(err.Error(), "handler", "PostApiV1AuthSignout") api_helpers.RespondWithJSON(w, http.StatusUnauthorized, map[string]string{"error": "No session found"}) return } repo := repository.NewSessionRepository(h.DB) err = repo.DeleteSessionInDB(r.Context(), cookie.Value) if err != nil { - slog.Warn(err.Error(), "handler", "LogoutSessionHandler") + slog.Warn(err.Error(), "handler", "PostApiV1AuthSignout") api_helpers.RespondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to delete session"}) return } diff --git a/internal/app/repository/user.go b/internal/app/repository/user.go index ba747fc..c9e3374 100644 --- a/internal/app/repository/user.go +++ b/internal/app/repository/user.go @@ -25,11 +25,12 @@ func NewUserRepository(db *sql.DB) UserRepository { } func (r *userRepo) Create(ctx context.Context, user *models.User) error { - query := `INSERT INTO users (user_name, avatar_url, role, status, password_hash) VALUES ($1, $2, $3, $4, $5) RETURNING id` - err := r.db.QueryRowContext(ctx, query, user.Username, user.AvatarUrl, user.Role, user.Status, user.PasswordHash).Scan(&user.Id) + query := `INSERT INTO users (display_name, user_name, avatar_url, role, status, password_hash) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id` + err := r.db.QueryRowContext(ctx, query, user.DisplayName, user.Username, user.AvatarUrl, user.Role, user.Status, user.PasswordHash).Scan(&user.Id) if err != nil { return err } + return nil } @@ -45,9 +46,9 @@ func (r *userRepo) AddUserToTeams(ctx context.Context, userId int, teamIds *[]in } func (r *userRepo) GetById(ctx context.Context, id int) (*models.User, error) { - query := `SELECT id, user_name, avatar_url, role, status FROM users WHERE id = $1` + query := `SELECT id, display_name, user_name, avatar_url, role, status FROM users WHERE id = $1` user := &models.User{} - err := r.db.QueryRowContext(ctx, query, id).Scan(&user.Id, &user.Username, &user.AvatarUrl, &user.Role, &user.Status) + err := r.db.QueryRowContext(ctx, query, id).Scan(&user.Id, &user.DisplayName, &user.Username, &user.AvatarUrl, &user.Role, &user.Status) if err != nil { return nil, err } @@ -65,8 +66,8 @@ func (r *userRepo) GetByUserName(ctx context.Context, name string) (*models.User } func (r *userRepo) Update(ctx context.Context, user *models.User) error { - query := `UPDATE users SET user_name = $1, avatar_url = $2, role = $3, status = $4, password_hash = $5 WHERE id = $6` - _, err := r.db.ExecContext(ctx, query, user.Username, user.AvatarUrl, user.Role, user.Status, user.PasswordHash, user.Id) + query := `UPDATE users SET user_name = $1, avatar_url = $2, role = $3, status = $4, password_hash = $5, login = $6 WHERE id = $7` + _, err := r.db.ExecContext(ctx, query, user.Username, user.AvatarUrl, user.Role, user.Status, user.PasswordHash, user.DisplayName, user.Id) return err } @@ -93,7 +94,7 @@ func (r *userRepo) Delete(ctx context.Context, id int) error { } func (r *userRepo) List(ctx context.Context) ([]*models.User, error) { - query := `SELECT id, user_name, avatar_url, role, status FROM users` + query := `SELECT id, display_name, user_name, avatar_url, role, status FROM users` rows, err := r.db.QueryContext(ctx, query) if err != nil { return nil, err @@ -103,7 +104,7 @@ func (r *userRepo) List(ctx context.Context) ([]*models.User, error) { var users []*models.User for rows.Next() { var user models.User - if err := rows.Scan(&user.Id, &user.Username, &user.AvatarUrl, &user.Role, &user.Status); err != nil { + if err := rows.Scan(&user.Id, &user.DisplayName, &user.Username, &user.AvatarUrl, &user.Role, &user.Status); err != nil { return nil, err } users = append(users, &user) diff --git a/internal/app/server/server.gen.go b/internal/app/server/server.gen.go index 8b3e389..083669e 100644 --- a/internal/app/server/server.gen.go +++ b/internal/app/server/server.gen.go @@ -181,6 +181,9 @@ type UserRequest struct { // AvatarUrl URL to the user's avatar AvatarUrl *string `json:"avatar_url,omitempty"` + // DisplayName The name of the user + DisplayName *string `json:"display_name,omitempty"` + // Password User password Password *string `json:"password,omitempty"` @@ -191,7 +194,7 @@ type UserRequest struct { Status *string `json:"status,omitempty"` TeamIds *[]int `json:"team_ids,omitempty"` - // UserName The name of the user + // UserName The login of the user UserName *string `json:"user_name,omitempty"` } @@ -203,6 +206,9 @@ type UserResponse struct { // AvatarUrl URL to the user's avatar AvatarUrl *string `json:"avatar_url,omitempty"` + // DisplayName The name of the user + DisplayName *string `json:"display_name,omitempty"` + // Id The unique identifier for the user Id *int `json:"id,omitempty"` @@ -212,7 +218,7 @@ type UserResponse struct { // Status Status of the user (active, disabled) Status *string `json:"status,omitempty"` - // UserName The name of the user + // UserName The login of the user UserName *string `json:"user_name,omitempty"` } @@ -266,7 +272,7 @@ type ServerInterface interface { // Validate current session and return user role // (GET /api/v1/auth/session) ValidateSession(w http.ResponseWriter, r *http.Request) - // Login user + // Sign in user // (POST /api/v1/auth/signin) PostApiV1AuthSignin(w http.ResponseWriter, r *http.Request) // Logout user @@ -356,7 +362,7 @@ func (_ Unimplemented) ValidateSession(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } -// Login user +// Sign in user // (POST /api/v1/auth/signin) func (_ Unimplemented) PostApiV1AuthSignin(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) diff --git a/internal/app/view/user.go b/internal/app/view/user.go index 713b788..fd7cb10 100644 --- a/internal/app/view/user.go +++ b/internal/app/view/user.go @@ -8,21 +8,23 @@ import ( ) type User struct { - Id int `json:"id"` - Username string `json:"user_name"` - Role string `json:"role,omitempty"` - AvatarUrl string `json:"avatar_url,omitempty"` - Status string `json:"status,omitempty"` + Id int `json:"id"` + DisplayName string `json:"display_name"` + Username string `json:"user_name"` + Role string `json:"role,omitempty"` + AvatarUrl string `json:"avatar_url,omitempty"` + Status string `json:"status,omitempty"` } func NewUserFromModel(u *db.User) *server.UserResponse { userRole := helpers.ConvertUserRequestRoleToUserResponseRole(u.Role) return &server.UserResponse{ - Id: &u.Id, - UserName: &u.Username, - Role: &userRole, - AvatarUrl: &u.AvatarUrl, - Status: &u.Status, + Id: &u.Id, + UserName: &u.Username, + DisplayName: &u.DisplayName, + Role: &userRole, + AvatarUrl: &u.AvatarUrl, + Status: &u.Status, } }