diff --git a/.forgejo/workflows/build-and-deploy.yml b/.forgejo/workflows/build-and-deploy.yml index b82fb3d..e7d0a83 100644 --- a/.forgejo/workflows/build-and-deploy.yml +++ b/.forgejo/workflows/build-and-deploy.yml @@ -18,10 +18,14 @@ jobs: - uses: actions/setup-go@v6 with: go-version: '^1.25' + check-latest: false + cache-dependency-path: | + modules/backend/go.sum - - name: Build backend + - name: Build Go app run: | cd modules/backend + go mod tidy go build -o nyanimedb . tar -czvf nyanimedb-backend.tar.gz nyanimedb @@ -31,18 +35,6 @@ jobs: name: nyanimedb-backend.tar.gz path: modules/backend/nyanimedb-backend.tar.gz - - name: Build auth - run: | - cd modules/auth - go build -o auth . - tar -czvf nyanimedb-auth.tar.gz auth - - - name: Upload built auth to artifactory - uses: actions/upload-artifact@v3 - with: - name: nyanimedb-auth.tar.gz - path: modules/auth/nyanimedb-auth.tar.gz - # Build frontend - uses: actions/setup-node@v5 with: @@ -84,14 +76,6 @@ jobs: push: true tags: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest - - name: Build and push auth image - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfiles/Dockerfile_auth - push: true - tags: meowgit.nekoea.red/nihonium/nyanimedb-auth:latest - - name: Build and push frontend image uses: docker/build-push-action@v6 with: @@ -101,7 +85,7 @@ jobs: tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest deploy: - runs-on: debian-test + runs-on: self-hosted needs: build env: POSTGRES_USER: ${{ secrets.POSTGRES_USER }} @@ -111,12 +95,6 @@ jobs: POSTGRES_VERSION: 18 LOG_LEVEL: ${{ vars.LOG_LEVEL }} DATABASE_URL: ${{ secrets.DATABASE_URL }} - SERVICE_ADDRESS: ${{ vars.SERVICE_ADDRESS }} - RABBITMQ_URL: ${{ secrets.RABBITMQ_URL }} - JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }} - RABBITMQ_DEFAULT_USER: ${{ secrets.RABBITMQ_USER }} - RABBITMQ_DEFAULT_PASS: ${{ secrets.RABBITMQ_PASSWORD }} - AUTH_ENABLED: ${{ vars.AUTH_ENABLED }} steps: - name: Checkout code diff --git a/api/_build/oapi-codegen.yaml b/api/_build/oapi-codegen.yaml deleted file mode 100644 index 32e029a..0000000 --- a/api/_build/oapi-codegen.yaml +++ /dev/null @@ -1,6 +0,0 @@ -package: oapi -generate: - strict-server: true - gin-server: true - models: true -output: api/api.gen.go \ No newline at end of file diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml deleted file mode 100644 index ad0c9be..0000000 --- a/api/_build/openapi.yaml +++ /dev/null @@ -1,844 +0,0 @@ -openapi: 3.0.4 -info: - title: 'Titles, Users, Reviews, Tags, and Media API' - version: 1.0.0 -servers: - - url: /api/v1 -paths: - /titles: - get: - summary: Get titles - parameters: - - $ref: '#/components/parameters/cursor' - - $ref: '#/components/parameters/title_sort' - - name: sort_forward - in: query - schema: - type: boolean - default: true - - name: ext_search - in: query - schema: - type: boolean - default: false - - name: word - in: query - schema: - type: string - - name: status - in: query - description: List of title statuses to filter - schema: - type: array - items: - $ref: '#/components/schemas/TitleStatus' - explode: false - style: form - - name: rating - in: query - schema: - type: number - format: double - - name: release_year - in: query - schema: - type: integer - format: int32 - - name: release_season - in: query - schema: - $ref: '#/components/schemas/ReleaseSeason' - - name: limit - in: query - schema: - type: integer - format: int32 - default: 10 - - name: offset - in: query - schema: - type: integer - format: int32 - default: 0 - - name: fields - in: query - schema: - type: string - default: all - responses: - '200': - description: List of titles with cursor - content: - application/json: - schema: - type: object - properties: - data: - description: List of titles - type: array - items: - $ref: '#/components/schemas/Title' - cursor: - $ref: '#/components/schemas/CursorObj' - required: - - data - - cursor - '204': - description: No titles found - '400': - description: Request params are not correct - '500': - description: Unknown server error - '/titles/{title_id}': - get: - operationId: getTitle - summary: Get title description - parameters: - - name: title_id - in: path - required: true - schema: - type: integer - format: int64 - - name: fields - in: query - schema: - type: string - default: all - responses: - '200': - description: Title description - content: - application/json: - schema: - $ref: '#/components/schemas/Title' - '204': - description: No title found - '400': - description: Request params are not correct - '404': - description: Title not found - '500': - description: Unknown server error - security: - - JwtAuthCookies: [] - /users/: - get: - summary: 'Search user by nickname or dispname (both in one param), response is always sorted by id' - parameters: - - name: word - in: query - schema: - type: string - - name: limit - in: query - schema: - type: integer - format: int32 - default: 10 - - name: cursor_id - in: query - description: pass cursor naked - schema: - type: integer - format: int32 - default: 1 - responses: - '200': - description: List of users with cursor - content: - application/json: - schema: - type: object - properties: - data: - description: List of users - type: array - items: - $ref: '#/components/schemas/User' - cursor: - type: integer - format: int64 - default: 1 - required: - - data - - cursor - '204': - description: No users found - '400': - description: Request params are not correct - '500': - description: Unknown server error - '/users/{user_id}': - get: - operationId: getUsersId - summary: Get user info - parameters: - - name: user_id - in: path - required: true - schema: - type: string - - name: fields - in: query - schema: - type: string - default: all - responses: - '200': - description: User info - content: - application/json: - schema: - $ref: '#/components/schemas/User' - '400': - description: Request params are not correct - '404': - description: User not found - '500': - description: Unknown server error - patch: - operationId: updateUser - summary: Partially update a user account - description: | - Update selected user profile fields (excluding password). - Password updates must be done via the dedicated auth-service (`/auth/`). - Fields not provided in the request body remain unchanged. - parameters: - - name: user_id - in: path - description: User ID (primary key) - required: true - schema: - type: integer - format: int64 - example: 123 - requestBody: - required: true - content: - application/json: - schema: - description: Only provided fields are updated. Omitted fields remain unchanged. - type: object - properties: - avatar_id: - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar - type: integer - format: int64 - example: 42 - nullable: true - mail: - description: User email (must be unique and valid) - type: string - format: email - example: john.doe.updated@example.com - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - nickname: - description: 'Username (alphanumeric + `_` or `-`, 3–16 chars)' - type: string - example: john_doe_43 - maxLength: 16 - minLength: 3 - pattern: '^[a-zA-Z0-9_-]{3,16}$' - disp_name: - description: Display name - type: string - example: John Smith - maxLength: 32 - user_desc: - description: User description / bio - type: string - example: Just a curious developer. - maxLength: 512 - additionalProperties: false - responses: - '200': - description: User updated successfully. Returns updated user representation (excluding sensitive fields). - content: - application/json: - schema: - $ref: '#/components/schemas/User' - '400': - description: 'Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON)' - '401': - description: Unauthorized — missing or invalid authentication token - '403': - description: 'Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights)' - '404': - description: User not found - '409': - description: 'Conflict — e.g., requested `nickname` or `mail` already taken by another user' - '422': - description: 'Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`)' - '500': - description: Unknown server error - security: - - XsrfAuthHeader: [] - '/users/{user_id}/titles': - get: - operationId: getUserTitles - summary: Get user titles - parameters: - - $ref: '#/components/parameters/cursor' - - $ref: '#/components/parameters/title_sort' - - name: user_id - in: path - required: true - schema: - type: string - - name: sort_forward - in: query - schema: - type: boolean - default: true - - name: word - in: query - schema: - type: string - - name: status - in: query - description: List of title statuses to filter - schema: - type: array - items: - $ref: '#/components/schemas/TitleStatus' - explode: false - style: form - - name: watch_status - in: query - schema: - type: array - items: - $ref: '#/components/schemas/UserTitleStatus' - explode: false - style: form - - name: rating - in: query - schema: - type: number - format: double - - name: my_rate - in: query - schema: - type: integer - format: int32 - - name: release_year - in: query - schema: - type: integer - format: int32 - - name: release_season - in: query - schema: - $ref: '#/components/schemas/ReleaseSeason' - - name: limit - in: query - schema: - type: integer - format: int32 - default: 10 - - name: fields - in: query - schema: - type: string - default: all - responses: - '200': - description: List of user titles - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/UserTitle' - cursor: - $ref: '#/components/schemas/CursorObj' - required: - - data - - cursor - '204': - description: No titles found - '400': - description: Request params are not correct - '404': - description: User not found - '500': - description: Unknown server error - post: - operationId: addUserTitle - summary: Add a title to a user - description: 'User adding title to list af watched, status required' - parameters: - - name: user_id - in: path - description: ID of the user to assign the title to - required: true - schema: - type: integer - format: int64 - example: 123 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - title_id: - type: integer - format: int64 - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - ftime: - type: string - format: date-time - required: - - title_id - - status - responses: - '200': - description: Title successfully added to user - content: - application/json: - schema: - $ref: '#/components/schemas/UserTitleMini' - '400': - description: 'Invalid request body (missing fields, invalid types, etc.)' - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to assign titles to this user - '404': - description: User or Title not found - '409': - description: Conflict — title already assigned to user (if applicable) - '500': - description: Internal server error - '/users/{user_id}/titles/{title_id}': - get: - operationId: getUserTitle - summary: Get user title - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - - name: title_id - in: path - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: User titles - content: - application/json: - schema: - $ref: '#/components/schemas/UserTitleMini' - '204': - description: No user title found - '400': - description: Request params are not correct - '404': - description: User or title not found - '500': - description: Unknown server error - patch: - operationId: updateUserTitle - summary: Update a usertitle - description: User updating title list of watched - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - - name: title_id - in: path - required: true - schema: - type: integer - format: int64 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - ftime: - type: string - format: date-time - responses: - '200': - description: Title successfully updated - content: - application/json: - schema: - $ref: '#/components/schemas/UserTitleMini' - '400': - description: 'Invalid request body (missing fields, invalid types, etc.)' - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to update title - '404': - description: User or Title not found - '500': - description: Internal server error - security: - - XsrfAuthHeader: [] - delete: - operationId: deleteUserTitle - summary: Delete a usertitle - description: User deleting title from list of watched - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - - name: title_id - in: path - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: Title successfully deleted - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to delete title - '404': - description: User or Title not found - '500': - description: Internal server error - security: - - XsrfAuthHeader: [] - /media/upload: - post: - summary: 'Upload an image (PNG, JPEG, or WebP)' - description: | - Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**. - requestBody: - required: true - content: - encoding: - image: - contentType: 'image/png, image/jpeg, image/webp' - multipart/form-data: - schema: - image: - type: string - format: binary - description: 'Image file (PNG, JPEG, or WebP)' - responses: - '200': - description: Image uploaded successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Image' - '400': - description: 'Bad request — e.g., invalid/malformed image, empty file' - content: - application/json: - schema: - type: string - '415': - description: | - Unsupported Media Type — e.g., request `Content-Type` is not `multipart/form-data`, - or the `image` part has an unsupported `Content-Type` (not image/png, image/jpeg, or image/webp) - '500': - description: Internal server error -components: - parameters: - cursor: - in: query - name: cursor - required: false - schema: - type: string - title_sort: - in: query - name: sort - required: false - schema: - $ref: '#/components/schemas/TitleSort' - schemas: - TitleSort: - description: Title sort order - type: string - default: id - enum: - - id - - year - - rating - - views - TitleStatus: - description: Title status - type: string - enum: - - finished - - ongoing - - planned - ReleaseSeason: - description: Title release season - type: string - enum: - - winter - - spring - - summer - - fall - StorageType: - description: Image storage type - type: string - enum: - - s3 - - local - Image: - type: object - properties: - id: - type: integer - format: int64 - storage_type: - $ref: '#/components/schemas/StorageType' - image_path: - type: string - Studio: - type: object - properties: - id: - type: integer - format: int64 - name: - type: string - poster: - $ref: '#/components/schemas/Image' - description: - type: string - required: - - id - - name - Tag: - description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names' - type: object - example: - en: Shojo - ru: Сёдзё - ja: 少女 - additionalProperties: - type: string - Tags: - description: Array of localized tags - type: array - items: - $ref: '#/components/schemas/Tag' - example: - - en: Shojo - ru: Сёдзё - ja: 少女 - - en: Shounen - ru: Сёнен - ja: 少年 - Title: - type: object - properties: - id: - description: Unique title ID (primary key) - type: integer - format: int64 - example: 1 - title_names: - description: 'Localized titles. Key = language (ISO 639-1), value = list of names' - type: object - example: - en: - - Attack on Titan - - AoT - ru: - - Атака титанов - - Титаны - ja: - - 進撃の巨人 - additionalProperties: - type: array - items: - type: string - example: Attack on Titan - minItems: 1 - example: - - Attack on Titan - - AoT - title_desc: - description: 'Localized description. Key = language (ISO 639-1), value = description.' - type: object - additionalProperties: - type: string - studio: - $ref: '#/components/schemas/Studio' - tags: - $ref: '#/components/schemas/Tags' - poster: - $ref: '#/components/schemas/Image' - title_status: - $ref: '#/components/schemas/TitleStatus' - rating: - type: number - format: double - rating_count: - type: integer - format: int32 - release_year: - type: integer - format: int32 - release_season: - $ref: '#/components/schemas/ReleaseSeason' - episodes_aired: - type: integer - format: int32 - episodes_all: - type: integer - format: int32 - episodes_len: - type: object - additionalProperties: - type: number - format: double - required: - - id - - title_names - - tags - CursorObj: - type: object - properties: - id: - type: integer - format: int64 - param: - type: string - required: - - id - User: - type: object - properties: - id: - description: Unique user ID (primary key) - type: integer - format: int64 - example: 1 - image: - $ref: '#/components/schemas/Image' - mail: - description: User email - type: string - format: email - example: john.doe@example.com - nickname: - description: Username (alphanumeric + _ or -) - type: string - example: john_doe_42 - maxLength: 16 - disp_name: - description: Display name - type: string - example: John Doe - maxLength: 32 - user_desc: - description: User description - type: string - example: Just a regular user. - maxLength: 512 - creation_date: - description: Timestamp when the user was created - type: string - format: date-time - example: '2025-10-10T23:45:47.908073Z' - required: - - user_id - - nickname - UserTitleStatus: - description: User's title status - type: string - enum: - - finished - - planned - - dropped - - in-progress - UserTitle: - type: object - properties: - user_id: - type: integer - format: int64 - title: - $ref: '#/components/schemas/Title' - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time - required: - - user_id - - title_id - - status - UserTitleMini: - type: object - properties: - user_id: - type: integer - format: int64 - title_id: - type: integer - format: int64 - status: - $ref: '#/components/schemas/UserTitleStatus' - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time - required: - - user_id - - title_id - - status - Review: - type: object - additionalProperties: true - securitySchemes: - XsrfAuthHeader: - type: apiKey - in: header - name: X-XSRF-TOKEN - description: | - Anti-CSRF token. Must match the `XSRF-TOKEN` cookie. - Required for all state-changing requests (POST/PUT/PATCH/DELETE). diff --git a/api/api.gen.go b/api/api.gen.go index 04d10c0..24aebd3 100644 --- a/api/api.gen.go +++ b/api/api.gen.go @@ -7,10 +7,7 @@ import ( "context" "encoding/json" "fmt" - "io" - "mime/multipart" "net/http" - "strings" "time" "github.com/gin-gonic/gin" @@ -19,130 +16,19 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) -const ( - JwtAuthCookiesScopes = "JwtAuthCookies.Scopes" - XsrfAuthHeaderScopes = "XsrfAuthHeader.Scopes" -) - -// Defines values for ReleaseSeason. -const ( - Fall ReleaseSeason = "fall" - Spring ReleaseSeason = "spring" - Summer ReleaseSeason = "summer" - Winter ReleaseSeason = "winter" -) - -// Defines values for StorageType. -const ( - Local StorageType = "local" - S3 StorageType = "s3" -) - -// Defines values for TitleSort. -const ( - Id TitleSort = "id" - Rating TitleSort = "rating" - Views TitleSort = "views" - Year TitleSort = "year" -) - -// Defines values for TitleStatus. -const ( - TitleStatusFinished TitleStatus = "finished" - TitleStatusOngoing TitleStatus = "ongoing" - TitleStatusPlanned TitleStatus = "planned" -) - -// Defines values for UserTitleStatus. -const ( - UserTitleStatusDropped UserTitleStatus = "dropped" - UserTitleStatusFinished UserTitleStatus = "finished" - UserTitleStatusInProgress UserTitleStatus = "in-progress" - UserTitleStatusPlanned UserTitleStatus = "planned" -) - -// CursorObj defines model for CursorObj. -type CursorObj struct { - Id int64 `json:"id"` - Param *string `json:"param,omitempty"` -} - -// Image defines model for Image. -type Image struct { - Id *int64 `json:"id,omitempty"` - ImagePath *string `json:"image_path,omitempty"` - - // StorageType Image storage type - StorageType *StorageType `json:"storage_type,omitempty"` -} - -// ReleaseSeason Title release season -type ReleaseSeason string - -// StorageType Image storage type -type StorageType string - -// Studio defines model for Studio. -type Studio struct { - Description *string `json:"description,omitempty"` - Id int64 `json:"id"` - Name string `json:"name"` - Poster *Image `json:"poster,omitempty"` -} - -// Tag A localized tag: keys are language codes (ISO 639-1), values are tag names -type Tag map[string]string - -// Tags Array of localized tags -type Tags = []Tag - -// Title defines model for Title. -type Title struct { - EpisodesAired *int32 `json:"episodes_aired,omitempty"` - EpisodesAll *int32 `json:"episodes_all,omitempty"` - EpisodesLen *map[string]float64 `json:"episodes_len,omitempty"` - - // Id Unique title ID (primary key) - Id int64 `json:"id"` - Poster *Image `json:"poster,omitempty"` - Rating *float64 `json:"rating,omitempty"` - RatingCount *int32 `json:"rating_count,omitempty"` - - // ReleaseSeason Title release season - ReleaseSeason *ReleaseSeason `json:"release_season,omitempty"` - ReleaseYear *int32 `json:"release_year,omitempty"` - Studio *Studio `json:"studio,omitempty"` - - // Tags Array of localized tags - Tags Tags `json:"tags"` - - // TitleDesc Localized description. Key = language (ISO 639-1), value = description. - TitleDesc *map[string]string `json:"title_desc,omitempty"` - - // TitleNames Localized titles. Key = language (ISO 639-1), value = list of names - TitleNames map[string][]string `json:"title_names"` - - // TitleStatus Title status - TitleStatus *TitleStatus `json:"title_status,omitempty"` -} - -// TitleSort Title sort order -type TitleSort string - -// TitleStatus Title status -type TitleStatus string - // User defines model for User. type User struct { + // AvatarId ID of the user avatar (references images table) + AvatarId *int64 `json:"avatar_id"` + // CreationDate Timestamp when the user was created - CreationDate *time.Time `json:"creation_date,omitempty"` + CreationDate time.Time `json:"creation_date"` // DispName Display name DispName *string `json:"disp_name,omitempty"` // Id Unique user ID (primary key) - Id *int64 `json:"id,omitempty"` - Image *Image `json:"image,omitempty"` + Id *int64 `json:"id,omitempty"` // Mail User email Mail *openapi_types.Email `json:"mail,omitempty"` @@ -154,178 +40,16 @@ type User struct { UserDesc *string `json:"user_desc,omitempty"` } -// UserTitle defines model for UserTitle. -type UserTitle struct { - Ctime *time.Time `json:"ctime,omitempty"` - Rate *int32 `json:"rate,omitempty"` - ReviewId *int64 `json:"review_id,omitempty"` - - // Status User's title status - Status UserTitleStatus `json:"status"` - Title *Title `json:"title,omitempty"` - UserId int64 `json:"user_id"` -} - -// UserTitleMini defines model for UserTitleMini. -type UserTitleMini struct { - Ctime *time.Time `json:"ctime,omitempty"` - Rate *int32 `json:"rate,omitempty"` - ReviewId *int64 `json:"review_id,omitempty"` - - // Status User's title status - Status UserTitleStatus `json:"status"` - TitleId int64 `json:"title_id"` - UserId int64 `json:"user_id"` -} - -// UserTitleStatus User's title status -type UserTitleStatus string - -// Cursor defines model for cursor. -type Cursor = string - -// PostMediaUploadMultipartBody defines parameters for PostMediaUpload. -type PostMediaUploadMultipartBody = interface{} - -// GetTitlesParams defines parameters for GetTitles. -type GetTitlesParams struct { - Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` - Sort *TitleSort `form:"sort,omitempty" json:"sort,omitempty"` - SortForward *bool `form:"sort_forward,omitempty" json:"sort_forward,omitempty"` - ExtSearch *bool `form:"ext_search,omitempty" json:"ext_search,omitempty"` - Word *string `form:"word,omitempty" json:"word,omitempty"` - - // Status List of title statuses to filter - Status *[]TitleStatus `form:"status,omitempty" json:"status,omitempty"` - Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` - ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` - ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"` - Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` - Offset *int32 `form:"offset,omitempty" json:"offset,omitempty"` - Fields *string `form:"fields,omitempty" json:"fields,omitempty"` -} - -// GetTitleParams defines parameters for GetTitle. -type GetTitleParams struct { +// GetUsersUserIdParams defines parameters for GetUsersUserId. +type GetUsersUserIdParams struct { Fields *string `form:"fields,omitempty" json:"fields,omitempty"` } -// GetUsersParams defines parameters for GetUsers. -type GetUsersParams struct { - Word *string `form:"word,omitempty" json:"word,omitempty"` - Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` - - // CursorId pass cursor naked - CursorId *int32 `form:"cursor_id,omitempty" json:"cursor_id,omitempty"` -} - -// GetUsersIdParams defines parameters for GetUsersId. -type GetUsersIdParams struct { - Fields *string `form:"fields,omitempty" json:"fields,omitempty"` -} - -// UpdateUserJSONBody defines parameters for UpdateUser. -type UpdateUserJSONBody struct { - // AvatarId ID of the user avatar (references `images.id`); set to `null` to remove avatar - AvatarId *int64 `json:"avatar_id"` - - // DispName Display name - DispName *string `json:"disp_name,omitempty"` - - // Mail User email (must be unique and valid) - Mail *openapi_types.Email `json:"mail,omitempty"` - - // Nickname Username (alphanumeric + `_` or `-`, 3–16 chars) - Nickname *string `json:"nickname,omitempty"` - - // UserDesc User description / bio - UserDesc *string `json:"user_desc,omitempty"` -} - -// GetUserTitlesParams defines parameters for GetUserTitles. -type GetUserTitlesParams struct { - Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` - Sort *TitleSort `form:"sort,omitempty" json:"sort,omitempty"` - SortForward *bool `form:"sort_forward,omitempty" json:"sort_forward,omitempty"` - Word *string `form:"word,omitempty" json:"word,omitempty"` - - // Status List of title statuses to filter - Status *[]TitleStatus `form:"status,omitempty" json:"status,omitempty"` - WatchStatus *[]UserTitleStatus `form:"watch_status,omitempty" json:"watch_status,omitempty"` - Rating *float64 `form:"rating,omitempty" json:"rating,omitempty"` - MyRate *int32 `form:"my_rate,omitempty" json:"my_rate,omitempty"` - ReleaseYear *int32 `form:"release_year,omitempty" json:"release_year,omitempty"` - ReleaseSeason *ReleaseSeason `form:"release_season,omitempty" json:"release_season,omitempty"` - Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` - Fields *string `form:"fields,omitempty" json:"fields,omitempty"` -} - -// AddUserTitleJSONBody defines parameters for AddUserTitle. -type AddUserTitleJSONBody struct { - Ftime *time.Time `json:"ftime,omitempty"` - Rate *int32 `json:"rate,omitempty"` - - // Status User's title status - Status UserTitleStatus `json:"status"` - TitleId int64 `json:"title_id"` -} - -// UpdateUserTitleJSONBody defines parameters for UpdateUserTitle. -type UpdateUserTitleJSONBody struct { - Ftime *time.Time `json:"ftime,omitempty"` - Rate *int32 `json:"rate,omitempty"` - - // Status User's title status - Status *UserTitleStatus `json:"status,omitempty"` -} - -// PostMediaUploadMultipartRequestBody defines body for PostMediaUpload for multipart/form-data ContentType. -type PostMediaUploadMultipartRequestBody = PostMediaUploadMultipartBody - -// UpdateUserJSONRequestBody defines body for UpdateUser for application/json ContentType. -type UpdateUserJSONRequestBody UpdateUserJSONBody - -// AddUserTitleJSONRequestBody defines body for AddUserTitle for application/json ContentType. -type AddUserTitleJSONRequestBody AddUserTitleJSONBody - -// UpdateUserTitleJSONRequestBody defines body for UpdateUserTitle for application/json ContentType. -type UpdateUserTitleJSONRequestBody UpdateUserTitleJSONBody - // ServerInterface represents all server handlers. type ServerInterface interface { - // Upload an image (PNG, JPEG, or WebP) - // (POST /media/upload) - PostMediaUpload(c *gin.Context) - // Get titles - // (GET /titles) - GetTitles(c *gin.Context, params GetTitlesParams) - // Get title description - // (GET /titles/{title_id}) - GetTitle(c *gin.Context, titleId int64, params GetTitleParams) - // Search user by nickname or dispname (both in one param), response is always sorted by id - // (GET /users/) - GetUsers(c *gin.Context, params GetUsersParams) // Get user info // (GET /users/{user_id}) - GetUsersId(c *gin.Context, userId string, params GetUsersIdParams) - // Partially update a user account - // (PATCH /users/{user_id}) - UpdateUser(c *gin.Context, userId int64) - // Get user titles - // (GET /users/{user_id}/titles) - GetUserTitles(c *gin.Context, userId string, params GetUserTitlesParams) - // Add a title to a user - // (POST /users/{user_id}/titles) - AddUserTitle(c *gin.Context, userId int64) - // Delete a usertitle - // (DELETE /users/{user_id}/titles/{title_id}) - DeleteUserTitle(c *gin.Context, userId int64, titleId int64) - // Get user title - // (GET /users/{user_id}/titles/{title_id}) - GetUserTitle(c *gin.Context, userId int64, titleId int64) - // Update a usertitle - // (PATCH /users/{user_id}/titles/{title_id}) - UpdateUserTitle(c *gin.Context, userId int64, titleId int64) + GetUsersUserId(c *gin.Context, userId string, params GetUsersUserIdParams) } // ServerInterfaceWrapper converts contexts to parameters. @@ -337,214 +61,8 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) -// PostMediaUpload operation middleware -func (siw *ServerInterfaceWrapper) PostMediaUpload(c *gin.Context) { - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.PostMediaUpload(c) -} - -// GetTitles operation middleware -func (siw *ServerInterfaceWrapper) GetTitles(c *gin.Context) { - - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params GetTitlesParams - - // ------------- Optional query parameter "cursor" ------------- - - err = runtime.BindQueryParameter("form", true, false, "cursor", c.Request.URL.Query(), ¶ms.Cursor) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter cursor: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "sort" ------------- - - err = runtime.BindQueryParameter("form", true, false, "sort", c.Request.URL.Query(), ¶ms.Sort) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "sort_forward" ------------- - - err = runtime.BindQueryParameter("form", true, false, "sort_forward", c.Request.URL.Query(), ¶ms.SortForward) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort_forward: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "ext_search" ------------- - - err = runtime.BindQueryParameter("form", true, false, "ext_search", c.Request.URL.Query(), ¶ms.ExtSearch) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter ext_search: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "word" ------------- - - err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), ¶ms.Word) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter word: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "status" ------------- - - err = runtime.BindQueryParameter("form", false, false, "status", c.Request.URL.Query(), ¶ms.Status) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter status: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "rating" ------------- - - err = runtime.BindQueryParameter("form", true, false, "rating", c.Request.URL.Query(), ¶ms.Rating) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter rating: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "release_year" ------------- - - err = runtime.BindQueryParameter("form", true, false, "release_year", c.Request.URL.Query(), ¶ms.ReleaseYear) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter release_year: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "release_season" ------------- - - err = runtime.BindQueryParameter("form", true, false, "release_season", c.Request.URL.Query(), ¶ms.ReleaseSeason) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter release_season: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "limit" ------------- - - err = runtime.BindQueryParameter("form", true, false, "limit", c.Request.URL.Query(), ¶ms.Limit) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "offset" ------------- - - err = runtime.BindQueryParameter("form", true, false, "offset", c.Request.URL.Query(), ¶ms.Offset) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter offset: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "fields" ------------- - - err = runtime.BindQueryParameter("form", true, false, "fields", c.Request.URL.Query(), ¶ms.Fields) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter fields: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.GetTitles(c, params) -} - -// GetTitle operation middleware -func (siw *ServerInterfaceWrapper) GetTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - c.Set(JwtAuthCookiesScopes, []string{}) - - // Parameter object where we will unmarshal all parameters from the context - var params GetTitleParams - - // ------------- Optional query parameter "fields" ------------- - - err = runtime.BindQueryParameter("form", true, false, "fields", c.Request.URL.Query(), ¶ms.Fields) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter fields: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.GetTitle(c, titleId, params) -} - -// GetUsers operation middleware -func (siw *ServerInterfaceWrapper) GetUsers(c *gin.Context) { - - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params GetUsersParams - - // ------------- Optional query parameter "word" ------------- - - err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), ¶ms.Word) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter word: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "limit" ------------- - - err = runtime.BindQueryParameter("form", true, false, "limit", c.Request.URL.Query(), ¶ms.Limit) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "cursor_id" ------------- - - err = runtime.BindQueryParameter("form", true, false, "cursor_id", c.Request.URL.Query(), ¶ms.CursorId) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter cursor_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.GetUsers(c, params) -} - -// GetUsersId operation middleware -func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) { +// GetUsersUserId operation middleware +func (siw *ServerInterfaceWrapper) GetUsersUserId(c *gin.Context) { var err error @@ -558,7 +76,7 @@ func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) { } // Parameter object where we will unmarshal all parameters from the context - var params GetUsersIdParams + var params GetUsersUserIdParams // ------------- Optional query parameter "fields" ------------- @@ -575,283 +93,7 @@ func (siw *ServerInterfaceWrapper) GetUsersId(c *gin.Context) { } } - siw.Handler.GetUsersId(c, userId, params) -} - -// UpdateUser operation middleware -func (siw *ServerInterfaceWrapper) UpdateUser(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest) - return - } - - c.Set(XsrfAuthHeaderScopes, []string{}) - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.UpdateUser(c, userId) -} - -// GetUserTitles operation middleware -func (siw *ServerInterfaceWrapper) GetUserTitles(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId string - - err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest) - return - } - - // Parameter object where we will unmarshal all parameters from the context - var params GetUserTitlesParams - - // ------------- Optional query parameter "cursor" ------------- - - err = runtime.BindQueryParameter("form", true, false, "cursor", c.Request.URL.Query(), ¶ms.Cursor) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter cursor: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "sort" ------------- - - err = runtime.BindQueryParameter("form", true, false, "sort", c.Request.URL.Query(), ¶ms.Sort) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "sort_forward" ------------- - - err = runtime.BindQueryParameter("form", true, false, "sort_forward", c.Request.URL.Query(), ¶ms.SortForward) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter sort_forward: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "word" ------------- - - err = runtime.BindQueryParameter("form", true, false, "word", c.Request.URL.Query(), ¶ms.Word) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter word: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "status" ------------- - - err = runtime.BindQueryParameter("form", false, false, "status", c.Request.URL.Query(), ¶ms.Status) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter status: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "watch_status" ------------- - - err = runtime.BindQueryParameter("form", false, false, "watch_status", c.Request.URL.Query(), ¶ms.WatchStatus) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter watch_status: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "rating" ------------- - - err = runtime.BindQueryParameter("form", true, false, "rating", c.Request.URL.Query(), ¶ms.Rating) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter rating: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "my_rate" ------------- - - err = runtime.BindQueryParameter("form", true, false, "my_rate", c.Request.URL.Query(), ¶ms.MyRate) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter my_rate: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "release_year" ------------- - - err = runtime.BindQueryParameter("form", true, false, "release_year", c.Request.URL.Query(), ¶ms.ReleaseYear) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter release_year: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "release_season" ------------- - - err = runtime.BindQueryParameter("form", true, false, "release_season", c.Request.URL.Query(), ¶ms.ReleaseSeason) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter release_season: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "limit" ------------- - - err = runtime.BindQueryParameter("form", true, false, "limit", c.Request.URL.Query(), ¶ms.Limit) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest) - return - } - - // ------------- Optional query parameter "fields" ------------- - - err = runtime.BindQueryParameter("form", true, false, "fields", c.Request.URL.Query(), ¶ms.Fields) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter fields: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.GetUserTitles(c, userId, params) -} - -// AddUserTitle operation middleware -func (siw *ServerInterfaceWrapper) AddUserTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.AddUserTitle(c, userId) -} - -// DeleteUserTitle operation middleware -func (siw *ServerInterfaceWrapper) DeleteUserTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest) - return - } - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - c.Set(XsrfAuthHeaderScopes, []string{}) - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.DeleteUserTitle(c, userId, titleId) -} - -// GetUserTitle operation middleware -func (siw *ServerInterfaceWrapper) GetUserTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest) - return - } - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.GetUserTitle(c, userId, titleId) -} - -// UpdateUserTitle operation middleware -func (siw *ServerInterfaceWrapper) UpdateUserTitle(c *gin.Context) { - - var err error - - // ------------- Path parameter "user_id" ------------- - var userId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "user_id", c.Param("user_id"), &userId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter user_id: %w", err), http.StatusBadRequest) - return - } - - // ------------- Path parameter "title_id" ------------- - var titleId int64 - - err = runtime.BindStyledParameterWithOptions("simple", "title_id", c.Param("title_id"), &titleId, runtime.BindStyledParameterOptions{Explode: false, Required: true}) - if err != nil { - siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter title_id: %w", err), http.StatusBadRequest) - return - } - - c.Set(XsrfAuthHeaderScopes, []string{}) - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.UpdateUserTitle(c, userId, titleId) + siw.Handler.GetUsersUserId(c, userId, params) } // GinServerOptions provides options for the Gin server. @@ -881,632 +123,40 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } - router.POST(options.BaseURL+"/media/upload", wrapper.PostMediaUpload) - router.GET(options.BaseURL+"/titles", wrapper.GetTitles) - router.GET(options.BaseURL+"/titles/:title_id", wrapper.GetTitle) - router.GET(options.BaseURL+"/users/", wrapper.GetUsers) - router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersId) - router.PATCH(options.BaseURL+"/users/:user_id", wrapper.UpdateUser) - router.GET(options.BaseURL+"/users/:user_id/titles", wrapper.GetUserTitles) - router.POST(options.BaseURL+"/users/:user_id/titles", wrapper.AddUserTitle) - router.DELETE(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.DeleteUserTitle) - router.GET(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.GetUserTitle) - router.PATCH(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.UpdateUserTitle) + router.GET(options.BaseURL+"/users/:user_id", wrapper.GetUsersUserId) } -type PostMediaUploadRequestObject struct { - Body io.Reader - MultipartBody *multipart.Reader -} - -type PostMediaUploadResponseObject interface { - VisitPostMediaUploadResponse(w http.ResponseWriter) error -} - -type PostMediaUpload200JSONResponse Image - -func (response PostMediaUpload200JSONResponse) VisitPostMediaUploadResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type PostMediaUpload400JSONResponse string - -func (response PostMediaUpload400JSONResponse) VisitPostMediaUploadResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) - - return json.NewEncoder(w).Encode(response) -} - -type PostMediaUpload415Response struct { -} - -func (response PostMediaUpload415Response) VisitPostMediaUploadResponse(w http.ResponseWriter) error { - w.WriteHeader(415) - return nil -} - -type PostMediaUpload500Response struct { -} - -func (response PostMediaUpload500Response) VisitPostMediaUploadResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type GetTitlesRequestObject struct { - Params GetTitlesParams -} - -type GetTitlesResponseObject interface { - VisitGetTitlesResponse(w http.ResponseWriter) error -} - -type GetTitles200JSONResponse struct { - Cursor CursorObj `json:"cursor"` - - // Data List of titles - Data []Title `json:"data"` -} - -func (response GetTitles200JSONResponse) VisitGetTitlesResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type GetTitles204Response struct { -} - -func (response GetTitles204Response) VisitGetTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil -} - -type GetTitles400Response struct { -} - -func (response GetTitles400Response) VisitGetTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type GetTitles500Response struct { -} - -func (response GetTitles500Response) VisitGetTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type GetTitleRequestObject struct { - TitleId int64 `json:"title_id"` - Params GetTitleParams -} - -type GetTitleResponseObject interface { - VisitGetTitleResponse(w http.ResponseWriter) error -} - -type GetTitle200JSONResponse Title - -func (response GetTitle200JSONResponse) VisitGetTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type GetTitle204Response struct { -} - -func (response GetTitle204Response) VisitGetTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil -} - -type GetTitle400Response struct { -} - -func (response GetTitle400Response) VisitGetTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type GetTitle404Response struct { -} - -func (response GetTitle404Response) VisitGetTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type GetTitle500Response struct { -} - -func (response GetTitle500Response) VisitGetTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type GetUsersRequestObject struct { - Params GetUsersParams -} - -type GetUsersResponseObject interface { - VisitGetUsersResponse(w http.ResponseWriter) error -} - -type GetUsers200JSONResponse struct { - Cursor int64 `json:"cursor"` - - // Data List of users - Data []User `json:"data"` -} - -func (response GetUsers200JSONResponse) VisitGetUsersResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type GetUsers204Response struct { -} - -func (response GetUsers204Response) VisitGetUsersResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil -} - -type GetUsers400Response struct { -} - -func (response GetUsers400Response) VisitGetUsersResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type GetUsers500Response struct { -} - -func (response GetUsers500Response) VisitGetUsersResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type GetUsersIdRequestObject struct { +type GetUsersUserIdRequestObject struct { UserId string `json:"user_id"` - Params GetUsersIdParams + Params GetUsersUserIdParams } -type GetUsersIdResponseObject interface { - VisitGetUsersIdResponse(w http.ResponseWriter) error +type GetUsersUserIdResponseObject interface { + VisitGetUsersUserIdResponse(w http.ResponseWriter) error } -type GetUsersId200JSONResponse User +type GetUsersUserId200JSONResponse User -func (response GetUsersId200JSONResponse) VisitGetUsersIdResponse(w http.ResponseWriter) error { +func (response GetUsersUserId200JSONResponse) VisitGetUsersUserIdResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type GetUsersId400Response struct { +type GetUsersUserId404Response struct { } -func (response GetUsersId400Response) VisitGetUsersIdResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type GetUsersId404Response struct { -} - -func (response GetUsersId404Response) VisitGetUsersIdResponse(w http.ResponseWriter) error { +func (response GetUsersUserId404Response) VisitGetUsersUserIdResponse(w http.ResponseWriter) error { w.WriteHeader(404) return nil } -type GetUsersId500Response struct { -} - -func (response GetUsersId500Response) VisitGetUsersIdResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type UpdateUserRequestObject struct { - UserId int64 `json:"user_id"` - Body *UpdateUserJSONRequestBody -} - -type UpdateUserResponseObject interface { - VisitUpdateUserResponse(w http.ResponseWriter) error -} - -type UpdateUser200JSONResponse User - -func (response UpdateUser200JSONResponse) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type UpdateUser400Response struct { -} - -func (response UpdateUser400Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type UpdateUser401Response struct { -} - -func (response UpdateUser401Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type UpdateUser403Response struct { -} - -func (response UpdateUser403Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type UpdateUser404Response struct { -} - -func (response UpdateUser404Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type UpdateUser409Response struct { -} - -func (response UpdateUser409Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(409) - return nil -} - -type UpdateUser422Response struct { -} - -func (response UpdateUser422Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(422) - return nil -} - -type UpdateUser500Response struct { -} - -func (response UpdateUser500Response) VisitUpdateUserResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type GetUserTitlesRequestObject struct { - UserId string `json:"user_id"` - Params GetUserTitlesParams -} - -type GetUserTitlesResponseObject interface { - VisitGetUserTitlesResponse(w http.ResponseWriter) error -} - -type GetUserTitles200JSONResponse struct { - Cursor CursorObj `json:"cursor"` - Data []UserTitle `json:"data"` -} - -func (response GetUserTitles200JSONResponse) VisitGetUserTitlesResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type GetUserTitles204Response struct { -} - -func (response GetUserTitles204Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil -} - -type GetUserTitles400Response struct { -} - -func (response GetUserTitles400Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type GetUserTitles404Response struct { -} - -func (response GetUserTitles404Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type GetUserTitles500Response struct { -} - -func (response GetUserTitles500Response) VisitGetUserTitlesResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type AddUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - Body *AddUserTitleJSONRequestBody -} - -type AddUserTitleResponseObject interface { - VisitAddUserTitleResponse(w http.ResponseWriter) error -} - -type AddUserTitle200JSONResponse UserTitleMini - -func (response AddUserTitle200JSONResponse) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type AddUserTitle400Response struct { -} - -func (response AddUserTitle400Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type AddUserTitle401Response struct { -} - -func (response AddUserTitle401Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type AddUserTitle403Response struct { -} - -func (response AddUserTitle403Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type AddUserTitle404Response struct { -} - -func (response AddUserTitle404Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type AddUserTitle409Response struct { -} - -func (response AddUserTitle409Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(409) - return nil -} - -type AddUserTitle500Response struct { -} - -func (response AddUserTitle500Response) VisitAddUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type DeleteUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - TitleId int64 `json:"title_id"` -} - -type DeleteUserTitleResponseObject interface { - VisitDeleteUserTitleResponse(w http.ResponseWriter) error -} - -type DeleteUserTitle200Response struct { -} - -func (response DeleteUserTitle200Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(200) - return nil -} - -type DeleteUserTitle401Response struct { -} - -func (response DeleteUserTitle401Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type DeleteUserTitle403Response struct { -} - -func (response DeleteUserTitle403Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type DeleteUserTitle404Response struct { -} - -func (response DeleteUserTitle404Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type DeleteUserTitle500Response struct { -} - -func (response DeleteUserTitle500Response) VisitDeleteUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type GetUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - TitleId int64 `json:"title_id"` -} - -type GetUserTitleResponseObject interface { - VisitGetUserTitleResponse(w http.ResponseWriter) error -} - -type GetUserTitle200JSONResponse UserTitleMini - -func (response GetUserTitle200JSONResponse) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type GetUserTitle204Response struct { -} - -func (response GetUserTitle204Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil -} - -type GetUserTitle400Response struct { -} - -func (response GetUserTitle400Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type GetUserTitle404Response struct { -} - -func (response GetUserTitle404Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type GetUserTitle500Response struct { -} - -func (response GetUserTitle500Response) VisitGetUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type UpdateUserTitleRequestObject struct { - UserId int64 `json:"user_id"` - TitleId int64 `json:"title_id"` - Body *UpdateUserTitleJSONRequestBody -} - -type UpdateUserTitleResponseObject interface { - VisitUpdateUserTitleResponse(w http.ResponseWriter) error -} - -type UpdateUserTitle200JSONResponse UserTitleMini - -func (response UpdateUserTitle200JSONResponse) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type UpdateUserTitle400Response struct { -} - -func (response UpdateUserTitle400Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type UpdateUserTitle401Response struct { -} - -func (response UpdateUserTitle401Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type UpdateUserTitle403Response struct { -} - -func (response UpdateUserTitle403Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(403) - return nil -} - -type UpdateUserTitle404Response struct { -} - -func (response UpdateUserTitle404Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(404) - return nil -} - -type UpdateUserTitle500Response struct { -} - -func (response UpdateUserTitle500Response) VisitUpdateUserTitleResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - // StrictServerInterface represents all server handlers. type StrictServerInterface interface { - // Upload an image (PNG, JPEG, or WebP) - // (POST /media/upload) - PostMediaUpload(ctx context.Context, request PostMediaUploadRequestObject) (PostMediaUploadResponseObject, error) - // Get titles - // (GET /titles) - GetTitles(ctx context.Context, request GetTitlesRequestObject) (GetTitlesResponseObject, error) - // Get title description - // (GET /titles/{title_id}) - GetTitle(ctx context.Context, request GetTitleRequestObject) (GetTitleResponseObject, error) - // Search user by nickname or dispname (both in one param), response is always sorted by id - // (GET /users/) - GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error) // Get user info // (GET /users/{user_id}) - GetUsersId(ctx context.Context, request GetUsersIdRequestObject) (GetUsersIdResponseObject, error) - // Partially update a user account - // (PATCH /users/{user_id}) - UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, error) - // Get user titles - // (GET /users/{user_id}/titles) - GetUserTitles(ctx context.Context, request GetUserTitlesRequestObject) (GetUserTitlesResponseObject, error) - // Add a title to a user - // (POST /users/{user_id}/titles) - AddUserTitle(ctx context.Context, request AddUserTitleRequestObject) (AddUserTitleResponseObject, error) - // Delete a usertitle - // (DELETE /users/{user_id}/titles/{title_id}) - DeleteUserTitle(ctx context.Context, request DeleteUserTitleRequestObject) (DeleteUserTitleResponseObject, error) - // Get user title - // (GET /users/{user_id}/titles/{title_id}) - GetUserTitle(ctx context.Context, request GetUserTitleRequestObject) (GetUserTitleResponseObject, error) - // Update a usertitle - // (PATCH /users/{user_id}/titles/{title_id}) - UpdateUserTitle(ctx context.Context, request UpdateUserTitleRequestObject) (UpdateUserTitleResponseObject, error) + GetUsersUserId(ctx context.Context, request GetUsersUserIdRequestObject) (GetUsersUserIdResponseObject, error) } type StrictHandlerFunc = strictgin.StrictGinHandlerFunc @@ -1521,137 +171,18 @@ type strictHandler struct { middlewares []StrictMiddlewareFunc } -// PostMediaUpload operation middleware -func (sh *strictHandler) PostMediaUpload(ctx *gin.Context) { - var request PostMediaUploadRequestObject - - if strings.HasPrefix(ctx.GetHeader("Content-Type"), "encoding") { - request.Body = ctx.Request.Body - } - if strings.HasPrefix(ctx.GetHeader("Content-Type"), "multipart/form-data") { - if reader, err := ctx.Request.MultipartReader(); err == nil { - request.MultipartBody = reader - } else { - ctx.Error(err) - return - } - } - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.PostMediaUpload(ctx, request.(PostMediaUploadRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostMediaUpload") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostMediaUploadResponseObject); ok { - if err := validResponse.VisitPostMediaUploadResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// GetTitles operation middleware -func (sh *strictHandler) GetTitles(ctx *gin.Context, params GetTitlesParams) { - var request GetTitlesRequestObject - - request.Params = params - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetTitles(ctx, request.(GetTitlesRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetTitles") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetTitlesResponseObject); ok { - if err := validResponse.VisitGetTitlesResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// GetTitle operation middleware -func (sh *strictHandler) GetTitle(ctx *gin.Context, titleId int64, params GetTitleParams) { - var request GetTitleRequestObject - - request.TitleId = titleId - request.Params = params - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetTitle(ctx, request.(GetTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetTitleResponseObject); ok { - if err := validResponse.VisitGetTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// GetUsers operation middleware -func (sh *strictHandler) GetUsers(ctx *gin.Context, params GetUsersParams) { - var request GetUsersRequestObject - - request.Params = params - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetUsers(ctx, request.(GetUsersRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUsers") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetUsersResponseObject); ok { - if err := validResponse.VisitGetUsersResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// GetUsersId operation middleware -func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetUsersIdParams) { - var request GetUsersIdRequestObject +// GetUsersUserId operation middleware +func (sh *strictHandler) GetUsersUserId(ctx *gin.Context, userId string, params GetUsersUserIdParams) { + var request GetUsersUserIdRequestObject request.UserId = userId request.Params = params handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetUsersId(ctx, request.(GetUsersIdRequestObject)) + return sh.ssi.GetUsersUserId(ctx, request.(GetUsersUserIdRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUsersId") + handler = middleware(handler, "GetUsersUserId") } response, err := handler(ctx, request) @@ -1659,198 +190,8 @@ func (sh *strictHandler) GetUsersId(ctx *gin.Context, userId string, params GetU if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetUsersIdResponseObject); ok { - if err := validResponse.VisitGetUsersIdResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// UpdateUser operation middleware -func (sh *strictHandler) UpdateUser(ctx *gin.Context, userId int64) { - var request UpdateUserRequestObject - - request.UserId = userId - - var body UpdateUserJSONRequestBody - if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return - } - request.Body = &body - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.UpdateUser(ctx, request.(UpdateUserRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "UpdateUser") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(UpdateUserResponseObject); ok { - if err := validResponse.VisitUpdateUserResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// GetUserTitles operation middleware -func (sh *strictHandler) GetUserTitles(ctx *gin.Context, userId string, params GetUserTitlesParams) { - var request GetUserTitlesRequestObject - - request.UserId = userId - request.Params = params - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetUserTitles(ctx, request.(GetUserTitlesRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUserTitles") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetUserTitlesResponseObject); ok { - if err := validResponse.VisitGetUserTitlesResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// AddUserTitle operation middleware -func (sh *strictHandler) AddUserTitle(ctx *gin.Context, userId int64) { - var request AddUserTitleRequestObject - - request.UserId = userId - - var body AddUserTitleJSONRequestBody - if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return - } - request.Body = &body - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.AddUserTitle(ctx, request.(AddUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "AddUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(AddUserTitleResponseObject); ok { - if err := validResponse.VisitAddUserTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// DeleteUserTitle operation middleware -func (sh *strictHandler) DeleteUserTitle(ctx *gin.Context, userId int64, titleId int64) { - var request DeleteUserTitleRequestObject - - request.UserId = userId - request.TitleId = titleId - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.DeleteUserTitle(ctx, request.(DeleteUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "DeleteUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(DeleteUserTitleResponseObject); ok { - if err := validResponse.VisitDeleteUserTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// GetUserTitle operation middleware -func (sh *strictHandler) GetUserTitle(ctx *gin.Context, userId int64, titleId int64) { - var request GetUserTitleRequestObject - - request.UserId = userId - request.TitleId = titleId - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetUserTitle(ctx, request.(GetUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetUserTitleResponseObject); ok { - if err := validResponse.VisitGetUserTitleResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// UpdateUserTitle operation middleware -func (sh *strictHandler) UpdateUserTitle(ctx *gin.Context, userId int64, titleId int64) { - var request UpdateUserTitleRequestObject - - request.UserId = userId - request.TitleId = titleId - - var body UpdateUserTitleJSONRequestBody - if err := ctx.ShouldBindJSON(&body); err != nil { - ctx.Status(http.StatusBadRequest) - ctx.Error(err) - return - } - request.Body = &body - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.UpdateUserTitle(ctx, request.(UpdateUserTitleRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "UpdateUserTitle") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(UpdateUserTitleResponseObject); ok { - if err := validResponse.VisitUpdateUserTitleResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(GetUsersUserIdResponseObject); ok { + if err := validResponse.VisitGetUsersUserIdResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { diff --git a/api/openapi.yaml b/api/openapi.yaml index 26813fc..b20f677 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1,31 +1,592 @@ -openapi: 3.0.4 +openapi: 3.1.1 info: title: Titles, Users, Reviews, Tags, and Media API version: 1.0.0 - servers: - url: /api/v1 - paths: - /titles: - $ref: "./paths/titles.yaml" - /titles/{title_id}: - $ref: "./paths/titles-id.yaml" - /users/: - $ref: "./paths/users.yaml" + # /title: + # get: + # summary: Get titles + # parameters: + # - in: query + # name: query + # schema: + # type: string + # - in: query + # name: limit + # schema: + # type: integer + # default: 10 + # - in: query + # name: offset + # schema: + # type: integer + # default: 0 + # - in: query + # name: fields + # schema: + # type: string + # default: all + # responses: + # '200': + # description: List of titles + # content: + # application/json: + # schema: + # type: array + # items: + # $ref: '#/components/schemas/Title' + # '204': + # description: No titles found + +# /title/{title_id}: +# get: +# summary: Get title description +# parameters: +# - in: path +# name: title_id +# required: true +# schema: +# type: string +# - in: query +# name: fields +# schema: +# type: string +# default: all +# responses: +# '200': +# description: Title description +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/Title' +# '404': +# description: Title not found + +# patch: +# summary: Update title info +# parameters: +# - in: path +# name: title_id +# required: true +# schema: +# type: string +# requestBody: +# required: true +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/Title' +# responses: +# '200': +# description: Update result +# content: +# application/json: +# schema: +# type: object +# properties: +# success: +# type: boolean +# error: +# type: string +# user_json: +# $ref: '#/components/schemas/User' + +# /title/{title_id}/reviews: +# get: +# summary: Get title reviews +# parameters: +# - in: path +# name: title_id +# required: true +# schema: +# type: string +# - in: query +# name: limit +# schema: +# type: integer +# default: 10 +# - in: query +# name: offset +# schema: +# type: integer +# default: 0 +# responses: +# '200': +# description: List of reviews +# content: +# application/json: +# schema: +# type: array +# items: +# $ref: '#/components/schemas/Review' +# '204': +# description: No reviews found + /users/{user_id}: - $ref: "./paths/users-id.yaml" - /users/{user_id}/titles: - $ref: "./paths/users-id-titles.yaml" - /users/{user_id}/titles/{title_id}: - $ref: "./paths/users-id-titles-id.yaml" - /media/upload: - $ref: "./paths/media_upload.yaml" - + get: + summary: Get user info + parameters: + - in: path + name: user_id + required: true + schema: + type: string + - in: query + name: fields + schema: + type: string + default: all + responses: + '200': + description: User info + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: User not found + + # patch: + # summary: Update user + # parameters: + # - in: path + # name: user_id + # required: true + # schema: + # type: string + # requestBody: + # required: true + # content: + # application/json: + # schema: + # $ref: '#/components/schemas/User' + # responses: + # '200': + # description: Update result + # content: + # application/json: + # schema: + # type: object + # properties: + # success: + # type: boolean + # error: + # type: string + + # delete: + # summary: Delete user + # parameters: + # - in: path + # name: user_id + # required: true + # schema: + # type: string + # responses: + # '200': + # description: Delete result + # content: + # application/json: + # schema: + # type: object + # properties: + # success: + # type: boolean + # error: + # type: string + + # /users: + # get: + # summary: Search user + # parameters: + # - in: query + # name: query + # schema: + # type: string + # - in: query + # name: fields + # schema: + # type: string + # responses: + # '200': + # description: List of users + # content: + # application/json: + # schema: + # type: array + # items: + # $ref: '#/components/schemas/User' + + # post: + # summary: Add new user + # requestBody: + # required: true + # content: + # application/json: + # schema: + # $ref: '#/components/schemas/User' + # responses: + # '200': + # description: Add result + # content: + # application/json: + # schema: + # type: object + # properties: + # success: + # type: boolean + # error: + # type: string + # user_json: + # $ref: '#/components/schemas/User' + +# /users/{user_id}/titles: +# get: +# summary: Get user titles +# parameters: +# - in: path +# name: user_id +# required: true +# schema: +# type: string +# - in: query +# name: query +# schema: +# type: string +# - in: query +# name: limit +# schema: +# type: integer +# default: 10 +# - in: query +# name: offset +# schema: +# type: integer +# default: 0 +# - in: query +# name: fields +# schema: +# type: string +# default: all +# responses: +# '200': +# description: List of user titles +# content: +# application/json: +# schema: +# type: array +# items: +# $ref: '#/components/schemas/UserTitle' +# '204': +# description: No titles found + +# post: +# summary: Add user title +# parameters: +# - in: path +# name: user_id +# required: true +# schema: +# type: string +# requestBody: +# required: true +# content: +# application/json: +# schema: +# type: object +# properties: +# title_id: +# type: string +# status: +# type: string +# responses: +# '200': +# description: Add result +# content: +# application/json: +# schema: +# type: object +# properties: +# success: +# type: boolean +# error: +# type: string + +# patch: +# summary: Update user title +# parameters: +# - in: path +# name: user_id +# required: true +# schema: +# type: string +# requestBody: +# required: true +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/UserTitle' +# responses: +# '200': +# description: Update result +# content: +# application/json: +# schema: +# type: object +# properties: +# success: +# type: boolean +# error: +# type: string + +# delete: +# summary: Delete user title +# parameters: +# - in: path +# name: user_id +# required: true +# schema: +# type: string +# - in: query +# name: title_id +# schema: +# type: string +# responses: +# '200': +# description: Delete result +# content: +# application/json: +# schema: +# type: object +# properties: +# success: +# type: boolean +# error: +# type: string + +# /users/{user_id}/reviews: +# get: +# summary: Get user reviews +# parameters: +# - in: path +# name: user_id +# required: true +# schema: +# type: string +# - in: query +# name: limit +# schema: +# type: integer +# default: 10 +# - in: query +# name: offset +# schema: +# type: integer +# default: 0 +# responses: +# '200': +# description: List of reviews +# content: +# application/json: +# schema: +# type: array +# items: +# $ref: '#/components/schemas/Review' + +# /reviews: +# post: +# summary: Add review +# requestBody: +# required: true +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/Review' +# responses: +# '200': +# description: Add result +# content: +# application/json: +# schema: +# type: object +# properties: +# success: +# type: boolean +# error: +# type: string + +# /reviews/{review_id}: +# patch: +# summary: Update review +# parameters: +# - in: path +# name: review_id +# required: true +# schema: +# type: string +# requestBody: +# required: true +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/Review' +# responses: +# '200': +# description: Update result +# content: +# application/json: +# schema: +# type: object +# properties: +# success: +# type: boolean +# error: +# type: string +# delete: +# summary: Delete review +# parameters: +# - in: path +# name: review_id +# required: true +# schema: +# type: string +# responses: +# '200': +# description: Delete result +# content: +# application/json: +# schema: +# type: object +# properties: +# success: +# type: boolean +# error: +# type: string + +# /tags: +# get: +# summary: Get tags +# parameters: +# - in: query +# name: limit +# schema: +# type: integer +# default: 10 +# - in: query +# name: offset +# schema: +# type: integer +# default: 0 +# - in: query +# name: fields +# schema: +# type: string +# responses: +# '200': +# description: List of tags +# content: +# application/json: +# schema: +# type: array +# items: +# $ref: '#/components/schemas/Tag' + +# /media: +# post: +# summary: Upload image +# responses: +# '200': +# description: Upload result +# content: +# application/json: +# schema: +# type: object +# properties: +# success: +# type: boolean +# error: +# type: string +# image_id: +# type: string + +# get: +# summary: Get image path +# parameters: +# - in: query +# name: image_id +# required: true +# schema: +# type: string +# responses: +# '200': +# description: Image path +# content: +# application/json: +# schema: +# type: object +# properties: +# success: +# type: boolean +# error: +# type: string +# image_path: +# type: string + components: - parameters: - $ref: "./parameters/_index.yaml" schemas: - $ref: "./schemas/_index.yaml" - securitySchemes: - $ref: "./securitySchemes/_index.yaml" \ No newline at end of file + Title: + type: object + additionalProperties: true + User: + type: object + properties: + id: + type: integer + format: int64 + description: Unique user ID (primary key) + example: 1 + avatar_id: + type: integer + format: int64 + description: ID of the user avatar (references images table) + nullable: true + example: null + mail: + type: string + format: email + description: User email + example: "john.doe@example.com" + nickname: + type: string + description: Username (alphanumeric + _ or -) + maxLength: 16 + example: "john_doe_42" + disp_name: + type: string + description: Display name + maxLength: 32 + example: "John Doe" + user_desc: + type: string + description: User description + maxLength: 512 + example: "Just a regular user." + creation_date: + type: string + format: date-time + description: Timestamp when the user was created + example: "2025-10-10T23:45:47.908073Z" + required: + - user_id + - nickname + - creation_date + UserTitle: + type: object + additionalProperties: true + Review: + type: object + additionalProperties: true + Tag: + type: object + additionalProperties: true diff --git a/api/parameters/_index.yaml b/api/parameters/_index.yaml deleted file mode 100644 index 6249e7d..0000000 --- a/api/parameters/_index.yaml +++ /dev/null @@ -1,4 +0,0 @@ -cursor: - $ref: "./cursor.yaml" -title_sort: - $ref: "./title_sort.yaml" \ No newline at end of file diff --git a/api/parameters/cursor.yaml b/api/parameters/cursor.yaml deleted file mode 100644 index 1730f18..0000000 --- a/api/parameters/cursor.yaml +++ /dev/null @@ -1,5 +0,0 @@ -in: query -name: cursor -required: false -schema: - type: string diff --git a/api/parameters/title_sort.yaml b/api/parameters/title_sort.yaml deleted file mode 100644 index 615294b..0000000 --- a/api/parameters/title_sort.yaml +++ /dev/null @@ -1,5 +0,0 @@ -in: query -name: sort -required: false -schema: - $ref: '../schemas/TitleSort.yaml' diff --git a/api/paths/media_upload.yaml b/api/paths/media_upload.yaml deleted file mode 100644 index 0453952..0000000 --- a/api/paths/media_upload.yaml +++ /dev/null @@ -1,37 +0,0 @@ -post: - summary: Upload an image (PNG, JPEG, or WebP) - description: | - Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**. - requestBody: - required: true - content: - multipart/form-data: - schema: - image: - type: string - format: binary - description: Image file (PNG, JPEG, or WebP) - encoding: - image: - contentType: image/png, image/jpeg, image/webp - - responses: - '200': - description: Image uploaded successfully - content: - application/json: - schema: - $ref: "../schemas/Image.yaml" - '400': - description: Bad request — e.g., invalid/malformed image, empty file - content: - application/json: - schema: - type: string - '415': - description: | - Unsupported Media Type — e.g., request `Content-Type` is not `multipart/form-data`, - or the `image` part has an unsupported `Content-Type` (not image/png, image/jpeg, or image/webp) - - '500': - description: Internal server error \ No newline at end of file diff --git a/api/paths/titles-id.yaml b/api/paths/titles-id.yaml deleted file mode 100644 index f1b9c55..0000000 --- a/api/paths/titles-id.yaml +++ /dev/null @@ -1,32 +0,0 @@ -get: - summary: Get title description - security: - - JwtAuthCookies: [] - operationId: getTitle - parameters: - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - - in: query - name: fields - schema: - type: string - default: all - responses: - '200': - description: Title description - content: - application/json: - schema: - $ref: "../schemas/Title.yaml" - '404': - description: Title not found - '400': - description: Request params are not correct - '500': - description: Unknown server error - '204': - description: No title found diff --git a/api/paths/titles.yaml b/api/paths/titles.yaml deleted file mode 100644 index 4288417..0000000 --- a/api/paths/titles.yaml +++ /dev/null @@ -1,83 +0,0 @@ -get: - summary: Get titles - parameters: - - $ref: "../parameters/cursor.yaml" - - $ref: "../parameters/title_sort.yaml" - - in: query - name: sort_forward - schema: - type: boolean - default: true - - in: query - name: ext_search - schema: - type: boolean - default: false - - in: query - name: word - schema: - type: string - - in: query - name: status - schema: - type: array - items: - $ref: '../schemas/enums/TitleStatus.yaml' - description: List of title statuses to filter - style: form - explode: false - - in: query - name: rating - schema: - type: number - format: double - - in: query - name: release_year - schema: - type: integer - format: int32 - - in: query - name: release_season - schema: - $ref: '../schemas/enums/ReleaseSeason.yaml' - - in: query - name: limit - schema: - type: integer - format: int32 - default: 10 - - in: query - name: offset - schema: - type: integer - format: int32 - default: 0 - - in: query - name: fields - schema: - type: string - default: all - responses: - '200': - description: List of titles with cursor - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '../schemas/Title.yaml' - description: List of titles - cursor: - $ref: '../schemas/CursorObj.yaml' - required: - - data - - cursor - '204': - description: No titles found - '400': - description: Request params are not correct - '500': - description: Unknown server error diff --git a/api/paths/users-id-titles-id.yaml b/api/paths/users-id-titles-id.yaml deleted file mode 100644 index 20a174f..0000000 --- a/api/paths/users-id-titles-id.yaml +++ /dev/null @@ -1,114 +0,0 @@ -get: - summary: Get user title - operationId: getUserTitle - parameters: - - in: path - name: user_id - required: true - schema: - type: integer - format: int64 - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: User titles - content: - application/json: - schema: - $ref: '../schemas/UserTitleMini.yaml' - '204': - description: No user title found - '400': - description: Request params are not correct - '404': - description: User or title not found - '500': - description: Unknown server error - -patch: - summary: Update a usertitle - description: User updating title list of watched - operationId: updateUserTitle - security: - - XsrfAuthHeader: [] - parameters: - - in: path - name: user_id - required: true - schema: - type: integer - format: int64 - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - status: - $ref: '../schemas/enums/UserTitleStatus.yaml' - rate: - type: integer - format: int32 - ftime: - type: string - format: date-time - responses: - '200': - description: Title successfully updated - content: - application/json: - schema: - $ref: '../schemas/UserTitleMini.yaml' - '400': - description: Invalid request body (missing fields, invalid types, etc.) - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to update title - '404': - description: User or Title not found - '500': - description: Internal server error - -delete: - summary: Delete a usertitle - description: User deleting title from list of watched - operationId: deleteUserTitle - security: - - XsrfAuthHeader: [] - parameters: - - in: path - name: user_id - required: true - schema: - type: integer - format: int64 - - in: path - name: title_id - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: Title successfully deleted - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to delete title - '404': - description: User or Title not found - '500': - description: Internal server error \ No newline at end of file diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml deleted file mode 100644 index f1e5e95..0000000 --- a/api/paths/users-id-titles.yaml +++ /dev/null @@ -1,146 +0,0 @@ -get: - summary: Get user titles - operationId: getUserTitles - parameters: - - $ref: '../parameters/cursor.yaml' - - $ref: "../parameters/title_sort.yaml" - - in: path - name: user_id - required: true - schema: - type: string - - in: query - name: sort_forward - schema: - type: boolean - default: true - - in: query - name: word - schema: - type: string - - in: query - name: status - schema: - type: array - items: - $ref: '../schemas/enums/TitleStatus.yaml' - description: List of title statuses to filter - style: form - explode: false - - in: query - name: watch_status - schema: - type: array - items: - $ref: '../schemas/enums/UserTitleStatus.yaml' - style: form - explode: false - - in: query - name: rating - schema: - type: number - format: double - - in: query - name: my_rate - schema: - type: integer - format: int32 - - in: query - name: release_year - schema: - type: integer - format: int32 - - in: query - name: release_season - schema: - $ref: '../schemas/enums/ReleaseSeason.yaml' - - in: query - name: limit - schema: - type: integer - format: int32 - default: 10 - - in: query - name: fields - schema: - type: string - default: all - responses: - '200': - description: List of user titles - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '../schemas/UserTitle.yaml' - cursor: - $ref: '../schemas/CursorObj.yaml' - required: - - data - - cursor - '204': - description: No titles found - '400': - description: Request params are not correct - '404': - description: User not found - '500': - description: Unknown server error - -post: - summary: Add a title to a user - description: User adding title to list af watched, status required - operationId: addUserTitle - parameters: - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - description: ID of the user to assign the title to - example: 123 - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - title_id - - status - properties: - title_id: - type: integer - format: int64 - status: - $ref: '../schemas/enums/UserTitleStatus.yaml' - rate: - type: integer - format: int32 - ftime: - type: string - format: date-time - responses: - '200': - description: Title successfully added to user - content: - application/json: - schema: - $ref: '../schemas/UserTitleMini.yaml' - '400': - description: Invalid request body (missing fields, invalid types, etc.) - '401': - description: Unauthorized — missing or invalid auth token - '403': - description: Forbidden — user not allowed to assign titles to this user - '404': - description: User or Title not found - '409': - description: Conflict — title already assigned to user (if applicable) - '500': - description: Internal server error \ No newline at end of file diff --git a/api/paths/users-id.yaml b/api/paths/users-id.yaml deleted file mode 100644 index 701df6b..0000000 --- a/api/paths/users-id.yaml +++ /dev/null @@ -1,106 +0,0 @@ -get: - summary: Get user info - operationId: getUsersId - parameters: - - in: path - name: user_id - required: true - schema: - type: string - - in: query - name: fields - schema: - type: string - default: all - responses: - '200': - description: User info - content: - application/json: - schema: - $ref: '../schemas/User.yaml' - '404': - description: User not found - '400': - description: Request params are not correct - '500': - description: Unknown server error - -patch: - summary: Partially update a user account - description: | - Update selected user profile fields (excluding password). - Password updates must be done via the dedicated auth-service (`/auth/`). - Fields not provided in the request body remain unchanged. - operationId: updateUser - security: - - XsrfAuthHeader: [] - parameters: - # - $ref: '../parameters/xsrf_token_header.yaml' - - name: user_id - in: path - required: true - schema: - type: integer - format: int64 - description: User ID (primary key) - example: 123 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - avatar_id: - type: integer - format: int64 - nullable: true - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar - example: 42 - mail: - type: string - format: email - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - description: User email (must be unique and valid) - example: john.doe.updated@example.com - nickname: - type: string - pattern: '^[a-zA-Z0-9_-]{3,16}$' - description: Username (alphanumeric + `_` or `-`, 3–16 chars) - maxLength: 16 - minLength: 3 - example: john_doe_43 - disp_name: - type: string - description: Display name - maxLength: 32 - example: John Smith - user_desc: - type: string - description: User description / bio - maxLength: 512 - example: Just a curious developer. - additionalProperties: false - description: Only provided fields are updated. Omitted fields remain unchanged. - responses: - '200': - description: User updated successfully. Returns updated user representation (excluding sensitive fields). - content: - application/json: - schema: - $ref: '../schemas/User.yaml' - '400': - description: Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON) - '401': - description: Unauthorized — missing or invalid authentication token - '403': - description: Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights) - '404': - description: User not found - '409': - description: Conflict — e.g., requested `nickname` or `mail` already taken by another user - '422': - description: Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`) - '500': - description: Unknown server error diff --git a/api/paths/users.yaml b/api/paths/users.yaml deleted file mode 100644 index 14fb0c0..0000000 --- a/api/paths/users.yaml +++ /dev/null @@ -1,46 +0,0 @@ -get: - summary: Search user by nickname or dispname (both in one param), response is always sorted by id - parameters: - - in: query - name: word - schema: - type: string - - in: query - name: limit - schema: - type: integer - format: int32 - default: 10 - - in: query - name: cursor_id - description: pass cursor naked - schema: - type: integer - format: int32 - default: 1 - responses: - '200': - description: List of users with cursor - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: '../schemas/User.yaml' - description: List of users - cursor: - type: integer - format: int64 - default: 1 - required: - - data - - cursor - '204': - description: No users found - '400': - description: Request params are not correct - '500': - description: Unknown server error diff --git a/api/schemas/CursorObj.yaml b/api/schemas/CursorObj.yaml deleted file mode 100644 index 5a3c96c..0000000 --- a/api/schemas/CursorObj.yaml +++ /dev/null @@ -1,9 +0,0 @@ -type: object -required: - - id -properties: - id: - type: integer - format: int64 - param: - type: string diff --git a/api/schemas/Image.yaml b/api/schemas/Image.yaml deleted file mode 100644 index 3fb520b..0000000 --- a/api/schemas/Image.yaml +++ /dev/null @@ -1,10 +0,0 @@ -type: object -properties: -# id выпиливаем - id: - type: integer - format: int64 - storage_type: - $ref: './enums/StorageType.yaml' - image_path: - type: string diff --git a/api/schemas/JWTAuth.yaml b/api/schemas/JWTAuth.yaml deleted file mode 100644 index 63c3baa..0000000 --- a/api/schemas/JWTAuth.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# type: apiKey -# in: cookie -# name: access_token -# scheme: bearer -# bearerFormat: JWT -# description: | -# JWT access token sent in `Cookie: access_token=...`. \ No newline at end of file diff --git a/api/schemas/Review.yaml b/api/schemas/Review.yaml deleted file mode 100644 index a116dde..0000000 --- a/api/schemas/Review.yaml +++ /dev/null @@ -1,2 +0,0 @@ -type: object -additionalProperties: true diff --git a/api/schemas/Studio.yaml b/api/schemas/Studio.yaml deleted file mode 100644 index 26a2adf..0000000 --- a/api/schemas/Studio.yaml +++ /dev/null @@ -1,15 +0,0 @@ -type: object -required: - - id - - name -properties: -# id не нужен - id: - type: integer - format: int64 - name: - type: string - poster: - $ref: ./Image.yaml - description: - type: string diff --git a/api/schemas/Tag.yaml b/api/schemas/Tag.yaml deleted file mode 100644 index 7239b10..0000000 --- a/api/schemas/Tag.yaml +++ /dev/null @@ -1,8 +0,0 @@ -type: object -description: 'A localized tag: keys are language codes (ISO 639-1), values are tag names' -additionalProperties: - type: string -example: - en: Shojo - ru: Сёдзё - ja: 少女 diff --git a/api/schemas/Tags.yaml b/api/schemas/Tags.yaml deleted file mode 100644 index ca8c4fd..0000000 --- a/api/schemas/Tags.yaml +++ /dev/null @@ -1,11 +0,0 @@ -type: array -description: Array of localized tags -items: - $ref: ./Tag.yaml -example: -- en: Shojo - ru: Сёдзё - ja: 少女 -- en: Shounen - ru: Сёнен - ja: 少年 diff --git a/api/schemas/Title.yaml b/api/schemas/Title.yaml deleted file mode 100644 index fac4a3f..0000000 --- a/api/schemas/Title.yaml +++ /dev/null @@ -1,67 +0,0 @@ -type: object -required: - - id - - title_names - - tags -properties: - id: - type: integer - format: int64 - description: Unique title ID (primary key) - example: 1 - title_names: - type: object - description: Localized titles. Key = language (ISO 639-1), value = list of names - additionalProperties: - type: array - items: - type: string - example: Attack on Titan - minItems: 1 - example: - - Attack on Titan - - AoT - example: - en: - - Attack on Titan - - AoT - ru: - - Атака титанов - - Титаны - ja: - - 進撃の巨人 - title_desc: - type: object - description: Localized description. Key = language (ISO 639-1), value = description. - additionalProperties: - type: string - studio: - $ref: ./Studio.yaml - tags: - $ref: ./Tags.yaml - poster: - $ref: ./Image.yaml - title_status: - $ref: ./enums/TitleStatus.yaml - rating: - type: number - format: double - rating_count: - type: integer - format: int32 - release_year: - type: integer - format: int32 - release_season: - $ref: ./enums/ReleaseSeason.yaml - episodes_aired: - type: integer - format: int32 - episodes_all: - type: integer - format: int32 - episodes_len: - type: object - additionalProperties: - type: number - format: double diff --git a/api/schemas/TitleSort.yaml b/api/schemas/TitleSort.yaml deleted file mode 100644 index d8ce8f7..0000000 --- a/api/schemas/TitleSort.yaml +++ /dev/null @@ -1,8 +0,0 @@ -type: string -description: Title sort order -default: id -enum: - - id - - year - - rating - - views diff --git a/api/schemas/User.yaml b/api/schemas/User.yaml deleted file mode 100644 index 4e53534..0000000 --- a/api/schemas/User.yaml +++ /dev/null @@ -1,37 +0,0 @@ -type: object -properties: - id: - type: integer - format: int64 - description: Unique user ID (primary key) - example: 1 - image: - $ref: '../schemas/Image.yaml' - mail: - type: string - format: email - description: User email - example: john.doe@example.com - nickname: - type: string - description: Username (alphanumeric + _ or -) - maxLength: 16 - example: john_doe_42 - disp_name: - type: string - description: Display name - maxLength: 32 - example: John Doe - user_desc: - type: string - description: User description - maxLength: 512 - example: Just a regular user. - creation_date: - type: string - format: date-time - description: Timestamp when the user was created - example: '2025-10-10T23:45:47.908073Z' -required: - - user_id - - nickname diff --git a/api/schemas/UserTitle.yaml b/api/schemas/UserTitle.yaml deleted file mode 100644 index ef619cb..0000000 --- a/api/schemas/UserTitle.yaml +++ /dev/null @@ -1,22 +0,0 @@ -type: object -required: - - user_id - - title_id - - status -properties: - user_id: - type: integer - format: int64 - title: - $ref: ./Title.yaml - status: - $ref: ./enums/UserTitleStatus.yaml - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time diff --git a/api/schemas/UserTitleMini.yaml b/api/schemas/UserTitleMini.yaml deleted file mode 100644 index e1a5a74..0000000 --- a/api/schemas/UserTitleMini.yaml +++ /dev/null @@ -1,23 +0,0 @@ -type: object -required: - - user_id - - title_id - - status -properties: - user_id: - type: integer - format: int64 - title_id: - type: integer - format: int64 - status: - $ref: ./enums/UserTitleStatus.yaml - rate: - type: integer - format: int32 - review_id: - type: integer - format: int64 - ctime: - type: string - format: date-time diff --git a/api/schemas/_index.yaml b/api/schemas/_index.yaml deleted file mode 100644 index 0cc0f9d..0000000 --- a/api/schemas/_index.yaml +++ /dev/null @@ -1,28 +0,0 @@ -CursorObj: - $ref: "./CursorObj.yaml" -TitleSort: - $ref: "./TitleSort.yaml" -Image: - $ref: "./Image.yaml" -TitleStatus: - $ref: "./enums/TitleStatus.yaml" -ReleaseSeason: - $ref: "./enums/ReleaseSeason.yaml" -UserTitleStatus: - $ref: "./enums/UserTitleStatus.yaml" -Review: - $ref: "./Review.yaml" -Tag: - $ref: "./Tag.yaml" -Tags: - $ref: "./Tags.yaml" -Studio: - $ref: "./Studio.yaml" -Title: - $ref: "./Title.yaml" -User: - $ref: "./User.yaml" -UserTitle: - $ref: "./UserTitle.yaml" -# JwtAuth: -# $ref: "./JWTAuth.yaml" diff --git a/api/schemas/enums/ReleaseSeason.yaml b/api/schemas/enums/ReleaseSeason.yaml deleted file mode 100644 index 5cf988d..0000000 --- a/api/schemas/enums/ReleaseSeason.yaml +++ /dev/null @@ -1,7 +0,0 @@ -type: string -description: Title release season -enum: - - winter - - spring - - summer - - fall diff --git a/api/schemas/enums/StorageType.yaml b/api/schemas/enums/StorageType.yaml deleted file mode 100644 index 1984a87..0000000 --- a/api/schemas/enums/StorageType.yaml +++ /dev/null @@ -1,5 +0,0 @@ -type: string -description: Image storage type -enum: - - s3 - - local \ No newline at end of file diff --git a/api/schemas/enums/TitleStatus.yaml b/api/schemas/enums/TitleStatus.yaml deleted file mode 100644 index 0bfce7d..0000000 --- a/api/schemas/enums/TitleStatus.yaml +++ /dev/null @@ -1,6 +0,0 @@ -type: string -description: Title status -enum: - - finished - - ongoing - - planned diff --git a/api/schemas/enums/UserTitleStatus.yaml b/api/schemas/enums/UserTitleStatus.yaml deleted file mode 100644 index 0b1a90d..0000000 --- a/api/schemas/enums/UserTitleStatus.yaml +++ /dev/null @@ -1,7 +0,0 @@ -type: string -description: User's title status -enum: - - finished - - planned - - dropped - - in-progress diff --git a/api/schemas/updateUser.yaml b/api/schemas/updateUser.yaml deleted file mode 100644 index e1d77af..0000000 --- a/api/schemas/updateUser.yaml +++ /dev/null @@ -1,26 +0,0 @@ -type: object -properties: - avatar_id: - type: integer - format: int64 - nullable: true - description: ID of the user avatar (references `images.id`); set to `null` to remove avatar - example: 42 - mail: - type: string - format: email - pattern: '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\\.[a-zA-Z0-9_-]+$' - description: User email (must be unique and valid) - example: john.doe.updated@example.com - disp_name: - type: string - description: Display name - maxLength: 32 - example: John Smith - user_desc: - type: string - description: User description / bio - maxLength: 512 - example: Just a curious developer. -additionalProperties: false -description: Only provided fields are updated. Omitted fields remain unchanged. \ No newline at end of file diff --git a/api/securitySchemes/_index.yaml b/api/securitySchemes/_index.yaml deleted file mode 100644 index ecc0ff6..0000000 --- a/api/securitySchemes/_index.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# accessToken: -# $ref: "./access_token.yaml" -# csrfToken: -# $ref: "./xsrf_token_cookie.yaml" -XsrfAuthHeader: - type: apiKey - in: header - name: X-XSRF-TOKEN - description: | - Anti-CSRF token. Must match the `XSRF-TOKEN` cookie. - Required for all state-changing requests (POST/PUT/PATCH/DELETE). \ No newline at end of file diff --git a/auth/auth.gen.go b/auth/auth.gen.go index 1e8803e..1f16575 100644 --- a/auth/auth.gen.go +++ b/auth/auth.gen.go @@ -13,55 +13,44 @@ import ( strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin" ) -const ( - BearerAuthScopes = "bearerAuth.Scopes" -) - -// GetImpersonationTokenJSONBody defines parameters for GetImpersonationToken. -type GetImpersonationTokenJSONBody struct { - ExternalId *int64 `json:"external_id,omitempty"` - UserId *int64 `json:"user_id,omitempty"` - union json.RawMessage -} - -// GetImpersonationTokenJSONBody0 defines parameters for GetImpersonationToken. -type GetImpersonationTokenJSONBody0 = interface{} - -// GetImpersonationTokenJSONBody1 defines parameters for GetImpersonationToken. -type GetImpersonationTokenJSONBody1 = interface{} - -// PostSignInJSONBody defines parameters for PostSignIn. -type PostSignInJSONBody struct { +// PostAuthSignInJSONBody defines parameters for PostAuthSignIn. +type PostAuthSignInJSONBody struct { Nickname string `json:"nickname"` Pass string `json:"pass"` } -// PostSignUpJSONBody defines parameters for PostSignUp. -type PostSignUpJSONBody struct { +// PostAuthSignUpJSONBody defines parameters for PostAuthSignUp. +type PostAuthSignUpJSONBody struct { Nickname string `json:"nickname"` Pass string `json:"pass"` } -// GetImpersonationTokenJSONRequestBody defines body for GetImpersonationToken for application/json ContentType. -type GetImpersonationTokenJSONRequestBody GetImpersonationTokenJSONBody +// PostAuthVerifyTokenJSONBody defines parameters for PostAuthVerifyToken. +type PostAuthVerifyTokenJSONBody struct { + // Token JWT token to validate + Token string `json:"token"` +} -// PostSignInJSONRequestBody defines body for PostSignIn for application/json ContentType. -type PostSignInJSONRequestBody PostSignInJSONBody +// PostAuthSignInJSONRequestBody defines body for PostAuthSignIn for application/json ContentType. +type PostAuthSignInJSONRequestBody PostAuthSignInJSONBody -// PostSignUpJSONRequestBody defines body for PostSignUp for application/json ContentType. -type PostSignUpJSONRequestBody PostSignUpJSONBody +// PostAuthSignUpJSONRequestBody defines body for PostAuthSignUp for application/json ContentType. +type PostAuthSignUpJSONRequestBody PostAuthSignUpJSONBody + +// PostAuthVerifyTokenJSONRequestBody defines body for PostAuthVerifyToken for application/json ContentType. +type PostAuthVerifyTokenJSONRequestBody PostAuthVerifyTokenJSONBody // ServerInterface represents all server handlers. type ServerInterface interface { - // Get service impersontaion token - // (POST /get-impersonation-token) - GetImpersonationToken(c *gin.Context) // Sign in a user and return JWT - // (POST /sign-in) - PostSignIn(c *gin.Context) + // (POST /auth/sign-in) + PostAuthSignIn(c *gin.Context) // Sign up a new user - // (POST /sign-up) - PostSignUp(c *gin.Context) + // (POST /auth/sign-up) + PostAuthSignUp(c *gin.Context) + // Verify JWT validity + // (POST /auth/verify-token) + PostAuthVerifyToken(c *gin.Context) } // ServerInterfaceWrapper converts contexts to parameters. @@ -73,10 +62,8 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) -// GetImpersonationToken operation middleware -func (siw *ServerInterfaceWrapper) GetImpersonationToken(c *gin.Context) { - - c.Set(BearerAuthScopes, []string{}) +// PostAuthSignIn operation middleware +func (siw *ServerInterfaceWrapper) PostAuthSignIn(c *gin.Context) { for _, middleware := range siw.HandlerMiddlewares { middleware(c) @@ -85,11 +72,11 @@ func (siw *ServerInterfaceWrapper) GetImpersonationToken(c *gin.Context) { } } - siw.Handler.GetImpersonationToken(c) + siw.Handler.PostAuthSignIn(c) } -// PostSignIn operation middleware -func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) { +// PostAuthSignUp operation middleware +func (siw *ServerInterfaceWrapper) PostAuthSignUp(c *gin.Context) { for _, middleware := range siw.HandlerMiddlewares { middleware(c) @@ -98,11 +85,11 @@ func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) { } } - siw.Handler.PostSignIn(c) + siw.Handler.PostAuthSignUp(c) } -// PostSignUp operation middleware -func (siw *ServerInterfaceWrapper) PostSignUp(c *gin.Context) { +// PostAuthVerifyToken operation middleware +func (siw *ServerInterfaceWrapper) PostAuthVerifyToken(c *gin.Context) { for _, middleware := range siw.HandlerMiddlewares { middleware(c) @@ -111,7 +98,7 @@ func (siw *ServerInterfaceWrapper) PostSignUp(c *gin.Context) { } } - siw.Handler.PostSignUp(c) + siw.Handler.PostAuthVerifyToken(c) } // GinServerOptions provides options for the Gin server. @@ -141,81 +128,76 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } - router.POST(options.BaseURL+"/get-impersonation-token", wrapper.GetImpersonationToken) - router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn) - router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp) + router.POST(options.BaseURL+"/auth/sign-in", wrapper.PostAuthSignIn) + router.POST(options.BaseURL+"/auth/sign-up", wrapper.PostAuthSignUp) + router.POST(options.BaseURL+"/auth/verify-token", wrapper.PostAuthVerifyToken) } -type UnauthorizedErrorResponse struct { +type PostAuthSignInRequestObject struct { + Body *PostAuthSignInJSONRequestBody } -type GetImpersonationTokenRequestObject struct { - Body *GetImpersonationTokenJSONRequestBody +type PostAuthSignInResponseObject interface { + VisitPostAuthSignInResponse(w http.ResponseWriter) error } -type GetImpersonationTokenResponseObject interface { - VisitGetImpersonationTokenResponse(w http.ResponseWriter) error +type PostAuthSignIn200JSONResponse struct { + Error *string `json:"error"` + Success *bool `json:"success,omitempty"` + + // Token JWT token to access protected endpoints + Token *string `json:"token"` + UserId *string `json:"user_id"` } -type GetImpersonationToken200JSONResponse struct { - // AccessToken JWT access token - AccessToken string `json:"access_token"` -} - -func (response GetImpersonationToken200JSONResponse) VisitGetImpersonationTokenResponse(w http.ResponseWriter) error { +func (response PostAuthSignIn200JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type GetImpersonationToken401Response = UnauthorizedErrorResponse - -func (response GetImpersonationToken401Response) VisitGetImpersonationTokenResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil +type PostAuthSignUpRequestObject struct { + Body *PostAuthSignUpJSONRequestBody } -type PostSignInRequestObject struct { - Body *PostSignInJSONRequestBody +type PostAuthSignUpResponseObject interface { + VisitPostAuthSignUpResponse(w http.ResponseWriter) error } -type PostSignInResponseObject interface { - VisitPostSignInResponse(w http.ResponseWriter) error +type PostAuthSignUp200JSONResponse struct { + Error *string `json:"error"` + Success *bool `json:"success,omitempty"` + UserId *string `json:"user_id"` } -type PostSignIn200JSONResponse struct { - UserId int64 `json:"user_id"` - UserName string `json:"user_name"` -} - -func (response PostSignIn200JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error { +func (response PostAuthSignUp200JSONResponse) VisitPostAuthSignUpResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type PostSignIn401Response = UnauthorizedErrorResponse - -func (response PostSignIn401Response) VisitPostSignInResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil +type PostAuthVerifyTokenRequestObject struct { + Body *PostAuthVerifyTokenJSONRequestBody } -type PostSignUpRequestObject struct { - Body *PostSignUpJSONRequestBody +type PostAuthVerifyTokenResponseObject interface { + VisitPostAuthVerifyTokenResponse(w http.ResponseWriter) error } -type PostSignUpResponseObject interface { - VisitPostSignUpResponse(w http.ResponseWriter) error +type PostAuthVerifyToken200JSONResponse struct { + // Error Error message if token is invalid + Error *string `json:"error"` + + // UserId User ID extracted from token if valid + UserId *string `json:"user_id"` + + // Valid True if token is valid + Valid *bool `json:"valid,omitempty"` } -type PostSignUp200JSONResponse struct { - UserId int64 `json:"user_id"` -} - -func (response PostSignUp200JSONResponse) VisitPostSignUpResponse(w http.ResponseWriter) error { +func (response PostAuthVerifyToken200JSONResponse) VisitPostAuthVerifyTokenResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) @@ -224,15 +206,15 @@ func (response PostSignUp200JSONResponse) VisitPostSignUpResponse(w http.Respons // StrictServerInterface represents all server handlers. type StrictServerInterface interface { - // Get service impersontaion token - // (POST /get-impersonation-token) - GetImpersonationToken(ctx context.Context, request GetImpersonationTokenRequestObject) (GetImpersonationTokenResponseObject, error) // Sign in a user and return JWT - // (POST /sign-in) - PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error) + // (POST /auth/sign-in) + PostAuthSignIn(ctx context.Context, request PostAuthSignInRequestObject) (PostAuthSignInResponseObject, error) // Sign up a new user - // (POST /sign-up) - PostSignUp(ctx context.Context, request PostSignUpRequestObject) (PostSignUpResponseObject, error) + // (POST /auth/sign-up) + PostAuthSignUp(ctx context.Context, request PostAuthSignUpRequestObject) (PostAuthSignUpResponseObject, error) + // Verify JWT validity + // (POST /auth/verify-token) + PostAuthVerifyToken(ctx context.Context, request PostAuthVerifyTokenRequestObject) (PostAuthVerifyTokenResponseObject, error) } type StrictHandlerFunc = strictgin.StrictGinHandlerFunc @@ -247,11 +229,11 @@ type strictHandler struct { middlewares []StrictMiddlewareFunc } -// GetImpersonationToken operation middleware -func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) { - var request GetImpersonationTokenRequestObject +// PostAuthSignIn operation middleware +func (sh *strictHandler) PostAuthSignIn(ctx *gin.Context) { + var request PostAuthSignInRequestObject - var body GetImpersonationTokenJSONRequestBody + var body PostAuthSignInJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { ctx.Status(http.StatusBadRequest) ctx.Error(err) @@ -260,10 +242,10 @@ func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) { request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.GetImpersonationToken(ctx, request.(GetImpersonationTokenRequestObject)) + return sh.ssi.PostAuthSignIn(ctx, request.(PostAuthSignInRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetImpersonationToken") + handler = middleware(handler, "PostAuthSignIn") } response, err := handler(ctx, request) @@ -271,8 +253,8 @@ func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) { if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(GetImpersonationTokenResponseObject); ok { - if err := validResponse.VisitGetImpersonationTokenResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(PostAuthSignInResponseObject); ok { + if err := validResponse.VisitPostAuthSignInResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { @@ -280,11 +262,11 @@ func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) { } } -// PostSignIn operation middleware -func (sh *strictHandler) PostSignIn(ctx *gin.Context) { - var request PostSignInRequestObject +// PostAuthSignUp operation middleware +func (sh *strictHandler) PostAuthSignUp(ctx *gin.Context) { + var request PostAuthSignUpRequestObject - var body PostSignInJSONRequestBody + var body PostAuthSignUpJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { ctx.Status(http.StatusBadRequest) ctx.Error(err) @@ -293,10 +275,10 @@ func (sh *strictHandler) PostSignIn(ctx *gin.Context) { request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.PostSignIn(ctx, request.(PostSignInRequestObject)) + return sh.ssi.PostAuthSignUp(ctx, request.(PostAuthSignUpRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostSignIn") + handler = middleware(handler, "PostAuthSignUp") } response, err := handler(ctx, request) @@ -304,8 +286,8 @@ func (sh *strictHandler) PostSignIn(ctx *gin.Context) { if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostSignInResponseObject); ok { - if err := validResponse.VisitPostSignInResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(PostAuthSignUpResponseObject); ok { + if err := validResponse.VisitPostAuthSignUpResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { @@ -313,11 +295,11 @@ func (sh *strictHandler) PostSignIn(ctx *gin.Context) { } } -// PostSignUp operation middleware -func (sh *strictHandler) PostSignUp(ctx *gin.Context) { - var request PostSignUpRequestObject +// PostAuthVerifyToken operation middleware +func (sh *strictHandler) PostAuthVerifyToken(ctx *gin.Context) { + var request PostAuthVerifyTokenRequestObject - var body PostSignUpJSONRequestBody + var body PostAuthVerifyTokenJSONRequestBody if err := ctx.ShouldBindJSON(&body); err != nil { ctx.Status(http.StatusBadRequest) ctx.Error(err) @@ -326,10 +308,10 @@ func (sh *strictHandler) PostSignUp(ctx *gin.Context) { request.Body = &body handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.PostSignUp(ctx, request.(PostSignUpRequestObject)) + return sh.ssi.PostAuthVerifyToken(ctx, request.(PostAuthVerifyTokenRequestObject)) } for _, middleware := range sh.middlewares { - handler = middleware(handler, "PostSignUp") + handler = middleware(handler, "PostAuthVerifyToken") } response, err := handler(ctx, request) @@ -337,8 +319,8 @@ func (sh *strictHandler) PostSignUp(ctx *gin.Context) { if err != nil { ctx.Error(err) ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(PostSignUpResponseObject); ok { - if err := validResponse.VisitPostSignUpResponse(ctx.Writer); err != nil { + } else if validResponse, ok := response.(PostAuthVerifyTokenResponseObject); ok { + if err := validResponse.VisitPostAuthVerifyTokenResponse(ctx.Writer); err != nil { ctx.Error(err) } } else if response != nil { diff --git a/auth/openapi-auth.yaml b/auth/openapi-auth.yaml index 803a4ae..7ffc60e 100644 --- a/auth/openapi-auth.yaml +++ b/auth/openapi-auth.yaml @@ -1,16 +1,12 @@ -openapi: 3.1.1 +openapi: 3.1.0 info: title: Auth Service version: 1.0.0 -servers: - - url: /auth - paths: - /sign-up: + /auth/sign-up: post: summary: Sign up a new user - operationId: postSignUp tags: [Auth] requestBody: required: true @@ -31,18 +27,20 @@ paths: content: application/json: schema: - required: - - user_id type: object properties: + success: + type: boolean + error: + type: string + nullable: true user_id: - type: integer - format: int64 + type: string + nullable: true - /sign-in: + /auth/sign-in: post: summary: Sign in a user and return JWT - operationId: postSignIn tags: [Auth] requestBody: required: true @@ -58,69 +56,57 @@ paths: type: string format: password responses: - # This one also sets two cookies: access_token and refresh_token "200": description: Sign-in result with JWT content: application/json: schema: - required: - - user_id - - user_name type: object properties: - user_id: - type: integer - format: int64 - user_name: + success: + type: boolean + error: type: string - "401": - $ref: '#/components/responses/UnauthorizedError' + nullable: true + user_id: + type: string + nullable: true + token: + type: string + description: JWT token to access protected endpoints + nullable: true - /get-impersonation-token: + /auth/verify-token: post: - summary: Get service impersontaion token - operationId: getImpersonationToken + summary: Verify JWT validity tags: [Auth] - security: - - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object + required: [token] properties: - user_id: - type: integer - format: int64 - external_id: - type: integer - format: int64 - oneOf: - - required: ["user_id"] - - required: ["external_id"] + token: + type: string + description: JWT token to validate responses: "200": - description: Generated impersonation access token + description: Token validation result content: application/json: schema: type: object - required: - - access_token properties: - access_token: + valid: + type: boolean + description: True if token is valid + user_id: type: string - description: JWT access token - "401": - $ref: '#/components/responses/UnauthorizedError' - -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - responses: - UnauthorizedError: - description: Access token is missing or invalid \ No newline at end of file + nullable: true + description: User ID extracted from token if valid + error: + type: string + nullable: true + description: Error message if token is invalid \ No newline at end of file diff --git a/deploy/api_gen.ps1 b/deploy/api_gen.ps1 deleted file mode 100644 index c8966b7..0000000 --- a/deploy/api_gen.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -cd ./api -openapi-format .\openapi.yaml --output .\_build\openapi.yaml --yaml -cd .. -oapi-codegen --config=api\oapi-codegen.yaml api\_build\openapi.yaml diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 3eff3d3..1a96253 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -11,50 +11,20 @@ services: - "${POSTGRES_PORT}:5432" volumes: - postgres_data:/var/lib/postgresql - networks: - - nyanimedb-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - rabbitmq: - image: rabbitmq:3-management - container_name: rabbitmq - ports: - - "5672:5672" - - "15672:15672" - environment: - RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} - RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} - volumes: - - rabbitmq_data:/var/lib/rabbitmq - networks: - - nyanimedb-network - healthcheck: - test: ["CMD", "rabbitmqctl", "status"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - - redis: - image: redis:8.4.0-alpine - container_name: redis - ports: - - "6379:6379" - restart: always - command: ["redis-server", "--appendonly", "yes"] - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 5s - volumes: - - redis_data:/data + # pgadmin: + # image: dpage/pgadmin4:${PGADMIN_VERSION} + # container_name: pgadmin + # restart: always + # environment: + # PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL} + # PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} + # ports: + # - "${PGADMIN_PORT}:80" + # depends_on: + # - postgres + # volumes: + # - pgadmin_data:/var/lib/pgadmin nyanimedb-backend: image: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest @@ -63,33 +33,10 @@ services: environment: LOG_LEVEL: ${LOG_LEVEL} DATABASE_URL: ${DATABASE_URL} - SERVICE_ADDRESS: ${SERVICE_ADDRESS} - RABBITMQ_URL: ${RABBITMQ_URL} - JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY} - AUTH_ENABLED: ${AUTH_ENABLED} - # ports: - # - "8080:8080" + ports: + - "8080:8080" depends_on: - postgres - - rabbitmq - networks: - - nyanimedb-network - - nyanimedb-auth: - image: meowgit.nekoea.red/nihonium/nyanimedb-auth:latest - container_name: nyanimedb-auth - restart: always - environment: - LOG_LEVEL: ${LOG_LEVEL} - DATABASE_URL: ${DATABASE_URL} - SERVICE_ADDRESS: ${SERVICE_ADDRESS} - JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY} - # ports: - # - "8082:8082" - depends_on: - - postgres - networks: - - nyanimedb-network nyanimedb-frontend: image: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest @@ -99,13 +46,7 @@ services: - "8081:80" depends_on: - nyanimedb-backend - networks: - - nyanimedb-network volumes: postgres_data: - rabbitmq_data: - redis_data: - -networks: - nyanimedb-network: + pgadmin_data: diff --git a/deploy/generate.sh b/deploy/generate.sh index 29587cf..d7d94a2 100644 --- a/deploy/generate.sh +++ b/deploy/generate.sh @@ -1,3 +1,3 @@ -npx openapi-typescript-codegen --input ..\..\api\openapi.yaml --output ./src/api --client axios --useUnionTypes +npx openapi-typescript-codegen --input ..\..\api\openapi.yaml --output ./src/api --client axios oapi-codegen --config=api/oapi-codegen.yaml .\api\openapi.yaml sqlc generate -f .\sql\sqlc.yaml \ No newline at end of file diff --git a/go.mod b/go.mod index 08a3dc1..4089c02 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,12 @@ module nyanimedb go 1.25.0 require ( - github.com/alexedwards/argon2id v1.0.0 github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.11.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/jackc/pgx/v5 v5.7.6 github.com/oapi-codegen/runtime v1.1.2 github.com/pelletier/go-toml/v2 v2.2.4 - github.com/sirupsen/logrus v1.9.3 ) require ( @@ -18,7 +16,6 @@ require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/disintegration/imaging v1.6.2 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -29,7 +26,6 @@ require ( github.com/google/uuid v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -38,18 +34,16 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect - github.com/rabbitmq/amqp091-go v1.10.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/image v0.33.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index dc41797..d8c4265 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w= -github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= @@ -13,8 +11,6 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= -github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= @@ -74,13 +70,7 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= -github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= -github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= -github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -95,82 +85,25 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= -golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go index 3af44f3..ca72192 100644 --- a/modules/auth/handlers/handlers.go +++ b/modules/auth/handlers/handlers.go @@ -2,28 +2,35 @@ package handlers import ( "context" - "crypto/rand" - "encoding/base64" "fmt" - "net/http" auth "nyanimedb/auth" sqlc "nyanimedb/sql" "strconv" "time" - "github.com/alexedwards/argon2id" - "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" - log "github.com/sirupsen/logrus" ) -type Server struct { - db *sqlc.Queries - JwtPrivateKey string +var secretKey = []byte("my_secret_key") + +func generateToken(userID string) (string, error) { + claims := jwt.MapClaims{ + "user_id": userID, + "exp": time.Now().Add(time.Hour * 24).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(secretKey) } -func NewServer(db *sqlc.Queries, JwtPrivatekey string) Server { - return Server{db: db, JwtPrivateKey: JwtPrivatekey} +var UserDb = make(map[string]string) //TEMP + +type Server struct { + db *sqlc.Queries +} + +func NewServer(db *sqlc.Queries) Server { + return Server{db: db} } func parseInt64(s string) (int32, error) { @@ -31,249 +38,71 @@ func parseInt64(s string) (int32, error) { return int32(i), err } -func HashPassword(password string) (string, error) { - params := &argon2id.Params{ - Memory: 64 * 1024, - Iterations: 3, - Parallelism: 2, - SaltLength: 16, - KeyLength: 32, - } +func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInRequestObject) (auth.PostAuthSignInResponseObject, error) { + err := "" + success := true + t, _ := generateToken(req.Body.Nickname) - return argon2id.CreateHash(password, params) -} + UserDb[req.Body.Nickname] = req.Body.Pass -func CheckPassword(password, hash string) (bool, error) { - return argon2id.ComparePasswordAndHash(password, hash) -} - -func (s Server) generateImpersonationToken(userID string, impersonated_by string) (accessToken string, err error) { - accessClaims := jwt.MapClaims{ - "user_id": userID, - "exp": time.Now().Add(15 * time.Minute).Unix(), - "imp_id": impersonated_by, - } - - at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) - - accessToken, err = at.SignedString([]byte(s.JwtPrivateKey)) - if err != nil { - return "", err - } - - return accessToken, nil -} - -func (s Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) { - accessClaims := jwt.MapClaims{ - "user_id": userID, - "exp": time.Now().Add(15 * time.Minute).Unix(), - //TODO: add created_at - } - at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) - accessToken, err = at.SignedString([]byte(s.JwtPrivateKey)) - if err != nil { - return "", "", "", err - } - - refreshClaims := jwt.MapClaims{ - "user_id": userID, - "exp": time.Now().Add(7 * 24 * time.Hour).Unix(), - } - rt := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) - refreshToken, err = rt.SignedString([]byte(s.JwtPrivateKey)) - if err != nil { - return "", "", "", err - } - - csrfBytes := make([]byte, 32) - _, err = rand.Read(csrfBytes) - if err != nil { - return "", "", "", err - } - csrfToken = base64.RawURLEncoding.EncodeToString(csrfBytes) - - return accessToken, refreshToken, csrfToken, nil -} - -func (s Server) PostSignUp(ctx context.Context, req auth.PostSignUpRequestObject) (auth.PostSignUpResponseObject, error) { - passhash, err := HashPassword(req.Body.Pass) - if err != nil { - log.Errorf("failed to hash password: %v", err) - // TODO: return 500 - } - - user_id, err := s.db.CreateNewUser(context.Background(), sqlc.CreateNewUserParams{ - Passhash: passhash, - Nickname: req.Body.Nickname, - }) - if err != nil { - log.Errorf("failed to create user %s: %v", req.Body.Nickname, err) - // TODO: check err and retyrn 400/500 - } - - return auth.PostSignUp200JSONResponse{ - UserId: user_id, + return auth.PostAuthSignIn200JSONResponse{ + Error: &err, + Success: &success, + UserId: &req.Body.Nickname, + Token: &t, }, nil } -func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject) (auth.PostSignInResponseObject, error) { - ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) - if !ok { - log.Print("failed to get gin context") - // TODO: change to 500 - return auth.PostSignIn200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") - } +func (s Server) PostAuthSignUp(ctx context.Context, req auth.PostAuthSignUpRequestObject) (auth.PostAuthSignUpResponseObject, error) { + err := "" + success := true + UserDb[req.Body.Nickname] = req.Body.Pass - user, err := s.db.GetUserByNickname(context.Background(), req.Body.Nickname) - if err != nil { - log.Errorf("failed to get user by nickname %s: %v", req.Body.Nickname, err) - // TODO: return 400/500 - } - - ok, err = CheckPassword(req.Body.Pass, user.Passhash) - if err != nil { - log.Errorf("failed to check password for user %s: %v", req.Body.Nickname, err) - // TODO: return 500 - } - if !ok { - return auth.PostSignIn401Response{}, nil - } - - accessToken, refreshToken, csrfToken, err := s.generateTokens(req.Body.Nickname) - if err != nil { - log.Errorf("failed to generate tokens for user %s: %v", req.Body.Nickname, err) - // TODO: return 500 - } - - // TODO: check cookie settings carefully - ginCtx.SetSameSite(http.SameSiteStrictMode) - ginCtx.SetCookie("access_token", accessToken, 900, "/api", "", false, true) - ginCtx.SetCookie("refresh_token", refreshToken, 1209600, "/auth", "", false, true) - ginCtx.SetCookie("xsrf_token", csrfToken, 1209600, "/", "", false, false) - - result := auth.PostSignIn200JSONResponse{ - UserId: user.ID, - UserName: user.Nickname, - } - return result, nil + return auth.PostAuthSignUp200JSONResponse{ + Error: &err, + Success: &success, + UserId: &req.Body.Nickname, + }, nil } -func (s Server) GetImpersonationToken(ctx context.Context, req auth.GetImpersonationTokenRequestObject) (auth.GetImpersonationTokenResponseObject, error) { - ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) - if !ok { - log.Print("failed to get gin context") - // TODO: change to 500 - return auth.GetImpersonationToken200JSONResponse{}, fmt.Errorf("failed to get gin.Context from context.Context") - } +func (s Server) PostAuthVerifyToken(ctx context.Context, req auth.PostAuthVerifyTokenRequestObject) (auth.PostAuthVerifyTokenResponseObject, error) { + valid := false + var userID *string + var errStr *string - token, err := ExtractBearerToken(ginCtx.Request.Header.Get("Authorization")) - if err != nil { - // TODO: return 500 - log.Errorf("failed to extract bearer token: %v", err) - return auth.GetImpersonationToken401Response{}, err - } - log.Printf("got auth token: %s", token) - - ext_service, err := s.db.GetExternalServiceByToken(context.Background(), &token) - if err != nil { - log.Errorf("failed to get external service by token: %v", err) - return auth.GetImpersonationToken401Response{}, err - // TODO: check err and retyrn 400/500 - } - - var user_id string = "" - - if req.Body.ExternalId != nil { - user, err := s.db.GetUserByExternalServiceId(context.Background(), sqlc.GetUserByExternalServiceIdParams{ - ExternalID: fmt.Sprintf("%d", *req.Body.ExternalId), - ServiceID: ext_service.ID, - }) - if err != nil { - log.Errorf("failed to get user by external user id: %v", err) - return auth.GetImpersonationToken401Response{}, err - // TODO: check err and retyrn 400/500 + token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method") } + return secretKey, nil + }) - user_id = fmt.Sprintf("%d", user.ID) + if err != nil { + e := err.Error() + errStr = &e + return auth.PostAuthVerifyToken200JSONResponse{ + Valid: &valid, + UserId: userID, + Error: errStr, + }, nil } - if req.Body.UserId != nil { - // TODO: check user existence - if user_id != "" && user_id != fmt.Sprintf("%d", *req.Body.UserId) { - log.Error("user_id and external_d are incorrect") - // TODO: 405 - return auth.GetImpersonationToken401Response{}, nil + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + if uid, ok := claims["user_id"].(string); ok { + valid = true + userID = &uid } else { - user_id = fmt.Sprintf("%d", *req.Body.UserId) + e := "user_id not found in token" + errStr = &e } + } else { + e := "invalid token claims" + errStr = &e } - accessToken, err := s.generateImpersonationToken(user_id, fmt.Sprintf("%d", ext_service.ID)) - if err != nil { - log.Errorf("failed to generate impersonation token: %v", err) - return auth.GetImpersonationToken401Response{}, err - // TODO: check err and retyrn 400/500 - } - - return auth.GetImpersonationToken200JSONResponse{AccessToken: accessToken}, nil -} - -// func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) { -// valid := false -// var userID *string -// var errStr *string - -// token, err := jwt.Parse(req.Body.Token, func(t *jwt.Token) (interface{}, error) { -// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { -// return nil, fmt.Errorf("unexpected signing method") -// } -// return refreshSecret, nil -// }) - -// if err != nil { -// e := err.Error() -// errStr = &e -// return auth.PostAuthVerifyToken200JSONResponse{ -// Valid: &valid, -// UserId: userID, -// Error: errStr, -// }, nil -// } - -// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { -// if uid, ok := claims["user_id"].(string); ok { -// // Refresh token is valid, generate new tokens -// newAccessToken, newRefreshToken, _ := generateTokens(uid) -// valid = true -// userID = &uid -// return auth.PostAuthVerifyToken200JSONResponse{ -// Valid: &valid, -// UserId: userID, -// Error: nil, -// Token: &newAccessToken, // return new access token -// // optionally return newRefreshToken as well -// }, nil -// } else { -// e := "user_id not found in refresh token" -// errStr = &e -// } -// } else { -// e := "invalid refresh token claims" -// errStr = &e -// } - -// return auth.PostAuthVerifyToken200JSONResponse{ -// Valid: &valid, -// UserId: userID, -// Error: errStr, -// }, nil -// } - -func ExtractBearerToken(header string) (string, error) { - const prefix = "Bearer " - if len(header) <= len(prefix) || header[:len(prefix)] != prefix { - return "", fmt.Errorf("invalid bearer token format") - } - return header[len(prefix):], nil + return auth.PostAuthVerifyToken200JSONResponse{ + Valid: &valid, + UserId: userID, + Error: errStr, + }, nil } diff --git a/modules/auth/helpers.go b/modules/auth/helpers.go deleted file mode 100644 index 9c3ab36..0000000 --- a/modules/auth/helpers.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "fmt" - "reflect" -) - -func setField(obj interface{}, name string, value interface{}) error { - v := reflect.ValueOf(obj) - - if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { - return fmt.Errorf("expected pointer to a struct") - } - - v = v.Elem() - field := v.FieldByName(name) - - if !field.IsValid() { - return fmt.Errorf("no such field: %s", name) - } - if !field.CanSet() { - return fmt.Errorf("cannot set field: %s", name) - } - - val := reflect.ValueOf(value) - - if field.Type() != val.Type() { - return fmt.Errorf("provided value type (%s) doesn't match field type (%s)", val.Type(), field.Type()) - } - - field.Set(val) - return nil -} diff --git a/modules/auth/main.go b/modules/auth/main.go index 7305b7d..c001e8b 100644 --- a/modules/auth/main.go +++ b/modules/auth/main.go @@ -1,10 +1,6 @@ package main import ( - "context" - "fmt" - "os" - "reflect" "time" auth "nyanimedb/auth" @@ -13,40 +9,19 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/pelletier/go-toml/v2" - log "github.com/sirupsen/logrus" ) var AppConfig Config func main() { - if len(os.Args) != 2 { - AppConfig.Mode = "env" - } else { - AppConfig.Mode = "argv" - } - - err := InitConfig() - if err != nil { - log.Fatalf("Failed to init config: %v\n", err) - } - r := gin.Default() - pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL")) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) - os.Exit(1) - } + var queries *sqlc.Queries = nil - var queries *sqlc.Queries = sqlc.New(pool) + server := handlers.NewServer(queries) - server := handlers.NewServer(queries, AppConfig.JwtPrivateKey) - - log.Info("allow origins:", AppConfig.ServiceAddress) r.Use(cors.New(cors.Config{ - AllowOrigins: []string{"*"}, + AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, ExposeHeaders: []string{"Content-Length"}, @@ -61,41 +36,3 @@ func main() { r.Run(":8082") } - -func InitConfig() error { - if AppConfig.Mode == "argv" { - content, err := os.ReadFile(os.Args[1]) - if err != nil { - return err - } - - toml.Unmarshal(content, &AppConfig) - - fmt.Printf("%+v\n", AppConfig) - - return nil - } else if AppConfig.Mode == "env" { - f := reflect.ValueOf(AppConfig) - - for i := 0; i < f.NumField(); i++ { - field := f.Type().Field(i) - tag := field.Tag - env_var := tag.Get("env") - fmt.Printf("Field: %v.\nEnvironment variable: %v.\n", field.Name, env_var) - if env_var != "" { - env_value, exists := os.LookupEnv(env_var) - if !exists { - return fmt.Errorf("there is no env variable %s", env_var) - } - err := setField(&AppConfig, field.Name, env_value) - if err != nil { - return fmt.Errorf("failed to set config field %s: %v", field.Name, err) - } - } - } - - return nil - } else { - return fmt.Errorf("incorrect config mode") - } -} diff --git a/modules/auth/queries.sql b/modules/auth/queries.sql deleted file mode 100644 index 0b9b941..0000000 --- a/modules/auth/queries.sql +++ /dev/null @@ -1,21 +0,0 @@ --- name: GetUserByNickname :one -SELECT * -FROM users -WHERE nickname = sqlc.arg('nickname'); - --- name: CreateNewUser :one -INSERT -INTO users (passhash, nickname) -VALUES (sqlc.arg(passhash), sqlc.arg(nickname)) -RETURNING id; - --- name: GetExternalServiceByToken :one -SELECT * -FROM external_services -WHERE auth_token = sqlc.arg('auth_token'); - --- name: GetUserByExternalServiceId :one -SELECT u.* -FROM users u -LEFT JOIN external_ids ei ON eu.user_id = u.id -WHERE ei.external_id = sqlc.arg('external_id') AND ei.service_id = sqlc.arg('service_id'); \ No newline at end of file diff --git a/modules/auth/types.go b/modules/auth/types.go index 694843e..038b179 100644 --- a/modules/auth/types.go +++ b/modules/auth/types.go @@ -1,9 +1,6 @@ package main type Config struct { - Mode string - ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"` - DdUrl string `toml:"DbUrl" env:"DATABASE_URL"` - JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"` - LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` + JwtPrivateKey string + LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` } diff --git a/modules/backend/handlers/common.go b/modules/backend/handlers/common.go deleted file mode 100644 index 7f2807f..0000000 --- a/modules/backend/handlers/common.go +++ /dev/null @@ -1,173 +0,0 @@ -package handlers - -import ( - "encoding/json" - "fmt" - oapi "nyanimedb/api" - "nyanimedb/modules/backend/rmq" - sqlc "nyanimedb/sql" - "strconv" -) - -// type Handler struct { -// publisher *rmq.Publisher -// } - -// func New(publisher *rmq.Publisher) *Handler { -// return &Handler{publisher: publisher} -// } - -type Server struct { - db *sqlc.Queries - // publisher *rmq.Publisher - RPCclient *rmq.RPCClient -} - -func NewServer(db *sqlc.Queries, rpcclient *rmq.RPCClient) *Server { - return &Server{ - db: db, - // publisher: publisher, - RPCclient: rpcclient, - } -} - -func sql2StorageType(s *sqlc.StorageTypeT) (*oapi.StorageType, error) { - if s == nil { - return nil, nil - } - var t oapi.StorageType - switch *s { - case sqlc.StorageTypeTLocal: - t = oapi.Local - case sqlc.StorageTypeTS3: - t = oapi.S3 - default: - return nil, fmt.Errorf("unexpected storage type: %s", *s) - } - return &t, nil -} - -func (s Server) mapTitle(title sqlc.GetTitleByIDRow) (oapi.Title, error) { - - oapi_title := oapi.Title{ - EpisodesAired: title.EpisodesAired, - EpisodesAll: title.EpisodesAll, - // EpisodesLen: &episodes_lens, - Id: title.ID, - // Poster: &oapi_image, - Rating: title.Rating, - RatingCount: title.RatingCount, - // ReleaseSeason: &release_season, - ReleaseYear: title.ReleaseYear, - // Studio: &oapi_studio, - // Tags: oapi_tag_names, - // TitleNames: title_names, - // TitleStatus: oapi_status, - // AdditionalProperties: - } - - title_names := make(map[string][]string, 0) - err := json.Unmarshal(title.TitleNames, &title_names) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal TitleNames: %v", err) - } - oapi_title.TitleNames = title_names - - if len(title.TitleDesc) > 0 { - title_descs := make(map[string]string, 0) - err = json.Unmarshal(title.TitleDesc, &title_descs) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal TitleDesc: %v", err) - } - oapi_title.TitleDesc = &title_descs - } - if len(title.EpisodesLen) > 0 { - episodes_lens := make(map[string]float64, 0) - err = json.Unmarshal(title.EpisodesLen, &episodes_lens) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshal EpisodesLen: %v", err) - } - oapi_title.EpisodesLen = &episodes_lens - } - - oapi_tag_names := make(oapi.Tags, 0) - err = json.Unmarshal(title.TagNames, &oapi_tag_names) - if err != nil { - return oapi.Title{}, fmt.Errorf("unmarshalling title_tag: %v", err) - } - oapi_title.Tags = oapi_tag_names - - var oapi_studio oapi.Studio - if title.StudioName != nil { - oapi_studio.Name = *title.StudioName - } - if title.StudioID != 0 { - oapi_studio.Id = title.StudioID - oapi_studio.Description = title.StudioDesc - if title.StudioIllustID != nil { - oapi_studio.Poster = &oapi.Image{} - oapi_studio.Poster.Id = title.StudioIllustID - oapi_studio.Poster.ImagePath = title.StudioImagePath - - s, err := sql2StorageType(title.StudioStorageType) - if err != nil { - return oapi.Title{}, fmt.Errorf("mapTitle, studio storage type: %v", err) - } - oapi_studio.Poster.StorageType = s - - } - } - oapi_title.Studio = &oapi_studio - - var oapi_image oapi.Image - - if title.PosterID != nil { - oapi_image.Id = title.PosterID - oapi_image.ImagePath = title.TitleImagePath - s, err := sql2StorageType(title.TitleStorageType) - if err != nil { - return oapi.Title{}, fmt.Errorf("mapTitle, title starage type: %v", err) - } - oapi_image.StorageType = s - } - oapi_title.Poster = &oapi_image - - var release_season oapi.ReleaseSeason - if title.ReleaseSeason != nil { - release_season = oapi.ReleaseSeason(*title.ReleaseSeason) - } - oapi_title.ReleaseSeason = &release_season - - oapi_status, err := TitleStatus2oapi(&title.TitleStatus) - if err != nil { - return oapi.Title{}, fmt.Errorf("TitleStatus2oapi: %v", err) - } - oapi_title.TitleStatus = oapi_status - - return oapi_title, nil -} - -func parseInt64(s string) (int64, error) { - i, err := strconv.ParseInt(s, 10, 64) - return i, err -} - -func TitleStatus2Sqlc(s *[]oapi.TitleStatus) ([]sqlc.TitleStatusT, error) { - var sqlc_status []sqlc.TitleStatusT - if s == nil { - return nil, nil - } - for _, t := range *s { - switch t { - case oapi.TitleStatusFinished: - sqlc_status = append(sqlc_status, sqlc.TitleStatusTFinished) - case oapi.TitleStatusOngoing: - sqlc_status = append(sqlc_status, sqlc.TitleStatusTOngoing) - case oapi.TitleStatusPlanned: - sqlc_status = append(sqlc_status, sqlc.TitleStatusTPlanned) - default: - return nil, fmt.Errorf("unexpected tittle status: %s", t) - } - } - return sqlc_status, nil -} diff --git a/modules/backend/handlers/cursor.go b/modules/backend/handlers/cursor.go deleted file mode 100644 index e3b48b0..0000000 --- a/modules/backend/handlers/cursor.go +++ /dev/null @@ -1,156 +0,0 @@ -package handlers - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "reflect" - "strconv" -) - -// ParseCursorInto parses an opaque base64 cursor and injects values into target struct. -// -// Supported sort types: -// - "id" → sets CursorID (must be *int64) -// - "year" → sets CursorID (*int64) + CursorYear (*int32) -// - "rating" → sets CursorID (*int64) + CursorRating (*float64) -// -// Target struct may have any subset of these fields (e.g. only CursorID). -// Unknown fields are ignored. Missing fields → values are dropped (safe). -// -// Returns error if cursor is invalid or inconsistent with sort_by. -func ParseCursorInto(sortBy, cursorStr string, target any) error { - if cursorStr == "" { - return nil // no cursor → nothing to do - } - - // 1. Decode cursor - payload, err := decodeCursor(cursorStr) - if err != nil { - return err - } - - // 2. Extract ID (required for all types) - id, err := extractInt64(payload, "id") - if err != nil { - return fmt.Errorf("cursor: %v", err) - } - - // 3. Get reflect value of target (must be ptr to struct) - v := reflect.ValueOf(target) - if v.Kind() != reflect.Pointer || v.IsNil() { - return fmt.Errorf("target must be non-nil pointer to struct") - } - v = v.Elem() - if v.Kind() != reflect.Struct { - return fmt.Errorf("target must be pointer to struct") - } - - // 4. Helper: set field if exists and compatible - setField := func(fieldName string, value any) { - f := v.FieldByName(fieldName) - if !f.IsValid() || !f.CanSet() { - return // field not found or unexported - } - ft := f.Type() - vv := reflect.ValueOf(value) - - // Case: field is *T, value is T → wrap in pointer - if ft.Kind() == reflect.Pointer { - elemType := ft.Elem() - if vv.Type().AssignableTo(elemType) { - ptr := reflect.New(elemType) - ptr.Elem().Set(vv) - f.Set(ptr) - } - // nil → leave as zero (nil pointer) - } else if vv.Type().AssignableTo(ft) { - f.Set(vv) - } - // else: type mismatch → silently skip (safe) - } - - // 5. Dispatch by sort type - switch sortBy { - case "id": - setField("CursorID", id) - - case "year": - setField("CursorID", id) - param, err := extractString(payload, "param") - if err != nil { - return fmt.Errorf("cursor year: %w", err) - } - year, err := strconv.Atoi(param) - if err != nil { - return fmt.Errorf("cursor year: param must be integer, got %q", param) - } - setField("CursorYear", int32(year)) // or int, depending on your schema - - case "rating": - setField("CursorID", id) - param, err := extractString(payload, "param") - if err != nil { - return fmt.Errorf("cursor rating: %w", err) - } - rating, err := strconv.ParseFloat(param, 64) - if err != nil { - return fmt.Errorf("cursor rating: param must be float, got %q", param) - } - setField("CursorRating", rating) - - default: - return fmt.Errorf("unsupported sort_by: %q", sortBy) - } - - return nil -} - -// --- helpers --- -func decodeCursor(cursorStr string) (map[string]any, error) { - data, err := base64.RawURLEncoding.DecodeString(cursorStr) - if err != nil { - data, err = base64.StdEncoding.DecodeString(cursorStr) - if err != nil { - return nil, fmt.Errorf("invalid base64 cursor") - } - } - var m map[string]any - if err := json.Unmarshal(data, &m); err != nil { - return nil, fmt.Errorf("invalid cursor JSON: %w", err) - } - return m, nil -} - -func extractInt64(m map[string]any, key string) (int64, error) { - v, ok := m[key] - if !ok { - return 0, fmt.Errorf("missing %q", key) - } - switch x := v.(type) { - case float64: - if x == float64(int64(x)) { - return int64(x), nil - } - case string: - i, err := strconv.ParseInt(x, 10, 64) - if err == nil { - return i, nil - } - case int64, int, int32: - return reflect.ValueOf(x).Int(), nil - } - return 0, fmt.Errorf("%q must be integer", key) -} - -func extractString(m map[string]any, key string) (string, error) { - v, ok := m[key] - if !ok { - return "", fmt.Errorf("missing %q", key) - } - s, ok := v.(string) - if !ok { - return "", fmt.Errorf("%q must be string", key) - } - return s, nil -} diff --git a/modules/backend/handlers/images.go b/modules/backend/handlers/images.go deleted file mode 100644 index c1e3d4b..0000000 --- a/modules/backend/handlers/images.go +++ /dev/null @@ -1,141 +0,0 @@ -package handlers - -import ( - "bytes" - "context" - "fmt" - "image" - "image/jpeg" - "image/png" - "io" - "net/http" - oapi "nyanimedb/api" - "os" - "path/filepath" - "strings" - - "github.com/disintegration/imaging" - log "github.com/sirupsen/logrus" - "golang.org/x/image/webp" -) - -// PostMediaUpload implements oapi.StrictServerInterface. -func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUploadRequestObject) (oapi.PostMediaUploadResponseObject, error) { - // Получаем multipart body - mp := request.MultipartBody - if mp == nil { - log.Errorf("PostMedia without body") - return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil - } - - // Парсим первую часть (предполагаем, что файл в поле "file") - part, err := mp.NextPart() - if err != nil { - log.Errorf("PostMedia without file") - return oapi.PostMediaUpload400JSONResponse("File required"), nil - } - defer part.Close() - - // Читаем ВЕСЬ файл в память (для небольших изображений — нормально) - // Если файлы могут быть большими — используйте лимитированный буфер (см. ниже) - data, err := io.ReadAll(part) - if err != nil { - log.Errorf("PostMedia cannot read file") - return oapi.PostMediaUpload400JSONResponse("File required"), nil - } - - if len(data) == 0 { - log.Errorf("PostMedia empty file") - return oapi.PostMediaUpload400JSONResponse("Empty file"), nil - } - - // Проверка MIME по первым 512 байтам - mimeType := http.DetectContentType(data) - if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" { - log.Errorf("PostMedia bad type") - return oapi.PostMediaUpload400JSONResponse("Bad data type"), nil - } - - // Декодируем изображение из буфера - var img image.Image - switch mimeType { - case "image/jpeg": - { - img, err = jpeg.Decode(bytes.NewReader(data)) - if err != nil { - log.Errorf("PostMedia cannot decode file: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - } - case "image/png": - { - img, err = png.Decode(bytes.NewReader(data)) - if err != nil { - log.Errorf("PostMedia cannot decode file: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - } - case "image/webp": - { - img, err = webp.Decode(bytes.NewReader(data)) - if err != nil { - log.Errorf("PostMedia cannot decode file: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - } - } - - var buf bytes.Buffer - err = imaging.Encode(&buf, img, imaging.PNG) - if err != nil { - log.Errorf("PostMedia failed to re-encode JPEG: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - - // TODO: to delete - filename := part.FileName() - if filename == "" { - filename = "upload_" + generateRandomHex(8) + ".jpg" - } else { - filename = sanitizeFilename(filename) - if !strings.HasSuffix(strings.ToLower(filename), ".png") { - filename += ".png" - } - } - - // TODO: пойти на хуй ( вызвать файловую помойку) - os.Mkdir("uploads", 0644) - err = os.WriteFile(filepath.Join("./uploads", filename), buf.Bytes(), 0644) - if err != nil { - log.Errorf("PostMedia failed to write: %v", err) - return oapi.PostMediaUpload500Response{}, nil - } - - return oapi.PostMediaUpload200JSONResponse{}, nil -} - -// Вспомогательные функции — как раньше -func generateRandomHex(n int) string { - b := make([]byte, n) - for i := range b { - b[i] = byte('a' + (i % 16)) - } - return fmt.Sprintf("%x", b) -} - -func sanitizeFilename(name string) string { - var clean strings.Builder - for _, r := range name { - if (r >= 'a' && r <= 'z') || - (r >= 'A' && r <= 'Z') || - (r >= '0' && r <= '9') || - r == '.' || r == '_' || r == '-' { - clean.WriteRune(r) - } - } - s := clean.String() - if s == "" { - return "file" - } - return s -} diff --git a/modules/backend/handlers/titles.go b/modules/backend/handlers/titles.go deleted file mode 100644 index 7aeeb11..0000000 --- a/modules/backend/handlers/titles.go +++ /dev/null @@ -1,304 +0,0 @@ -package handlers - -import ( - "context" - "encoding/json" - "fmt" - oapi "nyanimedb/api" - "nyanimedb/modules/backend/rmq" - sqlc "nyanimedb/sql" - "strconv" - "time" - - "github.com/jackc/pgx/v5" - log "github.com/sirupsen/logrus" -) - -func Word2Sqlc(s *string) *string { - if s == nil || *s == "" { - return nil - } - - return s -} - -func TitleStatus2oapi(s *sqlc.TitleStatusT) (*oapi.TitleStatus, error) { - if s == nil { - return nil, nil - } - var t oapi.TitleStatus - switch *s { - case sqlc.TitleStatusTFinished: - t = oapi.TitleStatusFinished - case sqlc.TitleStatusTOngoing: - t = oapi.TitleStatusOngoing - case sqlc.TitleStatusTPlanned: - t = oapi.TitleStatusPlanned - default: - return nil, fmt.Errorf("unexpected tittle status: %s", *s) - } - return &t, nil -} - -func ReleaseSeason2sqlc(s *oapi.ReleaseSeason) (*sqlc.ReleaseSeasonT, error) { - if s == nil { - return nil, nil - } - var t sqlc.ReleaseSeasonT - switch *s { - case oapi.Winter: - t = sqlc.ReleaseSeasonTWinter - case oapi.Spring: - t = sqlc.ReleaseSeasonTSpring - case oapi.Summer: - t = sqlc.ReleaseSeasonTSummer - case oapi.Fall: - t = sqlc.ReleaseSeasonTFall - default: - return nil, fmt.Errorf("unexpected release season: %s", *s) - } - return &t, nil -} - -func (s Server) GetTagsByTitleId(ctx context.Context, id int64) (oapi.Tags, error) { - - sqlc_title_tags, err := s.db.GetTitleTags(ctx, id) - if err != nil { - if err == pgx.ErrNoRows { - return nil, nil - } - return nil, fmt.Errorf("query GetTitleTags: %v", err) - } - - oapi_tag_names := make(oapi.Tags, 1) - for _, title_tag := range sqlc_title_tags { - oapi_tag_name := make(map[string]string, 1) - err = json.Unmarshal(title_tag, &oapi_tag_name) - if err != nil { - return nil, fmt.Errorf("unmarshalling title_tag: %v", err) - } - oapi_tag_names = append(oapi_tag_names, oapi_tag_name) - } - - return oapi_tag_names, nil -} - -// func (s Server) GetImage(ctx context.Context, id int64) (*oapi.Image, error) { - -// var oapi_image oapi.Image - -// sqlc_image, err := s.db.GetImageByID(ctx, id) -// if err != nil { -// if err == pgx.ErrNoRows { -// return nil, nil //todo: error reference in db -// } -// return &oapi_image, fmt.Errorf("query GetImageByID: %v", err) -// } - -// //can cast and dont use brain cause all this fields required in image table -// oapi_image.Id = &sqlc_image.ID -// oapi_image.ImagePath = &sqlc_image.ImagePath -// storageTypeStr := string(sqlc_image.StorageType) -// oapi_image.StorageType = string(storageTypeStr) - -// return &oapi_image, nil -// } - -// func (s Server) GetStudio(ctx context.Context, id int64) (*oapi.Studio, error) { - -// var oapi_studio oapi.Studio - -// sqlc_studio, err := s.db.GetStudioByID(ctx, id) -// if err != nil { -// if err == pgx.ErrNoRows { -// return nil, nil -// } -// return &oapi_studio, fmt.Errorf("query GetStudioByID: %v", err) -// } - -// oapi_studio.Id = sqlc_studio.ID -// oapi_studio.Name = sqlc_studio.StudioName -// oapi_studio.Description = sqlc_studio.StudioDesc - -// if sqlc_studio.IllustID == nil { -// return &oapi_studio, nil -// } -// oapi_illust, err := s.GetImage(ctx, *sqlc_studio.IllustID) -// if err != nil { -// return &oapi_studio, fmt.Errorf("GetImage: %v", err) -// } -// if oapi_illust != nil { -// oapi_studio.Poster = oapi_illust -// } - -// return &oapi_studio, nil -// } - -func (s Server) GetTitle(ctx context.Context, request oapi.GetTitleRequestObject) (oapi.GetTitleResponseObject, error) { - var oapi_title oapi.Title - - sqlc_title, err := s.db.GetTitleByID(ctx, request.TitleId) - if err != nil { - if err == pgx.ErrNoRows { - return oapi.GetTitle204Response{}, nil - } - log.Errorf("%v", err) - return oapi.GetTitle500Response{}, nil - } - - oapi_title, err = s.mapTitle(sqlc_title) - if err != nil { - log.Errorf("%v", err) - return oapi.GetTitle500Response{}, nil - } - - return oapi.GetTitle200JSONResponse(oapi_title), nil -} - -func (s Server) GetTitles(ctx context.Context, request oapi.GetTitlesRequestObject) (oapi.GetTitlesResponseObject, error) { - - opai_titles := make([]oapi.Title, 0) - mqreq := rmq.RabbitRequest{ - Timestamp: time.Now(), - } - - word := Word2Sqlc(request.Params.Word) - if word != nil { - mqreq.Name = *word - } - - season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) - if err != nil { - log.Errorf("%v", err) - return oapi.GetTitles400Response{}, err - } - if season != nil { - mqreq.Season = *request.Params.ReleaseSeason - } - - title_statuses, err := TitleStatus2Sqlc(request.Params.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.GetTitles400Response{}, err - } - if title_statuses != nil { - mqreq.Statuses = *request.Params.Status - } - - if request.Params.ExtSearch != nil && *request.Params.ExtSearch { - - // Структура для ответа (должна совпадать с тем, что шлёт микросервис) - var reply struct { - Status string `json:"status"` - Result string `json:"result"` - Preview string `json:"preview_url"` - } - - // Делаем RPC-вызов — и ЖДЁМ ответа - err := s.RPCclient.Call( - ctx, - mqreq, - &reply, - ) - if err != nil { - log.Errorf("RabitMQ: %v", err) - // return oapi.GetTitles500Response{}, err - } - // // Возвращаем результат - // return oapi.ProcessMedia200JSONResponse{ - // Status: reply.Status, - // Result: reply.Result, - // Preview: reply.Preview, - // }, nil - } - - params := sqlc.SearchTitlesParams{ - Word: word, - TitleStatuses: title_statuses, - Rating: request.Params.Rating, - ReleaseYear: request.Params.ReleaseYear, - ReleaseSeason: season, - Forward: true, // default - SortBy: "id", // default - Limit: request.Params.Limit, - } - - if request.Params.SortForward != nil { - params.Forward = *request.Params.SortForward - } - if request.Params.Sort != nil { - params.SortBy = string(*request.Params.Sort) - if request.Params.Cursor != nil { - // here we set CursorYear CursorID CursorRating fields - err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) - if err != nil { - log.Errorf("%v", err) - return oapi.GetTitles400Response{}, nil - } - } - } - // param = nil means it will not be used - titles, err := s.db.SearchTitles(ctx, params) - if err != nil { - log.Errorf("%v", err) - return oapi.GetTitles500Response{}, nil - } - if len(titles) == 0 { - return oapi.GetTitles204Response{}, nil - } - - var new_cursor oapi.CursorObj - - for _, title := range titles { - - _title := sqlc.GetTitleByIDRow{ - ID: title.ID, - // StudioID: title.StudioID, - PosterID: title.PosterID, - TitleStatus: title.TitleStatus, - Rating: title.Rating, - RatingCount: title.RatingCount, - ReleaseYear: title.ReleaseYear, - ReleaseSeason: title.ReleaseSeason, - Season: title.Season, - EpisodesAired: title.EpisodesAired, - EpisodesAll: title.EpisodesAll, - // EpisodesLen: title.EpisodesLen, - TitleStorageType: title.TitleStorageType, - TitleImagePath: title.TitleImagePath, - TitleNames: title.TitleNames, - TagNames: title.TagNames, - StudioName: title.StudioName, - // StudioIllustID: title.StudioIllustID, - // StudioDesc: title.StudioDesc, - // StudioStorageType: title.StudioStorageType, - // StudioImagePath: title.StudioImagePath, - } - - // if title.TitleStorageType != nil { - // s := *title.TitleStorageType - // _title.TitleStorageType = string(s) - // } - - t, err := s.mapTitle(_title) - if err != nil { - log.Errorf("%v", err) - return oapi.GetTitles500Response{}, nil - } - opai_titles = append(opai_titles, t) - - new_cursor.Id = t.Id - if request.Params.Sort != nil { - switch string(*request.Params.Sort) { - case "year": - tmp := fmt.Sprint(*t.ReleaseYear) - new_cursor.Param = &tmp - case "rating": - tmp := strconv.FormatFloat(*t.Rating, 'f', -1, 64) - new_cursor.Param = &tmp - } - } - } - - return oapi.GetTitles200JSONResponse{Cursor: new_cursor, Data: opai_titles}, nil -} diff --git a/modules/backend/handlers/users.go b/modules/backend/handlers/users.go index eecd82f..b67153d 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -2,534 +2,50 @@ package handlers import ( "context" - "errors" - "fmt" oapi "nyanimedb/api" sqlc "nyanimedb/sql" "strconv" - "time" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" "github.com/oapi-codegen/runtime/types" - log "github.com/sirupsen/logrus" ) -const ( - pgErrDuplicateKey = "23505" -) - -func mapUser(u sqlc.GetUserByIDRow) (oapi.User, error) { - i := oapi.Image{ - Id: u.AvatarID, - ImagePath: u.ImagePath, - } - s, err := sql2StorageType(u.StorageType) - if err != nil { - return oapi.User{}, fmt.Errorf("mapUser, storage type: %v", err) - } - i.StorageType = s - return oapi.User{ - Image: &i, - CreationDate: &u.CreationDate, - DispName: u.DispName, - Id: &u.ID, - Mail: StringToEmail(u.Mail), - Nickname: u.Nickname, - UserDesc: u.UserDesc, - }, nil +type Server struct { + db *sqlc.Queries } -func (s Server) GetUsersId(ctx context.Context, req oapi.GetUsersIdRequestObject) (oapi.GetUsersIdResponseObject, error) { +func NewServer(db *sqlc.Queries) Server { + return Server{db: db} +} + +func parseInt64(s string) (int32, error) { + i, err := strconv.ParseInt(s, 10, 64) + return int32(i), err +} + +func mapUser(u sqlc.GetUserByIDRow) oapi.User { + return oapi.User{ + AvatarId: u.AvatarID, + CreationDate: u.CreationDate, + DispName: u.DispName, + Id: &u.ID, + Mail: (*types.Email)(u.Mail), + Nickname: u.Nickname, + UserDesc: u.UserDesc, + } +} + +func (s Server) GetUsersUserId(ctx context.Context, req oapi.GetUsersUserIdRequestObject) (oapi.GetUsersUserIdResponseObject, error) { userID, err := parseInt64(req.UserId) if err != nil { - return oapi.GetUsersId404Response{}, nil + return oapi.GetUsersUserId404Response{}, nil } - _user, err := s.db.GetUserByID(context.TODO(), userID) + user, err := s.db.GetUserByID(context.TODO(), int64(userID)) if err != nil { if err == pgx.ErrNoRows { - return oapi.GetUsersId404Response{}, nil + return oapi.GetUsersUserId404Response{}, nil } return nil, err } - user, err := mapUser(_user) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUsersId500Response{}, err - } - return oapi.GetUsersId200JSONResponse(user), nil -} - -func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time { - if p_date.Valid { - t := p_date.Time - return &t - } - return nil -} - -func oapiDate2sql(t *time.Time) pgtype.Timestamptz { - if t == nil { - return pgtype.Timestamptz{Valid: false} - } - return pgtype.Timestamptz{ - Time: *t, - Valid: true, - } -} - -// func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) { -// var sqlc_status SqlcUserStatus -// if s == nil { -// return &sqlc_status, nil -// } -// for _, t := range *s { -// switch t { -// case oapi.UserTitleStatusFinished: -// sqlc_status.finished = "finished" -// case oapi.UserTitleStatusDropped: -// sqlc_status.dropped = "dropped" -// case oapi.UserTitleStatusPlanned: -// sqlc_status.planned = "planned" -// case oapi.UserTitleStatusInProgress: -// sqlc_status.in_progress = "in-progress" -// default: -// return nil, fmt.Errorf("unexpected tittle status: %s", t) -// } -// } -// return &sqlc_status, nil -// } - -func sql2usertitlestatus(s sqlc.UsertitleStatusT) (oapi.UserTitleStatus, error) { - var status oapi.UserTitleStatus - - switch s { - case sqlc.UsertitleStatusTFinished: - status = oapi.UserTitleStatusFinished - case sqlc.UsertitleStatusTDropped: - status = oapi.UserTitleStatusDropped - case sqlc.UsertitleStatusTPlanned: - status = oapi.UserTitleStatusPlanned - case sqlc.UsertitleStatusTInProgress: - status = oapi.UserTitleStatusInProgress - default: - return status, fmt.Errorf("unexpected tittle status: %s", s) - } - - return status, nil -} - -func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) ([]sqlc.UsertitleStatusT, error) { - var sqlc_status []sqlc.UsertitleStatusT - if s == nil { - return nil, nil - } - for _, t := range *s { - switch t { - case oapi.UserTitleStatusFinished: - sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTFinished) - case oapi.UserTitleStatusInProgress: - sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTInProgress) - case oapi.UserTitleStatusDropped: - sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTDropped) - case oapi.UserTitleStatusPlanned: - sqlc_status = append(sqlc_status, sqlc.UsertitleStatusTPlanned) - default: - return nil, fmt.Errorf("unexpected tittle status: %s", t) - } - } - return sqlc_status, nil -} - -func UserTitleStatus2Sqlc1(s *oapi.UserTitleStatus) (*sqlc.UsertitleStatusT, error) { - var sqlc_status sqlc.UsertitleStatusT = sqlc.UsertitleStatusTFinished - if s == nil { - return &sqlc_status, nil - } - - switch *s { - case oapi.UserTitleStatusFinished: - sqlc_status = sqlc.UsertitleStatusTFinished - case oapi.UserTitleStatusInProgress: - sqlc_status = sqlc.UsertitleStatusTInProgress - case oapi.UserTitleStatusDropped: - sqlc_status = sqlc.UsertitleStatusTDropped - case oapi.UserTitleStatusPlanned: - sqlc_status = sqlc.UsertitleStatusTPlanned - default: - return nil, fmt.Errorf("unexpected tittle status: %s", *s) - } - - return &sqlc_status, nil -} - -func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) { - - oapi_usertitle := oapi.UserTitle{ - Ctime: &t.UserCtime, - Rate: t.UserRate, - ReviewId: t.ReviewID, - // Status: , - // Title: , - UserId: t.UserID, - } - - status, err := sql2usertitlestatus(t.UsertitleStatus) - if err != nil { - return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err) - } - oapi_usertitle.Status = status - - _title := sqlc.GetTitleByIDRow{ - ID: t.ID, - // StudioID: title.StudioID, - PosterID: t.PosterID, - TitleStatus: t.TitleStatus, - Rating: t.Rating, - RatingCount: t.RatingCount, - ReleaseYear: t.ReleaseYear, - ReleaseSeason: t.ReleaseSeason, - Season: t.Season, - EpisodesAired: t.EpisodesAired, - EpisodesAll: t.EpisodesAll, - // EpisodesLen: title.EpisodesLen, - TitleStorageType: t.TitleStorageType, - TitleImagePath: t.TitleImagePath, - StudioName: t.StudioName, - TitleNames: t.TitleNames, - TagNames: t.TagNames, - // StudioIllustID: title.StudioIllustID, - // StudioDesc: title.StudioDesc, - // StudioStorageType: title.StudioStorageType, - // StudioImagePath: title.StudioImagePath, - } - - oapi_title, err := s.mapTitle(_title) - if err != nil { - return oapi_usertitle, fmt.Errorf("mapUsertitle: %v", err) - } - oapi_usertitle.Title = &oapi_title - - return oapi_usertitle, nil -} - -func (s Server) GetUserTitles(ctx context.Context, request oapi.GetUserTitlesRequestObject) (oapi.GetUserTitlesResponseObject, error) { - - oapi_usertitles := make([]oapi.UserTitle, 0) - - word := Word2Sqlc(request.Params.Word) - - season, err := ReleaseSeason2sqlc(request.Params.ReleaseSeason) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, err - } - - // var statuses_sort []string - // if request.Params.Status != nil { - // for _, s := range *request.Params.Status { - // ss := string(s) // s type is alias for string - // statuses_sort = append(statuses_sort, ss) - // } - // } - - watch_status, err := UserTitleStatus2Sqlc(request.Params.WatchStatus) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, err - } - - title_statuses, err := TitleStatus2Sqlc(request.Params.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, err - } - - userID, err := parseInt64(request.UserId) - if err != nil { - log.Errorf("get user titles: %v", err) - return oapi.GetUserTitles404Response{}, err - } - params := sqlc.SearchUserTitlesParams{ - UserID: userID, - Word: word, - TitleStatuses: title_statuses, - UsertitleStatuses: watch_status, - Rating: request.Params.Rating, - Rate: request.Params.MyRate, - ReleaseYear: request.Params.ReleaseYear, - ReleaseSeason: season, - Forward: true, // default - SortBy: "id", // default - Limit: request.Params.Limit, - } - - if request.Params.SortForward != nil { - params.Forward = *request.Params.SortForward - } - if request.Params.Sort != nil { - params.SortBy = string(*request.Params.Sort) - if request.Params.Cursor != nil { - // here we set CursorYear CursorID CursorRating fields - err := ParseCursorInto(string(*request.Params.Sort), string(*request.Params.Cursor), ¶ms) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles400Response{}, nil - } - } - } - // param = nil means it will not be used - titles, err := s.db.SearchUserTitles(ctx, params) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles500Response{}, nil - } - if len(titles) == 0 { - return oapi.GetUserTitles204Response{}, nil - } - - var new_cursor oapi.CursorObj - - for _, title := range titles { - - t, err := s.mapUsertitle(ctx, title) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitles500Response{}, nil - } - oapi_usertitles = append(oapi_usertitles, t) - - new_cursor.Id = t.Title.Id - if request.Params.Sort != nil { - switch string(*request.Params.Sort) { - case "year": - tmp := fmt.Sprint(*t.Title.ReleaseYear) - new_cursor.Param = &tmp - case "rating": - tmp := strconv.FormatFloat(*t.Title.Rating, 'f', -1, 64) // падает - new_cursor.Param = &tmp - } - } - } - - return oapi.GetUserTitles200JSONResponse{Cursor: new_cursor, Data: oapi_usertitles}, nil -} - -func EmailToStringPtr(e *types.Email) *string { - if e == nil { - return nil - } - s := string(*e) - return &s -} - -func StringToEmail(e *string) *types.Email { - if e == nil { - return nil - } - s := types.Email(*e) - return &s -} - -// UpdateUser implements oapi.StrictServerInterface. -func (s Server) UpdateUser(ctx context.Context, request oapi.UpdateUserRequestObject) (oapi.UpdateUserResponseObject, error) { - - params := sqlc.UpdateUserParams{ - AvatarID: request.Body.AvatarId, - DispName: request.Body.DispName, - UserDesc: request.Body.UserDesc, - Mail: EmailToStringPtr(request.Body.Mail), - UserID: request.UserId, - } - - user, err := s.db.UpdateUser(ctx, params) - if err != nil { - log.Errorf("%v", err) - return oapi.UpdateUser500Response{}, nil - } - - oapi_user := oapi.User{ // maybe its possible to make one sqlc type and use one map func iinstead of this shit - // AvatarId: user.AvatarID, - CreationDate: &user.CreationDate, - DispName: user.DispName, - Id: &user.ID, - Mail: StringToEmail(user.Mail), - Nickname: user.Nickname, - UserDesc: user.UserDesc, - } - - return oapi.UpdateUser200JSONResponse(oapi_user), nil -} - -func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleRequestObject) (oapi.AddUserTitleResponseObject, error) { - //TODO: add review if exists - status, err := UserTitleStatus2Sqlc1(&request.Body.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.AddUserTitle400Response{}, nil - } - - params := sqlc.InsertUserTitleParams{ - UserID: request.UserId, - TitleID: request.Body.TitleId, - Status: *status, - Rate: request.Body.Rate, - Ftime: oapiDate2sql(request.Body.Ftime), - } - - user_title, err := s.db.InsertUserTitle(ctx, params) - if err != nil { - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) { - // fmt.Println(pgErr.Message) // => syntax error at end of input - // fmt.Println(pgErr.Code) // => 42601 - if pgErr.Code == pgErrDuplicateKey { //duplicate key value - return oapi.AddUserTitle409Response{}, nil - } - } else { - log.Errorf("%v", err) - return oapi.AddUserTitle500Response{}, nil - } - } - oapi_status, err := sql2usertitlestatus(user_title.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.AddUserTitle500Response{}, nil - } - oapi_usertitle := oapi.UserTitleMini{ - Ctime: &user_title.Ctime, - Rate: user_title.Rate, - ReviewId: user_title.ReviewID, - Status: oapi_status, - TitleId: user_title.TitleID, - UserId: user_title.UserID, - } - - return oapi.AddUserTitle200JSONResponse(oapi_usertitle), nil -} - -// DeleteUserTitle implements oapi.StrictServerInterface. -func (s Server) DeleteUserTitle(ctx context.Context, request oapi.DeleteUserTitleRequestObject) (oapi.DeleteUserTitleResponseObject, error) { - params := sqlc.DeleteUserTitleParams{ - UserID: request.UserId, - TitleID: request.TitleId, - } - _, err := s.db.DeleteUserTitle(ctx, params) - if err != nil { - if err == pgx.ErrNoRows { - return oapi.DeleteUserTitle404Response{}, nil - } - log.Errorf("%v", err) - return oapi.DeleteUserTitle500Response{}, nil - } - return oapi.DeleteUserTitle200Response{}, nil -} - -// UpdateUserTitle implements oapi.StrictServerInterface. -func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitleRequestObject) (oapi.UpdateUserTitleResponseObject, error) { - - status, err := UserTitleStatus2Sqlc1(request.Body.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.UpdateUserTitle400Response{}, nil - } - params := sqlc.UpdateUserTitleParams{ - Status: status, - Rate: request.Body.Rate, - UserID: request.UserId, - TitleID: request.TitleId, - Ftime: oapiDate2sql(request.Body.Ftime), - } - - user_title, err := s.db.UpdateUserTitle(ctx, params) - if err != nil { - if err == pgx.ErrNoRows { - return oapi.UpdateUserTitle404Response{}, nil - } - log.Errorf("%v", err) - return oapi.UpdateUserTitle500Response{}, nil - } - oapi_status, err := sql2usertitlestatus(user_title.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.UpdateUserTitle500Response{}, nil - } - - oapi_usertitle := oapi.UserTitleMini{ - Ctime: &user_title.Ctime, - Rate: user_title.Rate, - ReviewId: user_title.ReviewID, - Status: oapi_status, - TitleId: user_title.TitleID, - UserId: user_title.UserID, - } - - return oapi.UpdateUserTitle200JSONResponse(oapi_usertitle), nil -} - -func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleRequestObject) (oapi.GetUserTitleResponseObject, error) { - user_title, err := s.db.GetUserTitleByID(ctx, sqlc.GetUserTitleByIDParams{ - TitleID: request.TitleId, - UserID: request.UserId, - }) - if err != nil { - if err == pgx.ErrNoRows { - return oapi.GetUserTitle404Response{}, nil - } else { - log.Errorf("%v", err) - return oapi.GetUserTitle500Response{}, nil - } - } - oapi_status, err := sql2usertitlestatus(user_title.Status) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUserTitle500Response{}, nil - } - oapi_usertitle := oapi.UserTitleMini{ - Ctime: &user_title.Ctime, - Rate: user_title.Rate, - ReviewId: user_title.ReviewID, - Status: oapi_status, - TitleId: user_title.TitleID, - UserId: user_title.UserID, - } - - return oapi.GetUserTitle200JSONResponse(oapi_usertitle), nil -} - -// GetUsers implements oapi.StrictServerInterface. -func (s *Server) GetUsers(ctx context.Context, request oapi.GetUsersRequestObject) (oapi.GetUsersResponseObject, error) { - params := sqlc.SearchUserParams{ - Word: request.Params.Word, - Cursor: request.Params.CursorId, - Limit: request.Params.Limit, - } - _users, err := s.db.SearchUser(ctx, params) - if err != nil { - log.Errorf("%v", err) - return oapi.GetUsers500Response{}, nil - } - if len(_users) == 0 { - return oapi.GetUsers204Response{}, nil - } - - var users []oapi.User - var cursor int64 - for _, user := range _users { - oapi_user := oapi.User{ // maybe its possible to make one sqlc type and use one map func iinstead of this shit - // add image - CreationDate: &user.CreationDate, - DispName: user.DispName, - Id: &user.ID, - Mail: StringToEmail(user.Mail), - Nickname: user.Nickname, - UserDesc: user.UserDesc, - } - users = append(users, oapi_user) - - cursor = user.ID - } - - return oapi.GetUsers200JSONResponse{Data: users, Cursor: cursor}, nil + return oapi.GetUsersUserId200JSONResponse(mapUser(user)), nil } diff --git a/modules/backend/main.go b/modules/backend/main.go index e7e6ec8..42a66d3 100644 --- a/modules/backend/main.go +++ b/modules/backend/main.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "net/http" sqlc "nyanimedb/sql" "os" "reflect" @@ -11,63 +10,46 @@ import ( oapi "nyanimedb/api" handlers "nyanimedb/modules/backend/handlers" - middleware "nyanimedb/modules/backend/middlewares" - "nyanimedb/modules/backend/rmq" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5/pgxpool" + "github.com/jackc/pgx/v5" "github.com/pelletier/go-toml/v2" - "github.com/rabbitmq/amqp091-go" - log "github.com/sirupsen/logrus" ) var AppConfig Config func main() { - if len(os.Args) != 2 { - AppConfig.Mode = "env" - } else { - AppConfig.Mode = "argv" - } + // if len(os.Args) != 2 { + // AppConfig.Mode = "env" + // } else { + // AppConfig.Mode = "argv" + // } - err := InitConfig() - if err != nil { - log.Fatalf("Failed to init config: %v\n", err) - } + // err := InitConfig() + // if err != nil { + // log.Fatalf("Failed to init config: %v\n", err) + // } - pool, err := pgxpool.New(context.Background(), AppConfig.DdUrl) + conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL")) if err != nil { fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) os.Exit(1) } - defer pool.Close() + defer conn.Close(context.Background()) r := gin.Default() - if len(AppConfig.AuthEnabled) > 0 && AppConfig.AuthEnabled != "false" { - r.Use(middleware.CSRFMiddleware()) - r.Use(middleware.JWTAuthMiddleware(AppConfig.JwtPrivateKey)) - } + queries := sqlc.New(conn) - queries := sqlc.New(pool) - - rmqConn, err := amqp091.Dial(AppConfig.RmqURL) - if err != nil { - log.Fatalf("Failed to connect to RabbitMQ: %v", err) - } - defer rmqConn.Close() - - rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second) - - server := handlers.NewServer(queries, rpcClient) + server := handlers.NewServer(queries) + // r.LoadHTMLGlob("templates/*") r.Use(cors.New(cors.Config{ - AllowOrigins: []string{AppConfig.ServiceAddress}, - // AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH"}, - AllowHeaders: []string{"Origin", "Content-Type", "Accept", "X-XSRF-TOKEN"}, + AllowOrigins: []string{"*"}, // allow all origins, change to specific domains in production + AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, MaxAge: 12 * time.Hour, @@ -75,15 +57,27 @@ func main() { oapi.RegisterHandlers(r, oapi.NewStrictHandler( server, - + // сюда можно добавить middlewares, если нужно []oapi.StrictMiddlewareFunc{}, )) + // r.GET("/", func(c *gin.Context) { + // c.HTML(http.StatusOK, "index.html", gin.H{ + // "title": "Welcome Page", + // "message": "Hello, Gin with HTML templates!", + // }) + // }) - // Запуск - log.Infof("Server starting on :8080") - if err := r.Run(":8080"); err != nil && err != http.ErrServerClosed { - log.Fatalf("server failed: %v", err) - } + // r.GET("/api", func(c *gin.Context) { + // items := []Item{ + // {ID: 1, Title: "First Item", Description: "This is the description of the first item."}, + // {ID: 2, Title: "Second Item", Description: "This is the description of the second item."}, + // {ID: 3, Title: "Third Item", Description: "This is the description of the third item."}, + // } + + // c.JSON(http.StatusOK, items) + // }) + + r.Run(":8080") } func InitConfig() error { diff --git a/modules/backend/middlewares/access.go b/modules/backend/middlewares/access.go deleted file mode 100644 index 73200e8..0000000 --- a/modules/backend/middlewares/access.go +++ /dev/null @@ -1,109 +0,0 @@ -package middleware - -import ( - "context" - "errors" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" -) - -// ctxKey — приватный тип для ключа контекста -type ctxKey struct{} - -// ginContextKey — уникальный ключ для хранения *gin.Context -var ginContextKey = &ctxKey{} - -// GinContextToContext сохраняет *gin.Context в context.Context запроса -func GinContextToContext(c *gin.Context) { - ctx := context.WithValue(c.Request.Context(), ginContextKey, c) - c.Request = c.Request.WithContext(ctx) -} - -// GinContextFromContext извлекает *gin.Context из context.Context -func GinContextFromContext(ctx context.Context) (*gin.Context, bool) { - ginCtx, ok := ctx.Value(ginContextKey).(*gin.Context) - return ginCtx, ok -} - -func JWTAuthMiddleware(secret string) gin.HandlerFunc { - return func(c *gin.Context) { - // 1. Получаем access_token из cookie - tokenStr, err := c.Cookie("access_token") - if err != nil { - abortWithJSON(c, http.StatusUnauthorized, "missing access_token cookie") - return - } - - // 2. Парсим токен с MapClaims - token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { - if t.Method != jwt.SigningMethodHS256 { - return nil, errors.New("unexpected signing method: " + t.Method.Alg()) - } - return []byte(secret), nil // ← конвертируем string → []byte - }) - if err != nil { - abortWithJSON(c, http.StatusUnauthorized, "invalid token: "+err.Error()) - return - } - - // 3. Проверяем валидность - if !token.Valid { - abortWithJSON(c, http.StatusUnauthorized, "token is invalid") - return - } - - // 4. Извлекаем user_id из claims - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - abortWithJSON(c, http.StatusUnauthorized, "invalid claims format") - return - } - - userID, ok := claims["user_id"].(string) - if !ok || userID == "" { - abortWithJSON(c, http.StatusUnauthorized, "user_id claim missing or invalid") - return - } - - // 5. Сохраняем в контексте - c.Set("user_id", userID) - - // 6. Для oapi-codegen — кладём gin.Context в request context - GinContextToContext(c) - - c.Next() - } -} - -// Вспомогательные функции (без изменений) -func UserIDFromGin(c *gin.Context) (string, bool) { - id, exists := c.Get("user_id") - if !exists { - return "", false - } - if s, ok := id.(string); ok { - return s, true - } - return "", false -} - -func UserIDFromContext(ctx context.Context) (string, error) { - ginCtx, ok := GinContextFromContext(ctx) - if !ok { - return "", errors.New("gin context not found") - } - userID, ok := UserIDFromGin(ginCtx) - if !ok { - return "", errors.New("user_id not found in context") - } - return userID, nil -} - -func abortWithJSON(c *gin.Context, code int, message string) { - c.AbortWithStatusJSON(code, gin.H{ - "error": "unauthorized", - "message": message, - }) -} diff --git a/modules/backend/middlewares/csrf.go b/modules/backend/middlewares/csrf.go deleted file mode 100644 index 41fad7b..0000000 --- a/modules/backend/middlewares/csrf.go +++ /dev/null @@ -1,70 +0,0 @@ -package middleware - -import ( - "crypto/subtle" - "net/http" - - "github.com/gin-gonic/gin" -) - -// CSRFMiddleware для Gin -func CSRFMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - // Пропускаем безопасные методы - if !isStateChangingMethod(c.Request.Method) { - c.Next() - return - } - - // 1. Получаем токен из заголовка - headerToken := c.GetHeader("X-XSRF-TOKEN") - if headerToken == "" { - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ - "error": "missing X-XSRF-TOKEN header", - }) - return - } - - // 2. Получаем токен из cookie - cookie, err := c.Cookie("xsrf_token") - if err != nil { - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ - "error": "missing xsrf_token cookie", - }) - return - } - - // 3. Безопасное сравнение - if subtle.ConstantTimeCompare([]byte(headerToken), []byte(cookie)) != 1 { - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ - "error": "CSRF token mismatch", - }) - return - } - - // 4. Опционально: сохраняем токен в контексте - c.Set("csrf_token", headerToken) - c.Next() - } -} - -func isStateChangingMethod(method string) bool { - switch method { - case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete: - return true - default: - return false - } -} - -// CSRFTokenFromGin извлекает токен из Gin context -func CSRFTokenFromGin(c *gin.Context) (string, bool) { - token, exists := c.Get("xsrf_token") - if !exists { - return "", false - } - if s, ok := token.(string); ok { - return s, true - } - return "", false -} diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 19971e5..b1dd8af 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -1,7 +1,7 @@ -- name: GetImageByID :one SELECT id, storage_type, image_path FROM images -WHERE id = sqlc.arg('illust_id')::bigint; +WHERE id = $1; -- name: CreateImage :one INSERT INTO images (storage_type, image_path) @@ -9,428 +9,134 @@ VALUES ($1, $2) RETURNING id, storage_type, image_path; -- name: GetUserByID :one -SELECT - t.id as id, - t.avatar_id as avatar_id, - t.mail as mail, - t.nickname as nickname, - t.disp_name as disp_name, - t.user_desc as user_desc, - t.creation_date as creation_date, - i.storage_type as storage_type, - i.image_path as image_path -FROM users as t -LEFT JOIN images as i ON (t.avatar_id = i.id) -WHERE t.id = sqlc.arg('id')::bigint; +SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date +FROM users +WHERE id = $1; --- name: SearchUser :many -SELECT - u.id AS id, - u.avatar_id AS avatar_id, - u.mail AS mail, - u.nickname AS nickname, - u.disp_name AS disp_name, - u.user_desc AS user_desc, - u.creation_date AS creation_date, - i.storage_type AS storage_type, - i.image_path AS image_path -FROM users AS u -LEFT JOIN images AS i ON u.avatar_id = i.id -WHERE - ( - sqlc.narg('word')::text IS NULL - OR ( - SELECT bool_and( - u.nickname ILIKE ('%' || term || '%') - OR u.disp_name ILIKE ('%' || term || '%') - ) - FROM unnest(string_to_array(trim(sqlc.narg('word')::text), ' ')) AS term - WHERE term <> '' - ) - ) - AND ( - sqlc.narg('cursor')::int IS NULL - OR u.id > sqlc.narg('cursor')::int - ) -ORDER BY u.id ASC -LIMIT COALESCE(sqlc.narg('limit')::int, 20); +-- -- name: ListUsers :many +-- SELECT user_id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date +-- FROM users +-- ORDER BY user_id +-- LIMIT $1 OFFSET $2; --- name: GetStudioByID :one -SELECT * -FROM studios -WHERE id = sqlc.arg('studio_id')::bigint; +-- -- name: CreateUser :one +-- INSERT INTO users (avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date) +-- VALUES ($1, $2, $3, $4, $5, $6, $7) +-- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; --- name: InsertStudio :one -INSERT INTO studios (studio_name, illust_id, studio_desc) -VALUES ( - sqlc.arg('studio_name')::text, - sqlc.narg('illust_id')::bigint, - sqlc.narg('studio_desc')::text) -RETURNING id, studio_name, illust_id, studio_desc; +-- -- name: UpdateUser :one +-- UPDATE users +-- SET +-- avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id), +-- disp_name = COALESCE(sqlc.narg('disp_name'), disp_name), +-- user_desc = COALESCE(sqlc.narg('user_desc'), user_desc), +-- passhash = COALESCE(sqlc.narg('passhash'), passhash) +-- WHERE user_id = sqlc.arg('user_id') +-- RETURNING user_id, avatar_id, nickname, disp_name, user_desc, creation_date; --- name: GetTitleTags :many -SELECT - tag_names -FROM tags as g -JOIN title_tags as t ON(t.tag_id = g.id) -WHERE t.title_id = sqlc.arg('title_id')::bigint; +-- -- name: DeleteUser :exec +-- DELETE FROM users +-- WHERE user_id = $1; --- name: InsertTitleTags :one -INSERT INTO title_tags (title_id, tag_id) -VALUES ( - sqlc.arg('title_id')::bigint, - sqlc.arg('tag_id')::bigint) -RETURNING title_id, tag_id; +-- -- name: GetTitleByID :one +-- SELECT title_id, title_names, studio_id, poster_id, signal_ids, +-- title_status, rating, rating_count, release_year, release_season, +-- season, episodes_aired, episodes_all, episodes_len +-- FROM titles +-- WHERE title_id = $1; --- name: InsertTag :one -INSERT INTO tags (tag_names) -VALUES ( - sqlc.arg('tag_names')::jsonb) -RETURNING id, tag_names; +-- -- name: ListTitles :many +-- SELECT title_id, title_names, studio_id, poster_id, signal_ids, +-- title_status, rating, rating_count, release_year, release_season, +-- season, episodes_aired, episodes_all, episodes_len +-- FROM titles +-- ORDER BY title_id +-- LIMIT $1 OFFSET $2; --- name: UpdateUser :one -UPDATE users -SET - avatar_id = COALESCE(sqlc.narg('avatar_id'), avatar_id), - disp_name = COALESCE(sqlc.narg('disp_name'), disp_name), - user_desc = COALESCE(sqlc.narg('user_desc'), user_desc), - mail = COALESCE(sqlc.narg('mail'), mail) -WHERE id = sqlc.arg('user_id') -RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail; +-- -- name: UpdateTitle :one +-- UPDATE titles +-- SET +-- title_names = COALESCE(sqlc.narg('title_names'), title_names), +-- studio_id = COALESCE(sqlc.narg('studio_id'), studio_id), +-- poster_id = COALESCE(sqlc.narg('poster_id'), poster_id), +-- signal_ids = COALESCE(sqlc.narg('signal_ids'), signal_ids), +-- title_status = COALESCE(sqlc.narg('title_status'), title_status), +-- release_year = COALESCE(sqlc.narg('release_year'), release_year), +-- release_season = COALESCE(sqlc.narg('release_season'), release_season), +-- episodes_aired = COALESCE(sqlc.narg('episodes_aired'), episodes_aired), +-- episodes_all = COALESCE(sqlc.narg('episodes_all'), episodes_all), +-- episodes_len = COALESCE(sqlc.narg('episodes_len'), episodes_len) +-- WHERE title_id = sqlc.arg('title_id') +-- RETURNING *; --- name: GetTitleByID :one --- sqlc.struct: TitlesFull -SELECT - t.*, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name, - s.illust_id as studio_illust_id, - s.studio_desc as studio_desc, - si.storage_type as studio_storage_type, - si.image_path as studio_image_path +-- -- name: GetReviewByID :one +-- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date +-- FROM reviews +-- WHERE review_id = $1; -FROM titles as t -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) -LEFT JOIN images as si ON (s.illust_id = si.id) +-- -- name: CreateReview :one +-- INSERT INTO reviews (user_id, title_id, image_ids, review_text, creation_date) +-- VALUES ($1, $2, $3, $4, $5) +-- RETURNING review_id, user_id, title_id, image_ids, review_text, creation_date; -WHERE t.id = sqlc.arg('title_id')::bigint -GROUP BY - t.id, i.id, s.id, si.id; +-- -- name: UpdateReview :one +-- UPDATE reviews +-- SET +-- image_ids = COALESCE(sqlc.narg('image_ids'), image_ids), +-- review_text = COALESCE(sqlc.narg('review_text'), review_text) +-- WHERE review_id = sqlc.arg('review_id') +-- RETURNING *; --- name: SearchTitles :many -SELECT - t.id as id, - t.title_names as title_names, - t.poster_id as poster_id, - t.title_status as title_status, - t.rating as rating, - t.rating_count as rating_count, - t.release_year as release_year, - t.release_season as release_season, - t.season as season, - t.episodes_aired as episodes_aired, - t.episodes_all as episodes_all, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name +-- -- name: DeleteReview :exec +-- DELETE FROM reviews +-- WHERE review_id = $1; -FROM titles as t -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) +-- -- name: ListReviewsByTitle :many +-- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date +-- FROM reviews +-- WHERE title_id = $1 +-- ORDER BY creation_date DESC +-- LIMIT $2 OFFSET $3; -WHERE - CASE - WHEN sqlc.arg('forward')::boolean THEN - -- forward: greater than cursor (next page) - CASE sqlc.arg('sort_by')::text - WHEN 'year' THEN - (sqlc.narg('cursor_year')::int IS NULL) OR - (t.release_year > sqlc.narg('cursor_year')::int) OR - (t.release_year = sqlc.narg('cursor_year')::int AND t.id > sqlc.narg('cursor_id')::bigint) +-- -- name: ListReviewsByUser :many +-- SELECT review_id, user_id, title_id, image_ids, review_text, creation_date +-- FROM reviews +-- WHERE user_id = $1 +-- ORDER BY creation_date DESC +-- LIMIT $2 OFFSET $3; - WHEN 'rating' THEN - (sqlc.narg('cursor_rating')::float IS NULL) OR - (t.rating > sqlc.narg('cursor_rating')::float) OR - (t.rating = sqlc.narg('cursor_rating')::float AND t.id > sqlc.narg('cursor_id')::bigint) +-- -- name: GetUserTitle :one +-- SELECT usertitle_id, user_id, title_id, status, rate, review_id +-- FROM usertitles +-- WHERE user_id = $1 AND title_id = $2; - WHEN 'id' THEN - (sqlc.narg('cursor_id')::bigint IS NULL) OR - (t.id > sqlc.narg('cursor_id')::bigint) +-- -- name: ListUserTitles :many +-- SELECT usertitle_id, user_id, title_id, status, rate, review_id +-- FROM usertitles +-- WHERE user_id = $1 +-- ORDER BY usertitle_id +-- LIMIT $2 OFFSET $3; - ELSE true -- fallback - END +-- -- name: CreateUserTitle :one +-- INSERT INTO usertitles (user_id, title_id, status, rate, review_id) +-- VALUES ($1, $2, $3, $4, $5) +-- RETURNING usertitle_id, user_id, title_id, status, rate, review_id; - ELSE - -- backward: less than cursor (prev page) - CASE sqlc.arg('sort_by')::text - WHEN 'year' THEN - (sqlc.narg('cursor_year')::int IS NULL) OR - (t.release_year < sqlc.narg('cursor_year')::int) OR - (t.release_year = sqlc.narg('cursor_year')::int AND t.id < sqlc.narg('cursor_id')::bigint) +-- -- name: UpdateUserTitle :one +-- UPDATE usertitles +-- SET +-- status = COALESCE(sqlc.narg('status'), status), +-- rate = COALESCE(sqlc.narg('rate'), rate), +-- review_id = COALESCE(sqlc.narg('review_id'), review_id) +-- WHERE user_id = $1 AND title_id = $2 +-- RETURNING *; - WHEN 'rating' THEN - (sqlc.narg('cursor_rating')::float IS NULL) OR - (t.rating < sqlc.narg('cursor_rating')::float) OR - (t.rating = sqlc.narg('cursor_rating')::float AND t.id < sqlc.narg('cursor_id')::bigint) +-- -- name: DeleteUserTitle :exec +-- DELETE FROM usertitles +-- WHERE user_id = $1 AND ($2::int IS NULL OR title_id = $2); - WHEN 'id' THEN - (sqlc.narg('cursor_id')::bigint IS NULL) OR - (t.id < sqlc.narg('cursor_id')::bigint) - - ELSE true - END - END - - AND ( - CASE - WHEN sqlc.narg('word')::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true - END - ) - - AND ( - sqlc.narg('title_statuses')::title_status_t[] IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) = 0 - OR t.title_status = ANY(sqlc.narg('title_statuses')::title_status_t[]) - ) - AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) - AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) - AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) - -GROUP BY - t.id, i.id, s.id - -ORDER BY - CASE WHEN sqlc.arg('forward')::boolean THEN - CASE - WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id - WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year - WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating - END - END ASC, - CASE WHEN NOT sqlc.arg('forward')::boolean THEN - CASE - WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id - WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year - WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating - END - END DESC, - - CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN t.id END ASC - -LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit - --- name: SearchUserTitles :many -SELECT - t.id as id, - t.title_names as title_names, - t.poster_id as poster_id, - t.title_status as title_status, - t.rating as rating, - t.rating_count as rating_count, - t.release_year as release_year, - t.release_season as release_season, - t.season as season, - t.episodes_aired as episodes_aired, - t.episodes_all as episodes_all, - u.user_id as user_id, - u.status as usertitle_status, - u.rate as user_rate, - u.review_id as review_id, - u.ctime as user_ctime, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name - -FROM usertitles as u -JOIN titles as t ON (u.title_id = t.id) -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) - -WHERE - u.user_id = sqlc.arg('user_id')::bigint - AND - CASE - WHEN sqlc.arg('forward')::boolean THEN - -- forward: greater than cursor (next page) - CASE sqlc.arg('sort_by')::text - WHEN 'year' THEN - (sqlc.narg('cursor_year')::int IS NULL) OR - (t.release_year > sqlc.narg('cursor_year')::int) OR - (t.release_year = sqlc.narg('cursor_year')::int AND t.id > sqlc.narg('cursor_id')::bigint) - - WHEN 'rating' THEN - (sqlc.narg('cursor_rating')::float IS NULL) OR - (t.rating > sqlc.narg('cursor_rating')::float) OR - (t.rating = sqlc.narg('cursor_rating')::float AND t.id > sqlc.narg('cursor_id')::bigint) - - WHEN 'id' THEN - (sqlc.narg('cursor_id')::bigint IS NULL) OR - (t.id > sqlc.narg('cursor_id')::bigint) - - ELSE true -- fallback - END - - ELSE - -- backward: less than cursor (prev page) - CASE sqlc.arg('sort_by')::text - WHEN 'year' THEN - (sqlc.narg('cursor_year')::int IS NULL) OR - (t.release_year < sqlc.narg('cursor_year')::int) OR - (t.release_year = sqlc.narg('cursor_year')::int AND t.id < sqlc.narg('cursor_id')::bigint) - - WHEN 'rating' THEN - (sqlc.narg('cursor_rating')::float IS NULL) OR - (t.rating < sqlc.narg('cursor_rating')::float) OR - (t.rating = sqlc.narg('cursor_rating')::float AND t.id < sqlc.narg('cursor_id')::bigint) - - WHEN 'id' THEN - (sqlc.narg('cursor_id')::bigint IS NULL) OR - (t.id < sqlc.narg('cursor_id')::bigint) - - ELSE true - END - END - - AND ( - CASE - WHEN sqlc.narg('word')::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array(sqlc.narg('word')::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true - END - ) - - AND ( - sqlc.narg('title_statuses')::title_status_t[] IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) IS NULL - OR array_length(sqlc.narg('title_statuses')::title_status_t[], 1) = 0 - OR t.title_status = ANY(sqlc.narg('title_statuses')::title_status_t[]) - ) - AND ( - sqlc.narg('usertitle_statuses')::usertitle_status_t[] IS NULL - OR array_length(sqlc.narg('usertitle_statuses')::usertitle_status_t[], 1) IS NULL - OR array_length(sqlc.narg('usertitle_statuses')::usertitle_status_t[], 1) = 0 - OR u.status = ANY(sqlc.narg('usertitle_statuses')::usertitle_status_t[]) - ) - AND (sqlc.narg('rate')::int IS NULL OR u.rate >= sqlc.narg('rate')::int) - AND (sqlc.narg('rating')::float IS NULL OR t.rating >= sqlc.narg('rating')::float) - AND (sqlc.narg('release_year')::int IS NULL OR t.release_year = sqlc.narg('release_year')::int) - AND (sqlc.narg('release_season')::release_season_t IS NULL OR t.release_season = sqlc.narg('release_season')::release_season_t) - -GROUP BY - t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id - -ORDER BY - CASE WHEN sqlc.arg('forward')::boolean THEN - CASE - WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id - WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year - WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating - WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate - END - END ASC, - CASE WHEN NOT sqlc.arg('forward')::boolean THEN - CASE - WHEN sqlc.arg('sort_by')::text = 'id' THEN t.id - WHEN sqlc.arg('sort_by')::text = 'year' THEN t.release_year - WHEN sqlc.arg('sort_by')::text = 'rating' THEN t.rating - WHEN sqlc.arg('sort_by')::text = 'rate' THEN u.rate - END - END DESC, - - CASE WHEN sqlc.arg('sort_by')::text <> 'id' THEN t.id END ASC - -LIMIT COALESCE(sqlc.narg('limit')::int, 100); -- 100 is default limit - --- name: GetReviewByID :one -SELECT * -FROM reviews -WHERE review_id = sqlc.arg('review_id')::bigint; - --- name: InsertUserTitle :one -INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ctime) -VALUES ( - sqlc.arg('user_id')::bigint, - sqlc.arg('title_id')::bigint, - sqlc.arg('status')::usertitle_status_t, - sqlc.narg('rate')::int, - sqlc.narg('review_id')::bigint, - sqlc.narg('ftime')::timestamptz -) -RETURNING user_id, title_id, status, rate, review_id, ctime; - --- name: UpdateUserTitle :one --- Fails with sql.ErrNoRows if (user_id, title_id) not found -UPDATE usertitles -SET - status = COALESCE(sqlc.narg('status')::usertitle_status_t, status), - rate = COALESCE(sqlc.narg('rate')::int, rate), - ctime = COALESCE(sqlc.narg('ftime')::timestamptz, ctime) -WHERE - user_id = sqlc.arg('user_id') - AND title_id = sqlc.arg('title_id') -RETURNING *; - --- name: DeleteUserTitle :one -DELETE FROM usertitles -WHERE user_id = sqlc.arg('user_id') - AND title_id = sqlc.arg('title_id') -RETURNING *; - --- name: GetUserTitleByID :one -SELECT - ut.* -FROM usertitles as ut -WHERE ut.title_id = sqlc.arg('title_id')::bigint AND ut.user_id = sqlc.arg('user_id')::bigint; \ No newline at end of file +-- -- name: ListTags :many +-- SELECT tag_id, tag_names +-- FROM tags +-- ORDER BY tag_id +-- LIMIT $1 OFFSET $2; \ No newline at end of file diff --git a/modules/backend/rmq/rabbit.go b/modules/backend/rmq/rabbit.go deleted file mode 100644 index 25abbdb..0000000 --- a/modules/backend/rmq/rabbit.go +++ /dev/null @@ -1,129 +0,0 @@ -package rmq - -import ( - "context" - "encoding/json" - "fmt" - "time" - - oapi "nyanimedb/api" - - amqp "github.com/rabbitmq/amqp091-go" -) - -const RPCQueueName = "anime_import_rpc" - -// RabbitRequest не меняем -type RabbitRequest struct { - Name string `json:"name"` - Statuses []oapi.TitleStatus `json:"statuses,omitempty"` - Rating float64 `json:"rating,omitempty"` - Year int32 `json:"year,omitempty"` - Season oapi.ReleaseSeason `json:"season,omitempty"` - Timestamp time.Time `json:"timestamp"` -} - -type RPCClient struct { - conn *amqp.Connection - timeout time.Duration -} - -func NewRPCClient(conn *amqp.Connection, timeout time.Duration) *RPCClient { - return &RPCClient{conn: conn, timeout: timeout} -} - -func (c *RPCClient) Call( - ctx context.Context, - request RabbitRequest, - replyPayload any, -) error { - - // 1. Канал для запроса и ответа - ch, err := c.conn.Channel() - if err != nil { - return fmt.Errorf("channel: %w", err) - } - defer ch.Close() - - // 2. Декларируем фиксированную очередь RPC (идемпотентно) - _, err = ch.QueueDeclare( - RPCQueueName, - true, // durable - false, // auto-delete - false, // exclusive - false, // no-wait - nil, - ) - if err != nil { - return fmt.Errorf("declare rpc queue: %w", err) - } - - // 3. Создаём временную очередь ДЛЯ ОТВЕТА - replyQueue, err := ch.QueueDeclare( - "", - false, - true, - true, - false, - nil, - ) - if err != nil { - return fmt.Errorf("declare reply queue: %w", err) - } - - // 4. Подписываемся на очередь ответов - msgs, err := ch.Consume( - replyQueue.Name, - "", - true, // auto-ack - true, // exclusive - false, - false, - nil, - ) - if err != nil { - return fmt.Errorf("consume reply: %w", err) - } - - // correlation ID - corrID := fmt.Sprintf("%d", time.Now().UnixNano()) - - // 5. сериализация запроса - body, err := json.Marshal(request) - if err != nil { - return fmt.Errorf("marshal request: %w", err) - } - - // 6. Публикация RPC-запроса - err = ch.Publish( - "", - RPCQueueName, // ← фиксированная очередь! - false, - false, - amqp.Publishing{ - ContentType: "application/json", - CorrelationId: corrID, - ReplyTo: replyQueue.Name, - Timestamp: time.Now(), - Body: body, - }, - ) - if err != nil { - return fmt.Errorf("publish: %w", err) - } - - // 7. Ждём ответ с таймаутом - timeoutCtx, cancel := context.WithTimeout(ctx, c.timeout) - defer cancel() - - for { - select { - case msg := <-msgs: - if msg.CorrelationId == corrID { - return json.Unmarshal(msg.Body, replyPayload) - } - case <-timeoutCtx.Done(): - return fmt.Errorf("rpc timeout: %w", timeoutCtx.Err()) - } - } -} diff --git a/modules/backend/types.go b/modules/backend/types.go index ceaec4e..20d3158 100644 --- a/modules/backend/types.go +++ b/modules/backend/types.go @@ -1,11 +1,12 @@ package main type Config struct { - Mode string - ServiceAddress string `toml:"ServiceAddress" env:"SERVICE_ADDRESS"` - DdUrl string `toml:"DbUrl" env:"DATABASE_URL"` - JwtPrivateKey string `toml:"JwtPrivateKey" env:"JWT_PRIVATE_KEY"` - LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` - RmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"` - AuthEnabled string `toml:"AuthEnabled" env:"AUTH_ENABLED"` + Mode string + LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"` +} + +type Item struct { + ID int `json:"id"` + Title string `json:"title"` + Description string `json:"description"` } diff --git a/modules/frontend/nginx-default.conf b/modules/frontend/nginx-default.conf index c3a851f..a538968 100644 --- a/modules/frontend/nginx-default.conf +++ b/modules/frontend/nginx-default.conf @@ -19,15 +19,6 @@ server { proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } - location /auth/ { - rewrite ^/auth/(.*)$ /$1 break; - proxy_pass http://nyanimedb-auth:8082/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - } #error_page 404 /404.html; error_page 500 502 503 504 /50x.html; diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index d2b5573..6a06afb 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -8,15 +8,10 @@ "name": "nyanimedb-frontend", "version": "0.0.0", "dependencies": { - "@headlessui/react": "^2.2.9", - "@heroicons/react": "^2.2.0", - "@tailwindcss/vite": "^4.1.17", "axios": "^1.12.2", "react": "^19.1.1", - "react-cookie": "^8.0.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.4", - "tailwindcss": "^4.1.17" + "react-router-dom": "^7.9.4" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -32,9 +27,6 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", "vite": "^7.1.7" - }, - "engines": { - "node": "20.x" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -345,6 +337,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -361,6 +354,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -377,6 +371,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -393,6 +388,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -409,6 +405,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -425,6 +422,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -441,6 +439,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -457,6 +456,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -473,6 +473,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -489,6 +490,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -505,6 +507,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -521,6 +524,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -537,6 +541,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -553,6 +558,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -569,6 +575,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -585,6 +592,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -601,6 +609,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -617,6 +626,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -633,6 +643,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -649,6 +660,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -665,6 +677,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -681,6 +694,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -697,6 +711,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -713,6 +728,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -729,6 +745,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -745,6 +762,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -911,88 +929,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.26.28", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" - }, - "node_modules/@headlessui/react": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", - "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.26.16", - "@react-aria/focus": "^3.20.2", - "@react-aria/interactions": "^3.25.0", - "@tanstack/react-virtual": "^3.13.9", - "use-sync-external-store": "^1.5.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/@heroicons/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", - "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", - "license": "MIT", - "peerDependencies": { - "react": ">= 16 || ^19.0.0-rc" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1049,6 +985,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1059,6 +996,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1069,6 +1007,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1078,12 +1017,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1135,103 +1076,6 @@ "node": ">= 8" } }, - "node_modules/@react-aria/focus": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", - "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/interactions": "^3.25.6", - "@react-aria/utils": "^3.31.0", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/interactions": { - "version": "3.25.6", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", - "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-aria/utils": "^3.31.0", - "@react-stately/flags": "^3.1.2", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/ssr": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", - "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/utils": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz", - "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-stately/flags": "^3.1.2", - "@react-stately/utils": "^3.10.8", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-stately/flags": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", - "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@react-stately/utils": { - "version": "3.10.8", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", - "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-types/shared": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", - "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.38", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", @@ -1246,6 +1090,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1259,6 +1104,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1272,6 +1118,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1285,6 +1132,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1298,6 +1146,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1311,6 +1160,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1324,6 +1174,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1337,6 +1188,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1350,6 +1202,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1363,6 +1216,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1376,6 +1230,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1389,6 +1244,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1402,6 +1258,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1415,6 +1272,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1428,6 +1286,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1441,6 +1300,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1454,6 +1314,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1467,6 +1328,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1480,6 +1342,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1493,6 +1356,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1506,6 +1370,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1519,305 +1384,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, - "node_modules/@swc/helpers": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", - "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", - "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.1", - "lightningcss": "1.30.2", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.17" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", - "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.17", - "@tailwindcss/oxide-darwin-arm64": "4.1.17", - "@tailwindcss/oxide-darwin-x64": "4.1.17", - "@tailwindcss/oxide-freebsd-x64": "4.1.17", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", - "@tailwindcss/oxide-linux-x64-musl": "4.1.17", - "@tailwindcss/oxide-wasm32-wasi": "4.1.17", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", - "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", - "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", - "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", - "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", - "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", - "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", - "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", - "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", - "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", - "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.6.0", - "@emnapi/runtime": "^1.6.0", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.0.7", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", - "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", - "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", - "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.1.17", - "@tailwindcss/oxide": "4.1.17", - "tailwindcss": "4.1.17" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", - "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", - "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1867,20 +1440,9 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", - "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", - "license": "MIT", - "dependencies": { - "hoist-non-react-statics": "^3.3.0" - }, - "peerDependencies": { - "@types/react": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1892,7 +1454,7 @@ "version": "24.7.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", - "devOptional": true, + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -1903,6 +1465,7 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -2443,15 +2006,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2536,6 +2090,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -2572,15 +2127,6 @@ "node": ">=0.4.0" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2602,19 +2148,6 @@ "dev": true, "license": "ISC" }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2664,6 +2197,7 @@ "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -3083,6 +2617,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3191,6 +2726,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -3271,15 +2807,6 @@ "node": ">= 0.4" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3357,15 +2884,6 @@ "dev": true, "license": "ISC" }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3470,255 +2988,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3752,15 +3021,6 @@ "yallist": "^3.0.2" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3849,6 +3109,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -3988,6 +3249,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -4007,6 +3269,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -4088,20 +3351,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-cookie": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.0.1.tgz", - "integrity": "sha512-QNdAd0MLuAiDiLcDU/2s/eyKmmfMHtjPUKJ2dZ/5CcQ9QKUium4B3o61/haq6PQl/YWFqC5PO8GvxeHKhy3GFA==", - "license": "MIT", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.6", - "hoist-non-react-statics": "^3.3.2", - "universal-cookie": "^8.0.0" - }, - "peerDependencies": { - "react": ">= 16.3.0" - } - }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", @@ -4115,12 +3364,6 @@ "react": "^19.2.0" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4194,6 +3437,7 @@ "version": "4.52.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -4314,6 +3558,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4345,35 +3590,11 @@ "node": ">=8" } }, - "node_modules/tabbable": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", - "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", - "license": "MIT" - }, - "node_modules/tailwindcss": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", - "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -4390,6 +3611,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -4407,6 +3629,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -4442,12 +3665,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4518,18 +3735,9 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "devOptional": true, + "dev": true, "license": "MIT" }, - "node_modules/universal-cookie": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-8.0.1.tgz", - "integrity": "sha512-B6ks9FLLnP1UbPPcveOidfvB9pHjP+wekP2uRYB9YDfKVpvcjKgy1W5Zj+cEXJ9KTPnqOKGfVDQBmn8/YCQfRg==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.2" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -4581,19 +3789,11 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/vite": { "version": "7.1.9", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -4669,6 +3869,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -4686,6 +3887,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "peer": true, "engines": { diff --git a/modules/frontend/package.json b/modules/frontend/package.json index af07b41..b4977aa 100644 --- a/modules/frontend/package.json +++ b/modules/frontend/package.json @@ -10,15 +10,10 @@ "preview": "vite preview" }, "dependencies": { - "@headlessui/react": "^2.2.9", - "@heroicons/react": "^2.2.0", - "@tailwindcss/vite": "^4.1.17", "axios": "^1.12.2", "react": "^19.1.1", - "react-cookie": "^8.0.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.4", - "tailwindcss": "^4.1.17" + "react-router-dom": "^7.9.4" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -34,8 +29,5 @@ "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", "vite": "^7.1.7" - }, - "engines": { - "node": "20.x" } } diff --git a/modules/frontend/src/App.css b/modules/frontend/src/App.css index e69de29..b9d355d 100644 --- a/modules/frontend/src/App.css +++ b/modules/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/modules/frontend/src/App.tsx b/modules/frontend/src/App.tsx index 84c9086..a88ad57 100644 --- a/modules/frontend/src/App.tsx +++ b/modules/frontend/src/App.tsx @@ -1,43 +1,15 @@ import React from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import UserPage from "./pages/UserPage/UserPage"; -import TitlesPage from "./pages/TitlesPage/TitlesPage"; -import TitlePage from "./pages/TitlePage/TitlePage"; -import { LoginPage } from "./pages/LoginPage/LoginPage"; -import { Header } from "./components/Header/Header"; - -// import { OpenAPI } from "./api"; - -// OpenAPI.WITH_CREDENTIALS = true +import UserPage from "./components/UserPage/UserPage"; const App: React.FC = () => { - const username = localStorage.getItem("username") || undefined; - const userId = localStorage.getItem("userId"); - return ( -
- {/* auth */} - } /> - } /> - {/*} />*/} - - {/* users */} - {/*} />*/} } /> - : } - /> - - {/* titles */} - } /> - } /> ); }; - export default App; \ No newline at end of file diff --git a/modules/frontend/src/api/client.gen.ts b/modules/frontend/src/api/client.gen.ts deleted file mode 100644 index 952c663..0000000 --- a/modules/frontend/src/api/client.gen.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig({ baseUrl: '/api/v1' })); diff --git a/modules/frontend/src/api/client/client.gen.ts b/modules/frontend/src/api/client/client.gen.ts deleted file mode 100644 index c2a5190..0000000 --- a/modules/frontend/src/api/client/client.gen.ts +++ /dev/null @@ -1,301 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { - Client, - Config, - RequestOptions, - ResolvedRequestOptions, -} from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - ResolvedRequestOptions - >(); - - const beforeRequest = async (options: RequestOptions) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined, - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - - return { opts, url }; - }; - - const request: Client['request'] = async (options) => { - // @ts-expect-error - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response: Response; - - try { - response = await _fetch(request); - } catch (error) { - // Handle fetch exceptions (AbortError, network errors, etc.) - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn( - error, - undefined as any, - request, - opts, - )) as unknown; - } - } - - finalError = finalError || ({} as unknown); - - if (opts.throwOnError) { - throw finalError; - } - - // Return error response - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - request, - response: undefined as any, - }; - } - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'json': - case 'text': - data = await response[parseAs](); - break; - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - const error = jsonError ?? textError; - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - const makeMethodFn = - (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = - (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - url, - }); - }; - - return { - buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/modules/frontend/src/api/client/index.ts b/modules/frontend/src/api/client/index.ts deleted file mode 100644 index b295ede..0000000 --- a/modules/frontend/src/api/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/modules/frontend/src/api/client/types.gen.ts b/modules/frontend/src/api/client/types.gen.ts deleted file mode 100644 index b4a499c..0000000 --- a/modules/frontend/src/api/client/types.gen.ts +++ /dev/null @@ -1,241 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { - Client as CoreClient, - Config as CoreConfig, -} from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, - CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: - | 'arrayBuffer' - | 'auto' - | 'blob' - | 'formData' - | 'json' - | 'stream' - | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record - ? TData[keyof TData] - : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? - | (TData extends Record - ? TData[keyof TData] - : TData) - | undefined - : ( - | { - data: TData extends Record - ? TData[keyof TData] - : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; - } - ) & { - request: Request; - response: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick< - Required>, - 'method' - >, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: TData & Options, -) => string; - -export type Client = CoreClient< - RequestFn, - Config, - MethodFn, - BuildUrlFn, - SseFn -> & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - ([TData] extends [never] ? unknown : Omit); diff --git a/modules/frontend/src/api/client/utils.gen.ts b/modules/frontend/src/api/client/utils.gen.ts deleted file mode 100644 index 4c48a9e..0000000 --- a/modules/frontend/src/api/client/utils.gen.ts +++ /dev/null @@ -1,332 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - const options = parameters[name] || args; - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'form', - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = ( - contentType: string | null, -): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if ( - cleanContent.startsWith('application/json') || - cleanContent.endsWith('+json') - ) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => - cleanContent.startsWith(type), - ) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { - headers: Headers; - }) => { - for (const auth of security) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -}; - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = - header instanceof Headers - ? headersEntries(header) - : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, -) => Err | Promise; - -type ReqInterceptor = ( - request: Req, - options: Options, -) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update( - id: number | Interceptor, - fn: Interceptor, - ): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/modules/frontend/src/auth/core/ApiError.ts b/modules/frontend/src/api/core/ApiError.ts similarity index 100% rename from modules/frontend/src/auth/core/ApiError.ts rename to modules/frontend/src/api/core/ApiError.ts diff --git a/modules/frontend/src/auth/core/ApiRequestOptions.ts b/modules/frontend/src/api/core/ApiRequestOptions.ts similarity index 100% rename from modules/frontend/src/auth/core/ApiRequestOptions.ts rename to modules/frontend/src/api/core/ApiRequestOptions.ts diff --git a/modules/frontend/src/auth/core/ApiResult.ts b/modules/frontend/src/api/core/ApiResult.ts similarity index 100% rename from modules/frontend/src/auth/core/ApiResult.ts rename to modules/frontend/src/api/core/ApiResult.ts diff --git a/modules/frontend/src/auth/core/CancelablePromise.ts b/modules/frontend/src/api/core/CancelablePromise.ts similarity index 100% rename from modules/frontend/src/auth/core/CancelablePromise.ts rename to modules/frontend/src/api/core/CancelablePromise.ts diff --git a/modules/frontend/src/auth/core/OpenAPI.ts b/modules/frontend/src/api/core/OpenAPI.ts similarity index 96% rename from modules/frontend/src/auth/core/OpenAPI.ts rename to modules/frontend/src/api/core/OpenAPI.ts index 79aa305..185e5c3 100644 --- a/modules/frontend/src/auth/core/OpenAPI.ts +++ b/modules/frontend/src/api/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: 'http://10.1.0.65:8081/auth', + BASE: '/api/v1', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/api/core/auth.gen.ts b/modules/frontend/src/api/core/auth.gen.ts deleted file mode 100644 index f8a7326..0000000 --- a/modules/frontend/src/api/core/auth.gen.ts +++ /dev/null @@ -1,42 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = - typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/modules/frontend/src/api/core/bodySerializer.gen.ts b/modules/frontend/src/api/core/bodySerializer.gen.ts deleted file mode 100644 index 552b50f..0000000 --- a/modules/frontend/src/api/core/bodySerializer.gen.ts +++ /dev/null @@ -1,100 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: any) => any; - -type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; -}; - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; -}; - -const serializeFormDataPair = ( - data: FormData, - key: string, - value: unknown, -): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): FormData => { - const data = new FormData(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => - typeof value === 'bigint' ? value.toString() : value, - ), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): string => { - const data = new URLSearchParams(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/modules/frontend/src/api/core/params.gen.ts b/modules/frontend/src/api/core/params.gen.ts deleted file mode 100644 index 602715c..0000000 --- a/modules/frontend/src/api/core/params.gen.ts +++ /dev/null @@ -1,176 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ('key' in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; - } else if ('allowExtra' in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/modules/frontend/src/api/core/pathSerializer.gen.ts b/modules/frontend/src/api/core/pathSerializer.gen.ts deleted file mode 100644 index 8d99931..0000000 --- a/modules/frontend/src/api/core/pathSerializer.gen.ts +++ /dev/null @@ -1,181 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; diff --git a/modules/frontend/src/api/core/queryKeySerializer.gen.ts b/modules/frontend/src/api/core/queryKeySerializer.gen.ts deleted file mode 100644 index d3bb683..0000000 --- a/modules/frontend/src/api/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,136 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if ( - value === undefined || - typeof value === 'function' || - typeof value === 'symbol' - ) { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => - a.localeCompare(b), - ); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = ( - value: unknown, -): JsonValue | undefined => { - if (value === null) { - return null; - } - - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - return value; - } - - if ( - value === undefined || - typeof value === 'function' || - typeof value === 'symbol' - ) { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if ( - typeof URLSearchParams !== 'undefined' && - value instanceof URLSearchParams - ) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/modules/frontend/src/auth/core/request.ts b/modules/frontend/src/api/core/request.ts similarity index 100% rename from modules/frontend/src/auth/core/request.ts rename to modules/frontend/src/api/core/request.ts diff --git a/modules/frontend/src/api/core/serverSentEvents.gen.ts b/modules/frontend/src/api/core/serverSentEvents.gen.ts deleted file mode 100644 index f8fd78e..0000000 --- a/modules/frontend/src/api/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,264 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit< - RequestInit, - 'method' -> & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult< - TData = unknown, - TReturn = void, - TNext = unknown, -> = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export const createSseClient = ({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult => { - let lastEventId: string | undefined; - - const sleep = - sseSleepFn ?? - ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) - throw new Error( - `SSE failed: ${response.status} ${response.statusText}`, - ); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body - .pipeThrough(new TextDecoderStream()) - .getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt( - line.replace(/^retry:\s*/, ''), - 10, - ); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if ( - sseMaxRetryAttempts !== undefined && - attempt >= sseMaxRetryAttempts - ) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min( - retryDelay * 2 ** (attempt - 1), - sseMaxRetryDelay ?? 30000, - ); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -}; diff --git a/modules/frontend/src/api/core/types.gen.ts b/modules/frontend/src/api/core/types.gen.ts deleted file mode 100644 index 643c070..0000000 --- a/modules/frontend/src/api/core/types.gen.ts +++ /dev/null @@ -1,118 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] - ? { sse?: never } - : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true - ? never - : K]: T[K]; -}; diff --git a/modules/frontend/src/api/core/utils.gen.ts b/modules/frontend/src/api/core/utils.gen.ts deleted file mode 100644 index 0b5389d..0000000 --- a/modules/frontend/src/api/core/utils.gen.ts +++ /dev/null @@ -1,143 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e. client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/modules/frontend/src/api/index.ts b/modules/frontend/src/api/index.ts index c352c10..e4f4ef4 100644 --- a/modules/frontend/src/api/index.ts +++ b/modules/frontend/src/api/index.ts @@ -1,4 +1,16 @@ -// This file is auto-generated by @hey-api/openapi-ts +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export { ApiError } from './core/ApiError'; +export { CancelablePromise, CancelError } from './core/CancelablePromise'; +export { OpenAPI } from './core/OpenAPI'; +export type { OpenAPIConfig } from './core/OpenAPI'; -export type * from './types.gen'; -export * from './sdk.gen'; +export type { Review } from './models/Review'; +export type { Tag } from './models/Tag'; +export type { Title } from './models/Title'; +export type { User } from './models/User'; +export type { UserTitle } from './models/UserTitle'; + +export { DefaultService } from './services/DefaultService'; diff --git a/modules/frontend/src/api/models/Review.ts b/modules/frontend/src/api/models/Review.ts new file mode 100644 index 0000000..9b453b7 --- /dev/null +++ b/modules/frontend/src/api/models/Review.ts @@ -0,0 +1,5 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Review = Record; diff --git a/modules/frontend/src/api/models/Tag.ts b/modules/frontend/src/api/models/Tag.ts new file mode 100644 index 0000000..9560ea8 --- /dev/null +++ b/modules/frontend/src/api/models/Tag.ts @@ -0,0 +1,5 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Tag = Record; diff --git a/modules/frontend/src/api/models/Title.ts b/modules/frontend/src/api/models/Title.ts new file mode 100644 index 0000000..4da7aa3 --- /dev/null +++ b/modules/frontend/src/api/models/Title.ts @@ -0,0 +1,5 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Title = Record; diff --git a/modules/frontend/src/api/models/User.ts b/modules/frontend/src/api/models/User.ts new file mode 100644 index 0000000..b03d22f --- /dev/null +++ b/modules/frontend/src/api/models/User.ts @@ -0,0 +1,35 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type User = { + /** + * Unique user ID (primary key) + */ + id?: number; + /** + * ID of the user avatar (references images table) + */ + avatar_id?: number | null; + /** + * User email + */ + mail?: string; + /** + * Username (alphanumeric + _ or -) + */ + nickname: string; + /** + * Display name + */ + disp_name?: string; + /** + * User description + */ + user_desc?: string; + /** + * Timestamp when the user was created + */ + creation_date: string; +}; + diff --git a/modules/frontend/src/api/models/UserTitle.ts b/modules/frontend/src/api/models/UserTitle.ts new file mode 100644 index 0000000..26d5ddc --- /dev/null +++ b/modules/frontend/src/api/models/UserTitle.ts @@ -0,0 +1,5 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type UserTitle = Record; diff --git a/modules/frontend/src/api/sdk.gen.ts b/modules/frontend/src/api/sdk.gen.ts deleted file mode 100644 index 7d46120..0000000 --- a/modules/frontend/src/api/sdk.gen.ts +++ /dev/null @@ -1,115 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersData, GetUsersErrors, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUsersResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -/** - * Get titles - */ -export const getTitles = (options?: Options) => (options?.client ?? client).get({ - querySerializer: { parameters: { status: { array: { explode: false } } } }, - url: '/titles', - ...options -}); - -/** - * Get title description - */ -export const getTitle = (options: Options) => (options.client ?? client).get({ url: '/titles/{title_id}', ...options }); - -/** - * Search user by nickname or dispname (both in one param), response is always sorted by id - */ -export const getUsers = (options?: Options) => (options?.client ?? client).get({ url: '/users/', ...options }); - -/** - * Get user info - */ -export const getUsersId = (options: Options) => (options.client ?? client).get({ url: '/users/{user_id}', ...options }); - -/** - * Partially update a user account - * - * Update selected user profile fields (excluding password). - * Password updates must be done via the dedicated auth-service (`/auth/`). - * Fields not provided in the request body remain unchanged. - * - */ -export const updateUser = (options: Options) => (options.client ?? client).patch({ - security: [{ name: 'X-XSRF-TOKEN', type: 'apiKey' }], - url: '/users/{user_id}', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Get user titles - */ -export const getUserTitles = (options: Options) => (options.client ?? client).get({ - querySerializer: { parameters: { status: { array: { explode: false } }, watch_status: { array: { explode: false } } } }, - url: '/users/{user_id}/titles', - ...options -}); - -/** - * Add a title to a user - * - * User adding title to list af watched, status required - */ -export const addUserTitle = (options: Options) => (options.client ?? client).post({ - url: '/users/{user_id}/titles', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Delete a usertitle - * - * User deleting title from list of watched - */ -export const deleteUserTitle = (options: Options) => (options.client ?? client).delete({ - security: [{ name: 'X-XSRF-TOKEN', type: 'apiKey' }], - url: '/users/{user_id}/titles/{title_id}', - ...options -}); - -/** - * Get user title - */ -export const getUserTitle = (options: Options) => (options.client ?? client).get({ url: '/users/{user_id}/titles/{title_id}', ...options }); - -/** - * Update a usertitle - * - * User updating title list of watched - */ -export const updateUserTitle = (options: Options) => (options.client ?? client).patch({ - security: [{ name: 'X-XSRF-TOKEN', type: 'apiKey' }], - url: '/users/{user_id}/titles/{title_id}', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts new file mode 100644 index 0000000..7ebd129 --- /dev/null +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -0,0 +1,35 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { User } from '../models/User'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class DefaultService { + /** + * Get user info + * @param userId + * @param fields + * @returns User User info + * @throws ApiError + */ + public static getUsers( + userId: string, + fields: string = 'all', + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/users/{user_id}', + path: { + 'user_id': userId, + }, + query: { + 'fields': fields, + }, + errors: { + 404: `User not found`, + }, + }); + } +} diff --git a/modules/frontend/src/api/types.gen.ts b/modules/frontend/src/api/types.gen.ts deleted file mode 100644 index d4526a7..0000000 --- a/modules/frontend/src/api/types.gen.ts +++ /dev/null @@ -1,620 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: `${string}://${string}/api/v1` | (string & {}); -}; - -/** - * Title sort order - */ -export type TitleSort = 'id' | 'year' | 'rating' | 'views'; - -/** - * Title status - */ -export type TitleStatus = 'finished' | 'ongoing' | 'planned'; - -/** - * Title release season - */ -export type ReleaseSeason = 'winter' | 'spring' | 'summer' | 'fall'; - -/** - * Image storage type - */ -export type StorageType = 's3' | 'local'; - -export type Image = { - id?: number; - storage_type?: StorageType; - image_path?: string; -}; - -export type Studio = { - id: number; - name: string; - poster?: Image; - description?: string; -}; - -/** - * A localized tag: keys are language codes (ISO 639-1), values are tag names - */ -export type Tag = { - [key: string]: string; -}; - -/** - * Array of localized tags - */ -export type Tags = Array; - -export type Title = { - /** - * Unique title ID (primary key) - */ - id: number; - /** - * Localized titles. Key = language (ISO 639-1), value = list of names - */ - title_names: { - [key: string]: Array; - }; - /** - * Localized description. Key = language (ISO 639-1), value = description. - */ - title_desc?: { - [key: string]: string; - }; - studio?: Studio; - tags: Tags; - poster?: Image; - title_status?: TitleStatus; - rating?: number; - rating_count?: number; - release_year?: number; - release_season?: ReleaseSeason; - episodes_aired?: number; - episodes_all?: number; - episodes_len?: { - [key: string]: number; - }; -}; - -export type CursorObj = { - id: number; - param?: string; -}; - -export type User = { - /** - * Unique user ID (primary key) - */ - id?: number; - image?: Image; - /** - * User email - */ - mail?: string; - /** - * Username (alphanumeric + _ or -) - */ - nickname: string; - /** - * Display name - */ - disp_name?: string; - /** - * User description - */ - user_desc?: string; - /** - * Timestamp when the user was created - */ - creation_date?: string; -}; - -/** - * User's title status - */ -export type UserTitleStatus = 'finished' | 'planned' | 'dropped' | 'in-progress'; - -export type UserTitle = { - user_id: number; - title?: Title; - status: UserTitleStatus; - rate?: number; - review_id?: number; - ctime?: string; -}; - -export type UserTitleMini = { - user_id: number; - title_id: number; - status: UserTitleStatus; - rate?: number; - review_id?: number; - ctime?: string; -}; - -export type Review = { - [key: string]: unknown; -}; - -export type Cursor = string; - -export type TitleSort2 = TitleSort; - -export type GetTitlesData = { - body?: never; - path?: never; - query?: { - cursor?: string; - sort?: TitleSort; - sort_forward?: boolean; - ext_search?: boolean; - word?: string; - /** - * List of title statuses to filter - */ - status?: Array; - rating?: number; - release_year?: number; - release_season?: ReleaseSeason; - limit?: number; - offset?: number; - fields?: string; - }; - url: '/titles'; -}; - -export type GetTitlesErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetTitlesResponses = { - /** - * List of titles with cursor - */ - 200: { - /** - * List of titles - */ - data: Array; - cursor: CursorObj; - }; - /** - * No titles found - */ - 204: void; -}; - -export type GetTitlesResponse = GetTitlesResponses[keyof GetTitlesResponses]; - -export type GetTitleData = { - body?: never; - path: { - title_id: number; - }; - query?: { - fields?: string; - }; - url: '/titles/{title_id}'; -}; - -export type GetTitleErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * Title not found - */ - 404: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetTitleResponses = { - /** - * Title description - */ - 200: Title; - /** - * No title found - */ - 204: void; -}; - -export type GetTitleResponse = GetTitleResponses[keyof GetTitleResponses]; - -export type GetUsersData = { - body?: never; - path?: never; - query?: { - word?: string; - limit?: number; - /** - * pass cursor naked - */ - cursor_id?: number; - }; - url: '/users/'; -}; - -export type GetUsersErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetUsersResponses = { - /** - * List of users with cursor - */ - 200: { - /** - * List of users - */ - data: Array<User>; - cursor: number; - }; - /** - * No users found - */ - 204: void; -}; - -export type GetUsersResponse = GetUsersResponses[keyof GetUsersResponses]; - -export type GetUsersIdData = { - body?: never; - path: { - user_id: string; - }; - query?: { - fields?: string; - }; - url: '/users/{user_id}'; -}; - -export type GetUsersIdErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * User not found - */ - 404: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetUsersIdResponses = { - /** - * User info - */ - 200: User; -}; - -export type GetUsersIdResponse = GetUsersIdResponses[keyof GetUsersIdResponses]; - -export type UpdateUserData = { - /** - * Only provided fields are updated. Omitted fields remain unchanged. - */ - body: { - /** - * ID of the user avatar (references `images.id`); set to `null` to remove avatar - */ - avatar_id?: number | null; - /** - * User email (must be unique and valid) - */ - mail?: string; - /** - * Username (alphanumeric + `_` or `-`, 3–16 chars) - */ - nickname?: string; - /** - * Display name - */ - disp_name?: string; - /** - * User description / bio - */ - user_desc?: string; - }; - path: { - /** - * User ID (primary key) - */ - user_id: number; - }; - query?: never; - url: '/users/{user_id}'; -}; - -export type UpdateUserErrors = { - /** - * Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON) - */ - 400: unknown; - /** - * Unauthorized — missing or invalid authentication token - */ - 401: unknown; - /** - * Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights) - */ - 403: unknown; - /** - * User not found - */ - 404: unknown; - /** - * Conflict — e.g., requested `nickname` or `mail` already taken by another user - */ - 409: unknown; - /** - * Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`) - */ - 422: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type UpdateUserResponses = { - /** - * User updated successfully. Returns updated user representation (excluding sensitive fields). - */ - 200: User; -}; - -export type UpdateUserResponse = UpdateUserResponses[keyof UpdateUserResponses]; - -export type GetUserTitlesData = { - body?: never; - path: { - user_id: string; - }; - query?: { - cursor?: string; - sort?: TitleSort; - sort_forward?: boolean; - word?: string; - /** - * List of title statuses to filter - */ - status?: Array<TitleStatus>; - watch_status?: Array<UserTitleStatus>; - rating?: number; - my_rate?: number; - release_year?: number; - release_season?: ReleaseSeason; - limit?: number; - fields?: string; - }; - url: '/users/{user_id}/titles'; -}; - -export type GetUserTitlesErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * User not found - */ - 404: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetUserTitlesResponses = { - /** - * List of user titles - */ - 200: { - data: Array<UserTitle>; - cursor: CursorObj; - }; - /** - * No titles found - */ - 204: void; -}; - -export type GetUserTitlesResponse = GetUserTitlesResponses[keyof GetUserTitlesResponses]; - -export type AddUserTitleData = { - body: { - title_id: number; - status: UserTitleStatus; - rate?: number; - }; - path: { - /** - * ID of the user to assign the title to - */ - user_id: number; - }; - query?: never; - url: '/users/{user_id}/titles'; -}; - -export type AddUserTitleErrors = { - /** - * Invalid request body (missing fields, invalid types, etc.) - */ - 400: unknown; - /** - * Unauthorized — missing or invalid auth token - */ - 401: unknown; - /** - * Forbidden — user not allowed to assign titles to this user - */ - 403: unknown; - /** - * User or Title not found - */ - 404: unknown; - /** - * Conflict — title already assigned to user (if applicable) - */ - 409: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type AddUserTitleResponses = { - /** - * Title successfully added to user - */ - 200: UserTitleMini; -}; - -export type AddUserTitleResponse = AddUserTitleResponses[keyof AddUserTitleResponses]; - -export type DeleteUserTitleData = { - body?: never; - path: { - user_id: number; - title_id: number; - }; - query?: never; - url: '/users/{user_id}/titles/{title_id}'; -}; - -export type DeleteUserTitleErrors = { - /** - * Unauthorized — missing or invalid auth token - */ - 401: unknown; - /** - * Forbidden — user not allowed to delete title - */ - 403: unknown; - /** - * User or Title not found - */ - 404: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type DeleteUserTitleResponses = { - /** - * Title successfully deleted - */ - 200: unknown; -}; - -export type GetUserTitleData = { - body?: never; - path: { - user_id: number; - title_id: number; - }; - query?: never; - url: '/users/{user_id}/titles/{title_id}'; -}; - -export type GetUserTitleErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * User or title not found - */ - 404: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetUserTitleResponses = { - /** - * User titles - */ - 200: UserTitleMini; - /** - * No user title found - */ - 204: void; -}; - -export type GetUserTitleResponse = GetUserTitleResponses[keyof GetUserTitleResponses]; - -export type UpdateUserTitleData = { - body: { - status?: UserTitleStatus; - rate?: number; - }; - path: { - user_id: number; - title_id: number; - }; - query?: never; - url: '/users/{user_id}/titles/{title_id}'; -}; - -export type UpdateUserTitleErrors = { - /** - * Invalid request body (missing fields, invalid types, etc.) - */ - 400: unknown; - /** - * Unauthorized — missing or invalid auth token - */ - 401: unknown; - /** - * Forbidden — user not allowed to update title - */ - 403: unknown; - /** - * User or Title not found - */ - 404: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type UpdateUserTitleResponses = { - /** - * Title successfully updated - */ - 200: UserTitleMini; -}; - -export type UpdateUserTitleResponse = UpdateUserTitleResponses[keyof UpdateUserTitleResponses]; diff --git a/modules/frontend/src/auth/index.ts b/modules/frontend/src/auth/index.ts deleted file mode 100644 index b0989c4..0000000 --- a/modules/frontend/src/auth/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export { ApiError } from './core/ApiError'; -export { CancelablePromise, CancelError } from './core/CancelablePromise'; -export { OpenAPI } from './core/OpenAPI'; -export type { OpenAPIConfig } from './core/OpenAPI'; - -export { AuthService } from './services/AuthService'; diff --git a/modules/frontend/src/auth/services/AuthService.ts b/modules/frontend/src/auth/services/AuthService.ts deleted file mode 100644 index 74a8fa7..0000000 --- a/modules/frontend/src/auth/services/AuthService.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class AuthService { - /** - * Sign up a new user - * @param requestBody - * @returns any Sign-up result - * @throws ApiError - */ - public static postSignUp( - requestBody: { - nickname: string; - pass: string; - }, - ): CancelablePromise<{ - user_id: number; - }> { - return __request(OpenAPI, { - method: 'POST', - url: '/sign-up', - body: requestBody, - mediaType: 'application/json', - }); - } - /** - * Sign in a user and return JWT - * @param requestBody - * @returns any Sign-in result with JWT - * @throws ApiError - */ - public static postSignIn( - requestBody: { - nickname: string; - pass: string; - }, - ): CancelablePromise<{ - user_id: number; - user_name: string; - }> { - return __request(OpenAPI, { - method: 'POST', - url: '/sign-in', - body: requestBody, - mediaType: 'application/json', - errors: { - 401: `Access denied due to invalid credentials`, - }, - }); - } -} diff --git a/modules/frontend/src/components/Header/Header.tsx b/modules/frontend/src/components/Header/Header.tsx deleted file mode 100644 index 26f1658..0000000 --- a/modules/frontend/src/components/Header/Header.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useState } from "react"; -import { Link } from "react-router-dom"; -import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid"; - -type HeaderProps = { - username?: string; -}; - -export const Header: React.FC<HeaderProps> = ({ username }) => { - const [menuOpen, setMenuOpen] = useState(false); - - const toggleMenu = () => setMenuOpen(!menuOpen); - - return ( - <header className="w-full bg-white shadow-md sticky top-0 left-0 z-50"> - <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> - <div className="flex justify-between h-16 items-center"> - - {/* Левый блок — логотип / название */} - <div className="flex-shrink-0"> - <Link to="/" className="text-xl font-bold text-gray-800 hover:text-blue-600"> - NyanimeDB - </Link> - </div> - - {/* Центр — ссылки на разделы (desktop) */} - <nav className="hidden md:flex space-x-4"> - <Link to="/titles" className="text-gray-700 hover:text-blue-600"> - Titles - </Link> - <Link to="/users" className="text-gray-700 hover:text-blue-600"> - Users - </Link> - <Link to="/about" className="text-gray-700 hover:text-blue-600"> - About - </Link> - </nav> - - {/* Правый блок — профиль */} - <div className="hidden md:flex items-center space-x-4"> - {username ? ( - <Link to="/profile" className="text-gray-700 hover:text-blue-600 font-medium"> - {username} - </Link> - ) : ( - <Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium"> - Login - </Link> - )} - </div> - - {/* Бургер для мобильного */} - <div className="md:hidden flex items-center"> - <button - onClick={toggleMenu} - className="p-2 rounded-md hover:bg-gray-200 transition" - > - {menuOpen ? ( - <XMarkIcon className="w-6 h-6 text-gray-800" /> - ) : ( - <Bars3Icon className="w-6 h-6 text-gray-800" /> - )} - </button> - </div> - - </div> - </div> - - {/* Мобильное меню */} - {menuOpen && ( - <div className="md:hidden bg-white border-t border-gray-200 shadow-md"> - <nav className="flex flex-col p-4 space-y-2"> - <Link to="/titles" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>Titles</Link> - <Link to="/users" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>Users</Link> - <Link to="/about" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>About</Link> - {username ? ( - <Link to="/profile" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}> - {username} - </Link> - ) : ( - <Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}> - Login - </Link> - )} - </nav> - </div> - )} - </header> - ); -}; diff --git a/modules/frontend/src/components/LayoutSwitch/LayoutSwitch.tsx b/modules/frontend/src/components/LayoutSwitch/LayoutSwitch.tsx deleted file mode 100644 index 679feea..0000000 --- a/modules/frontend/src/components/LayoutSwitch/LayoutSwitch.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import { Squares2X2Icon, Bars3Icon } from "@heroicons/react/24/solid"; - -export type LayoutSwitchProps = { - layout: "square" | "horizontal" - setLayout: (value: React.SetStateAction<"square" | "horizontal">) => void -}; - -export function LayoutSwitch({ - layout, - setLayout -}: LayoutSwitchProps) { - - return ( - <div className="flex justify-end"> - <button - className="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition" - onClick={() => - setLayout(prev => (prev === "square" ? "horizontal" : "square")) - }> - {layout === "square" - ? <Squares2X2Icon className="w-6 h-6" /> - : <Bars3Icon className="w-6 h-6" /> - } - </button> - </div> - ); -} diff --git a/modules/frontend/src/components/ListView/ListView.tsx b/modules/frontend/src/components/ListView/ListView.tsx deleted file mode 100644 index ea6d683..0000000 --- a/modules/frontend/src/components/ListView/ListView.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react"; - -export type ListViewProps<T> = { - items: T[]; - layout: "square" | "horizontal"; - renderItem: (item: T, layout: "square" | "horizontal") => React.ReactNode; - onLoadMore: () => void; - hasMore: boolean; - loadingMore: boolean; -}; - -export function ListView<T>({ - items, - layout, - renderItem, - onLoadMore, - hasMore, - loadingMore -}: ListViewProps<T>) { - - return ( - <div className="w-full flex flex-col items-center"> - {/* Items */} - <div - className={`w-full sm:w-4/5 grid gap-6 ${ - layout === "square" - ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" - : "grid-cols-1" - }`} - > - {items.map(item => renderItem(item, layout))} - </div> - - {/* Load More */} - {hasMore && ( - <div className="mt-8"> - <button - className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50" - disabled={loadingMore} - onClick={onLoadMore} - > - {loadingMore ? "Loading..." : "Load More"} - </button> - </div> - )} - </div> - ); -} diff --git a/modules/frontend/src/components/SearchBar/SearchBar.tsx b/modules/frontend/src/components/SearchBar/SearchBar.tsx deleted file mode 100644 index 87aee66..0000000 --- a/modules/frontend/src/components/SearchBar/SearchBar.tsx +++ /dev/null @@ -1,34 +0,0 @@ -type SearchBarProps = { - placeholder?: string; - search: string; - setSearch: (value: string) => void; -}; - -export function SearchBar({ - placeholder = "Search...", - search, - setSearch, -}: SearchBarProps) { - return ( - <div className="w-full"> - <input - type="text" - value={search} - placeholder={placeholder} - onChange={(e) => setSearch(e.target.value)} - className=" - w-full - px-4 - py-2 - border - border-gray-300 - rounded-lg - focus:outline-none - focus:ring-2 - focus:ring-blue-500 - text-black - " - /> - </div> - ); -} diff --git a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx deleted file mode 100644 index 98fa868..0000000 --- a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useEffect, useState } from "react"; -// import { DefaultService } from "../../api"; -import { addUserTitle, deleteUserTitle, getUserTitle, updateUserTitle } from "../../api"; -import type { UserTitleStatus } from "../../api"; -import { useCookies } from 'react-cookie'; - -import { - ClockIcon, - CheckCircleIcon, - PlayCircleIcon, - XCircleIcon, -} from "@heroicons/react/24/solid"; -// import { stat } from "fs"; - -// Статусы с иконками и подписью -const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [ - { status: "planned", icon: <ClockIcon className="w-5 h-5" />, label: "Planned" }, - { status: "finished", icon: <CheckCircleIcon className="w-5 h-5" />, label: "Finished" }, - { status: "in-progress", icon: <PlayCircleIcon className="w-5 h-5" />, label: "In Progress" }, - { status: "dropped", icon: <XCircleIcon className="w-5 h-5" />, label: "Dropped" }, -]; - -export function TitleStatusControls({ titleId }: { titleId: number }) { - const [cookies] = useCookies(['xsrf_token']); - const xsrfToken = cookies['xsrf_token'] || null; - - const [currentStatus, setCurrentStatus] = useState<UserTitleStatus | null>(null); - const [loading, setLoading] = useState(false); - - const userIdStr = localStorage.getItem("userId"); - const userId = userIdStr ? Number(userIdStr) : null; - - // --- Load initial status --- - useEffect(() => { - if (!userId) return; - getUserTitle({ path: { user_id: userId, title_id: titleId } }) - .then(res => setCurrentStatus(res.data?.status ?? null)) - .catch(() => setCurrentStatus(null)); // 404 = not assigned - - // DefaultService.getUserTitle(userId, titleId) - // .then((res) => setCurrentStatus(res.status)) - // .catch(() => setCurrentStatus(null)); // 404 = user title does not exist - }, [titleId, userId]); - - // --- Handle click --- - const handleStatusClick = async (status: UserTitleStatus) => { - if (!userId || loading) return; - - setLoading(true); - - try { - // 1) Если кликнули на текущий статус — DELETE - if (currentStatus === status) { - // await DefaultService.deleteUserTitle(userId, titleId); - await deleteUserTitle({path: { - user_id: userId, - title_id: titleId, - }, - headers: { "X-XSRF-TOKEN": xsrfToken }, - }) - setCurrentStatus(null); - return; - } - - // 2) Если другой статус — POST или PATCH - if (!currentStatus) { - // ещё нет записи — POST - // const added = await DefaultService.addUserTitle(userId, { - // title_id: titleId, - // status, - // }); - const added = await addUserTitle({ - body: { - title_id: titleId, - status: status, - }, - path: {user_id: userId}, - headers: { "X-XSRF-TOKEN": xsrfToken }, - }); - - setCurrentStatus(added.data?.status ?? null); - } else { - // уже есть запись — PATCH - //const updated = await DefaultService.updateUserTitle(userId, titleId, { status }); - const updated = await updateUserTitle({ - path: { user_id: userId, title_id: titleId }, - body: { status }, - headers: { "X-XSRF-TOKEN": xsrfToken }, - }); - setCurrentStatus(updated.data?.status ?? null); - } - } finally { - setLoading(false); - } - }; - - return ( - <div className="flex gap-2 flex-wrap justify-center mt-2"> - {STATUS_BUTTONS.map(btn => ( - <button - key={btn.status} - onClick={() => handleStatusClick(btn.status)} - disabled={loading} - className={` - px-3 py-1 rounded-md border flex items-center gap-1 transition - ${currentStatus === btn.status - ? "bg-blue-600 text-white border-blue-700" - : "bg-gray-200 text-black border-gray-300 hover:bg-gray-300"} - `} - title={btn.label} - > - {btn.icon} - <span>{btn.label}</span> - </button> - ))} - </div> - ); -} diff --git a/modules/frontend/src/components/TitlesFilterPanel/TitlesFilterPanel.tsx b/modules/frontend/src/components/TitlesFilterPanel/TitlesFilterPanel.tsx deleted file mode 100644 index 3cfef69..0000000 --- a/modules/frontend/src/components/TitlesFilterPanel/TitlesFilterPanel.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useState } from "react"; -import type { TitleStatus, ReleaseSeason } from "../../api"; -import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/solid"; - -export type TitlesFilter = { - extSearch: boolean; - status: TitleStatus | ""; - rating: number | ""; - releaseYear: number | ""; - releaseSeason: ReleaseSeason | ""; -}; - -type TitlesFilterPanelProps = { - filters: TitlesFilter; - setFilters: (filters: TitlesFilter) => void; -}; - -const STATUS_OPTIONS: (TitleStatus | "")[] = ["", "planned", "finished", "ongoing"]; -const SEASON_OPTIONS: (ReleaseSeason | "")[] = ["", "winter", "spring", "summer", "fall"]; -const RATING_OPTIONS = ["", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - -export function TitlesFilterPanel({ filters, setFilters }: TitlesFilterPanelProps) { - const [open, setOpen] = useState(false); - - const handleChange = (field: keyof TitlesFilter, value: any) => { - setFilters({ ...filters, [field]: value }); - }; - - return ( - <div className="w-full flex justify-center my-4"> - <div className="bg-white shadow rounded-lg w-full max-w-3xl p-4"> - {/* Заголовок панели */} - <div - className="flex justify-between items-center cursor-pointer" - onClick={() => setOpen((prev) => !prev)} - > - <h3 className="text-lg font-medium">Filters</h3> - {open ? <ChevronUpIcon className="w-5 h-5" /> : <ChevronDownIcon className="w-5 h-5" />} - </div> - - {/* Контент панели */} - {open && ( - <div className="mt-4 grid grid-cols-2 sm:grid-cols-3 gap-4"> - {/* Extended Search */} - <div className="flex items-center gap-2"> - <input - type="checkbox" - id="extSearch" - checked={filters.extSearch} - onChange={(e) => handleChange("extSearch", e.target.checked)} - className="w-4 h-4" - /> - <label htmlFor="extSearch" className="text-sm"> - Extended Search - </label> - </div> - - {/* Status */} - <div className="flex flex-col"> - <label htmlFor="status" className="text-sm mb-1">Status</label> - <select - id="status" - value={filters.status} - onChange={(e) => handleChange("status", e.target.value || "")} - className="border rounded px-2 py-1" - > - {STATUS_OPTIONS.map((s) => ( - <option key={s || "all"} value={s}>{s || "All"}</option> - ))} - </select> - </div> - - {/* Rating */} - <div className="flex flex-col"> - <label htmlFor="rating" className="text-sm mb-1">Rating</label> - <select - id="rating" - value={filters.rating} - onChange={(e) => handleChange("rating", e.target.value ? Number(e.target.value) : "")} - className="border rounded px-2 py-1" - > - {RATING_OPTIONS.map((r) => ( - <option key={r} value={r}>{r || "All"}</option> - ))} - </select> - </div> - - {/* Release Year */} - <div className="flex flex-col"> - <label htmlFor="releaseYear" className="text-sm mb-1">Release Year</label> - <input - type="number" - id="releaseYear" - value={filters.releaseYear || ""} - onChange={(e) => - handleChange("releaseYear", e.target.value ? Number(e.target.value) : "") - } - className="border rounded px-2 py-1" - placeholder="Any" - /> - </div> - - {/* Release Season */} - <div className="flex flex-col"> - <label htmlFor="releaseSeason" className="text-sm mb-1">Release Season</label> - <select - id="releaseSeason" - value={filters.releaseSeason} - onChange={(e) => handleChange("releaseSeason", e.target.value || "")} - className="border rounded px-2 py-1" - > - {SEASON_OPTIONS.map((s) => ( - <option key={s || "all"} value={s}>{s || "All"}</option> - ))} - </select> - </div> - </div> - )} - </div> - </div> - ); -} diff --git a/modules/frontend/src/components/TitlesSortBox/TitlesSortBox.tsx b/modules/frontend/src/components/TitlesSortBox/TitlesSortBox.tsx deleted file mode 100644 index ddafd34..0000000 --- a/modules/frontend/src/components/TitlesSortBox/TitlesSortBox.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useState } from "react"; -import type { TitleSort } from "../../api"; -import { ChevronDownIcon, ArrowUpIcon, ArrowDownIcon } from "@heroicons/react/24/solid"; - -type TitlesSortBoxProps = { - sort: TitleSort; - setSort: (value: TitleSort) => void; - sortForward: boolean; - setSortForward: (value: boolean) => void; -}; - -const SORT_OPTIONS: TitleSort[] = ["id", "rating", "year", "views"]; - -export function TitlesSortBox({ - sort, - setSort, - sortForward, - setSortForward, -}: TitlesSortBoxProps) { - const [open, setOpen] = useState(false); - - const toggleSortDirection = () => setSortForward(!sortForward); - const handleSortSelect = (newSort: TitleSort) => { - setSort(newSort); - setOpen(false); - }; - - return ( - <div className="inline-flex relative z-50"> - {/* Левая часть — смена направления */} - <button - onClick={toggleSortDirection} - className="px-4 py-2 flex items-center justify-center bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-l-lg transition" - > - {sortForward ? <ArrowUpIcon className="w-4 h-4 mr-1" /> : <ArrowDownIcon className="w-4 h-4 mr-1" />} - <span className="text-sm font-medium">Order</span> - </button> - - {/* Правая часть — выбор параметра */} - <button - onClick={() => setOpen(!open)} - className="px-4 py-2 flex items-center justify-center bg-gray-100 hover:bg-gray-200 border border-gray-300 border-l-0 rounded-r-lg transition" - > - <span className="text-sm font-medium">{sort}</span> - <ChevronDownIcon className="w-4 h-4 ml-1" /> - </button> - - {/* Dropdown */} - {open && ( - <ul className="absolute top-full left-0 mt-1 w-40 bg-white border border-gray-300 rounded-md shadow-lg z-[1000]"> - {SORT_OPTIONS.map(option => ( - <li key={option}> - <button - className={`w-full text-left px-4 py-2 hover:bg-gray-100 transition ${ - option === sort ? "font-bold bg-gray-100" : "" - }`} - onClick={() => handleSortSelect(option)} - > - {option} - </button> - </li> - ))} - </ul> - )} - </div> - ); -} diff --git a/modules/frontend/src/components/UserPage/UserPage.module.css b/modules/frontend/src/components/UserPage/UserPage.module.css new file mode 100644 index 0000000..75195bf --- /dev/null +++ b/modules/frontend/src/components/UserPage/UserPage.module.css @@ -0,0 +1,97 @@ +.container { + display: flex; + justify-content: center; + align-items: flex-start; + padding: 3rem 1rem; + background-color: #f5f6fa; + min-height: 100vh; + font-family: "Inter", sans-serif; +} + +.card { + background-color: #ffffff; + border-radius: 1rem; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); + padding: 2rem; + max-width: 400px; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.avatar { + margin-bottom: 1.5rem; +} + +.avatarImg { + width: 120px; + height: 120px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #4a90e2; +} + +.avatarPlaceholder { + width: 120px; + height: 120px; + border-radius: 50%; + background-color: #dcdde1; + display: flex; + align-items: center; + justify-content: center; + font-size: 3rem; + color: #4a4a4a; + font-weight: bold; + border: 3px solid #4a90e2; +} + +.info { + display: flex; + flex-direction: column; + align-items: center; +} + +.name { + font-size: 1.8rem; + font-weight: 700; + margin: 0.25rem 0; + color: #2f3640; +} + +.nickname { + font-size: 1rem; + color: #718093; + margin-bottom: 1rem; +} + +.desc { + font-size: 1rem; + color: #353b48; + margin-bottom: 1rem; +} + +.created { + font-size: 0.9rem; + color: #7f8fa6; +} + +.loader { + display: flex; + justify-content: center; + align-items: center; + height: 80vh; + font-size: 1.5rem; + color: #4a90e2; +} + +.error { + display: flex; + justify-content: center; + align-items: center; + height: 80vh; + color: #e84118; + font-weight: 600; + font-size: 1.2rem; +} diff --git a/modules/frontend/src/components/UserPage/UserPage.tsx b/modules/frontend/src/components/UserPage/UserPage.tsx new file mode 100644 index 0000000..0a83679 --- /dev/null +++ b/modules/frontend/src/components/UserPage/UserPage.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; // <-- import +import { DefaultService } from "../../api/services/DefaultService"; +import type { User } from "../../api/models/User"; +import styles from "./UserPage.module.css"; + +const UserPage: React.FC = () => { + const { id } = useParams<{ id: string }>(); // <-- get user id from URL + const [user, setUser] = useState<User | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + useEffect(() => { + if (!id) return; + + const getUserInfo = async () => { + try { + const userInfo = await DefaultService.getUsers(id, "all"); // <-- use dynamic id + setUser(userInfo); + } catch (err) { + console.error(err); + setError("Failed to fetch user info."); + } finally { + setLoading(false); + } + }; + getUserInfo(); + }, [id]); + + if (loading) return <div className={styles.loader}>Loading...</div>; + if (error) return <div className={styles.error}>{error}</div>; + if (!user) return <div className={styles.error}>User not found.</div>; + + return ( + <div className={styles.container}> + <div className={styles.card}> + <div className={styles.avatar}> + {user.avatar_id ? ( + <img + src={`/images/${user.avatar_id}.png`} + alt="User Avatar" + className={styles.avatarImg} + /> + ) : ( + <div className={styles.avatarPlaceholder}> + {user.disp_name?.[0] || "U"} + </div> + )} + </div> + + <div className={styles.info}> + <h1 className={styles.name}>{user.disp_name || user.nickname}</h1> + <p className={styles.nickname}>@{user.nickname}</p> + {user.user_desc && <p className={styles.desc}>{user.user_desc}</p>} + <p className={styles.created}> + Joined: {new Date(user.creation_date).toLocaleDateString()} + </p> + </div> + </div> + </div> + ); +}; + +export default UserPage; diff --git a/modules/frontend/src/components/cards/TitleCardHorizontal.tsx b/modules/frontend/src/components/cards/TitleCardHorizontal.tsx deleted file mode 100644 index b848702..0000000 --- a/modules/frontend/src/components/cards/TitleCardHorizontal.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Title } from "../../api"; - -export function TitleCardHorizontal({ title }: { title: Title }) { - return ( - <div style={{ - display: "flex", - gap: 12, - padding: 12, - border: "1px solid #ddd", - borderRadius: 8 - }}> - {title.poster?.image_path && ( - <img src={title.poster.image_path} width={80} /> - )} - <div> - <h3>{title.title_names["en"]}</h3> - <p>{title.release_year} · {title.release_season} · Rating: {title.rating}</p> - <p>Status: {title.title_status}</p> - </div> - </div> - ); -} diff --git a/modules/frontend/src/components/cards/TitleCardSquare.tsx b/modules/frontend/src/components/cards/TitleCardSquare.tsx deleted file mode 100644 index 0bcb49d..0000000 --- a/modules/frontend/src/components/cards/TitleCardSquare.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Title } from "../../api"; - -export function TitleCardSquare({ title }: { title: Title }) { - return ( - <div style={{ - width: 160, - border: "1px solid #ddd", - padding: 8, - borderRadius: 8, - textAlign: "center" - }}> - {title.poster?.image_path && ( - <img src={title.poster.image_path} width={140} /> - )} - <div> - <h4>{title.title_names["en"]}</h4> - <small>{title.release_year} • {title.rating}</small> - </div> - </div> - ); -} diff --git a/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx b/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx deleted file mode 100644 index ad7d5df..0000000 --- a/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { UserTitle } from "../../api"; - -export function UserTitleCardHorizontal({ title }: { title: UserTitle }) { - return ( - <div style={{ - display: "flex", - gap: 12, - padding: 12, - border: "1px solid #ddd", - borderRadius: 8 - }}> - {title.title?.poster?.image_path && ( - <img src={title.title?.poster.image_path} width={80} /> - )} - <div> - <h3>{title.title?.title_names["en"]}</h3> - <p>{title.title?.release_year} · {title.title?.release_season} · Rating: {title.title?.rating}</p> - <p>Status: {title.title?.title_status}</p> - </div> - </div> - ); -} diff --git a/modules/frontend/src/components/cards/UserTitleCardSquare.tsx b/modules/frontend/src/components/cards/UserTitleCardSquare.tsx deleted file mode 100644 index edcf1d5..0000000 --- a/modules/frontend/src/components/cards/UserTitleCardSquare.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { UserTitle } from "../../api"; - -export function UserTitleCardSquare({ title }: { title: UserTitle }) { - return ( - <div style={{ - width: 160, - border: "1px solid #ddd", - padding: 8, - borderRadius: 8, - textAlign: "center" - }}> - {title.title?.poster?.image_path && ( - <img src={title.title?.poster.image_path} width={140} /> - )} - <div> - <h4>{title.title?.title_names["en"]}</h4> - <h5>{title.status}</h5> - <small>{title.title?.release_year} • {title.title?.rating}</small> - </div> - </div> - ); -} diff --git a/modules/frontend/src/index.css b/modules/frontend/src/index.css index 7b32a9b..08a3ac9 100644 --- a/modules/frontend/src/index.css +++ b/modules/frontend/src/index.css @@ -1,9 +1,68 @@ -@import "tailwindcss"; +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; -html, body, #root { + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { margin: 0; - padding: 0; - width: 100%; - height: 100%; - @apply text-black bg-white; -} \ No newline at end of file + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/modules/frontend/src/main.tsx b/modules/frontend/src/main.tsx index c225a33..bef5202 100644 --- a/modules/frontend/src/main.tsx +++ b/modules/frontend/src/main.tsx @@ -1,13 +1,10 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { CookiesProvider } from 'react-cookie' import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( <StrictMode> - <CookiesProvider> - <App /> - </CookiesProvider> + <App /> </StrictMode>, ) diff --git a/modules/frontend/src/pages/LoginPage/LoginPage.tsx b/modules/frontend/src/pages/LoginPage/LoginPage.tsx deleted file mode 100644 index 928766e..0000000 --- a/modules/frontend/src/pages/LoginPage/LoginPage.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useState } from "react"; -import { AuthService } from "../../auth/services/AuthService"; -import { useNavigate } from "react-router-dom"; - -export const LoginPage: React.FC = () => { - const navigate = useNavigate(); - const [isLogin, setIsLogin] = useState(true); // true = login, false = signup - const [nickname, setNickname] = useState(""); - const [password, setPassword] = useState(""); - const [loading, setLoading] = useState(false); - const [error, setError] = useState<string | null>(null); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(null); - - try { - if (isLogin) { - const res = await AuthService.postSignIn({ nickname, pass: password }); - if (res.user_id && res.user_name) { - // Сохраняем user_id и username в localStorage - localStorage.setItem("userId", res.user_id.toString()); - localStorage.setItem("username", res.user_name); - - navigate("/profile"); // редирект на профиль - } else { - setError("Login failed"); - } - } else { - // SignUp оставляем без сохранения данных - const res = await AuthService.postSignUp({ nickname, pass: password }); - if (res.user_id) { - setIsLogin(true); // переключаемся на login после регистрации - } else { - setError("Sign up failed"); - } - } - } catch (err: any) { - console.error(err); - setError(err?.message || "Something went wrong"); - } finally { - setLoading(false); - } - }; - - return ( - <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4"> - <div className="max-w-md w-full bg-white shadow-md rounded-lg p-8"> - <h2 className="text-2xl font-bold mb-6 text-center"> - {isLogin ? "Login" : "Sign Up"} - </h2> - - {error && <div className="text-red-600 mb-4">{error}</div>} - - <form onSubmit={handleSubmit} className="space-y-4"> - <div> - <label className="block text-sm font-medium text-gray-700 mb-1"> - Nickname - </label> - <input - type="text" - value={nickname} - onChange={(e) => setNickname(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" - required - /> - </div> - - <div> - <label className="block text-sm font-medium text-gray-700 mb-1"> - Password - </label> - <input - type="password" - value={password} - onChange={(e) => setPassword(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" - required - /> - </div> - - <button - type="submit" - disabled={loading} - className="w-full bg-blue-600 text-white py-2 rounded-lg font-semibold hover:bg-blue-700 transition disabled:opacity-50" - > - {loading ? "Please wait..." : isLogin ? "Login" : "Sign Up"} - </button> - </form> - - <div className="mt-4 text-center text-sm text-gray-600"> - {isLogin ? ( - <> - Don't have an account?{" "} - <button - onClick={() => setIsLogin(false)} - className="text-blue-600 hover:underline" - > - Sign Up - </button> - </> - ) : ( - <> - Already have an account?{" "} - <button - onClick={() => setIsLogin(true)} - className="text-blue-600 hover:underline" - > - Login - </button> - </> - )} - </div> - </div> - </div> - ); -}; diff --git a/modules/frontend/src/pages/TitlePage/TitlePage.tsx b/modules/frontend/src/pages/TitlePage/TitlePage.tsx deleted file mode 100644 index 0d9e297..0000000 --- a/modules/frontend/src/pages/TitlePage/TitlePage.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useEffect, useState } from "react"; -import { useParams, Link } from "react-router-dom"; -// import { DefaultService } from "../../api/services/DefaultService"; -import { getTitle, type Title } from "../../api"; -import { TitleStatusControls } from "../../components/TitleStatusControls/TitleStatusControls"; - -export default function TitlePage() { - const params = useParams(); - const titleId = Number(params.id); - - const [title, setTitle] = useState<Title | null>(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState<string | null>(null); - - // --------------------------- - // LOAD TITLE INFO - // --------------------------- - useEffect(() => { - const fetchTitle = async () => { - setLoading(true); - try { - // const data = await DefaultService.getTitle(titleId, "all"); - const data = await getTitle({path: {title_id: titleId}}) - setTitle(data?.data ?? null); - setError(null); - } catch (err: any) { - console.error(err); - setError(err?.message || "Failed to fetch title"); - } finally { - setLoading(false); - } - }; - fetchTitle(); - }, [titleId]); - - const getTagsString = () => - title?.tags?.map(tag => tag.en).filter(Boolean).join(", "); - - if (loading) return <div className="mt-20 font-medium text-black">Loading title...</div>; - if (error) return <div className="mt-20 text-red-600 font-medium">{error}</div>; - if (!title) return null; - - return ( - <div className="w-full min-h-screen bg-gray-50 p-6 flex justify-center"> - <div className="flex flex-col md:flex-row bg-white shadow-lg rounded-xl max-w-4xl w-full p-6 gap-6"> - {/* Poster + status buttons */} - <div className="flex flex-col items-center"> - <img - src={title.poster?.image_path || "/default-poster.png"} - alt={title.title_names?.en?.[0] || "Title poster"} - className="w-48 h-72 object-cover rounded-lg mb-4" - /> - - {/* Status buttons */} - <TitleStatusControls titleId={titleId} /> - </div> - - {/* Title info */} - <div className="flex-1 flex flex-col"> - <h1 className="text-3xl font-bold mb-2"> - {title.title_names?.en?.[0] || "Untitled"} - </h1> - - {title.studio && ( - <p className="text-gray-700 mb-1"> - Studio:{" "} - {title.studio.id ? ( - <Link - to={`/studios/${title.studio.id}`} - className="text-blue-600 hover:underline" - > - {title.studio.name} - </Link> - ) : ( - title.studio.name - )} - </p> - )} - - {title.title_status && <p className="text-gray-700 mb-1">Status: {title.title_status}</p>} - - {title.rating !== undefined && ( - <p className="text-gray-700 mb-1"> - Rating: {title.rating} ({title.rating_count} votes) - </p> - )} - - {title.release_year && ( - <p className="text-gray-700 mb-1"> - Released: {title.release_year} {title.release_season || ""} - </p> - )} - - {title.episodes_aired !== undefined && ( - <p className="text-gray-700 mb-1"> - Episodes: {title.episodes_aired}/{title.episodes_all} - </p> - )} - - {title.tags && title.tags.length > 0 && ( - <p className="text-gray-700 mb-1"> - Tags: {getTagsString()} - </p> - )} - </div> - </div> - </div> - ); -} diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx deleted file mode 100644 index 727e072..0000000 --- a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { useEffect, useState } from "react"; -import { ListView } from "../../components/ListView/ListView"; -import { SearchBar } from "../../components/SearchBar/SearchBar"; -import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox"; -// import { DefaultService } from "../../api/services/DefaultService"; -import { TitleCardSquare } from "../../components/cards/TitleCardSquare"; -import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal"; -import { getTitles, type CursorObj, type Title, type TitleSort } from "../../api"; -import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; -import { Link } from "react-router-dom"; -import { type TitlesFilter, TitlesFilterPanel } from "../../components/TitlesFilterPanel/TitlesFilterPanel"; - -const PAGE_SIZE = 10; - -export default function TitlesPage() { - const [titles, setTitles] = useState<Title[]>([]); - const [nextPage, setNextPage] = useState<Title[]>([]); - const [cursor, setCursor] = useState<CursorObj | null>(null); - const [search, setSearch] = useState(""); - const [loading, setLoading] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [sort, setSort] = useState<TitleSort>("id"); - const [sortForward, setSortForward] = useState(true); - const [layout, setLayout] = useState<"square" | "horizontal">("square"); - - const [filters, setFilters] = useState<TitlesFilter>({ - extSearch: false, - status: "", - rating: "", - releaseYear: "", - releaseSeason: "", - }); - - const fetchPage = async (cursorObj: CursorObj | null) => { - const cursorStr = cursorObj - ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") - : undefined; - - const response = await getTitles({ - query: { - cursor: cursorStr, - sort: sort, - sort_forward: sortForward, - ext_search: filters.extSearch, - word: search.trim() || undefined, - status: filters.status ? [filters.status] : undefined, - rating: filters.rating || undefined, - release_year: filters.releaseYear || undefined, - release_season: filters.releaseSeason || undefined, - limit: PAGE_SIZE, - offset: PAGE_SIZE, - fields: "all", - }, - }); - - return { - items: response.data?.data ?? [], - nextCursor: response.data?.cursor ?? null, - }; - }; - - // Инициализация: загружаем сразу две страницы - useEffect(() => { - const initLoad = async () => { - setLoading(true); - setTitles([]); - setNextPage([]); - setCursor(null); - - const firstPage = await fetchPage(null); - const secondPage = firstPage.nextCursor ? await fetchPage(firstPage.nextCursor) : { items: [], nextCursor: null }; - - setTitles(firstPage.items); - setNextPage(secondPage.items); - setCursor(secondPage.nextCursor); - setLoading(false); - }; - - initLoad(); - }, [search, sort, sortForward, filters]); - - -const handleLoadMore = async () => { - if (nextPage.length === 0) { - setLoadingMore(false); - return; - } - - setLoadingMore(true); - - setTitles(prev => [...prev, ...nextPage]); - setNextPage([]); - - // Подгружаем следующую страницу с сервера - if (cursor) { - try { - const next = await fetchPage(cursor); - if (next.items.length > 0) { - setNextPage(next.items); - } - setCursor(next.nextCursor); - } catch (err) { - console.error(err); - } - } - - // Любой сценарий – выключаем loadingMore - setLoadingMore(false); -}; - - - - return ( - <div className="w-full min-h-screen bg-gray-50 p-6 flex flex-col items-center"> - - <h1 className="text-4xl font-bold mb-6 text-center text-black">Titles</h1> - - <div className="w-full sm:w-4/5 flex flex-col sm:flex-row gap-4 mb-6 items-center"> - <SearchBar placeholder="Search titles..." search={search} setSearch={setSearch} /> - <LayoutSwitch layout={layout} setLayout={setLayout} /> - <TitlesSortBox - sort={sort} - setSort={setSort} - sortForward={sortForward} - setSortForward={setSortForward} - /> - </div> - <TitlesFilterPanel filters={filters} setFilters={setFilters} /> - - {loading && ( - <div className="mt-20 flex flex-col items-center justify-center space-y-4 font-medium text-black"> - <span>Loading...</span> - <img - src="https://images.steamusercontent.com/ugc/920301026407341369/69CBEF69DED504CD8CC7838D370061089F4D81BD/" - alt="Loading animation" - className="size-100 object-contain" - /> - </div> - )} - - {!loading && titles.length === 0 && ( - <div className="mt-20 font-medium text-black">No titles found.</div> - )} - - {titles.length > 0 && ( - <> - <ListView<Title> - items={titles} - layout={layout} - hasMore={!!cursor || nextPage.length > 1} - loadingMore={loadingMore} - onLoadMore={handleLoadMore} - renderItem={(title, layout) => ( - <Link to={`/titles/${title.id}`} key={title.id} className="block"> - {layout === "square" ? <TitleCardSquare title={title} /> : <TitleCardHorizontal title={title} />} - </Link> - )} - /> - - {!cursor && nextPage.length == 0 && ( - <div className="mt-6 font-medium text-black"> - Результатов больше нет, было найдено {titles.length} тайтлов. - </div> - )} - </> - )} - </div> - ); -} diff --git a/modules/frontend/src/pages/UserPage/UserPage.tsx b/modules/frontend/src/pages/UserPage/UserPage.tsx deleted file mode 100644 index 1a8ba1e..0000000 --- a/modules/frontend/src/pages/UserPage/UserPage.tsx +++ /dev/null @@ -1,201 +0,0 @@ -// pages/UserPage/UserPage.tsx -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -// import { DefaultService } from "../../api/services/DefaultService"; -import { SearchBar } from "../../components/SearchBar/SearchBar"; -import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox"; -import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; -import { ListView } from "../../components/ListView/ListView"; -import { UserTitleCardSquare } from "../../components/cards/UserTitleCardSquare"; -import { UserTitleCardHorizontal } from "../../components/cards/UserTitleCardHorizontal"; -import { type User, type UserTitle, type CursorObj, type TitleSort, getUsersId, getUserTitles } from "../../api"; -import { Link } from "react-router-dom"; - -const PAGE_SIZE = 10; - -type UserPageProps = { - userId?: string; -}; - -export default function UserPage({ userId }: UserPageProps) { - const params = useParams(); - const id = userId || params?.id; - - const [user, setUser] = useState<User | null>(null); - const [loadingUser, setLoadingUser] = useState(true); - const [errorUser, setErrorUser] = useState<string | null>(null); - - // Для списка тайтлов - const [titles, setTitles] = useState<UserTitle[]>([]); - const [nextPage, setNextPage] = useState<UserTitle[]>([]); - const [cursor, setCursor] = useState<CursorObj | null>(null); - const [loadingTitles, setLoadingTitles] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [search, setSearch] = useState(""); - const [sort, setSort] = useState<TitleSort>("id"); - const [sortForward, setSortForward] = useState(true); - const [layout, setLayout] = useState<"square" | "horizontal">("square"); - - // --- Получение данных пользователя --- - useEffect(() => { - const fetchUser = async () => { - if (!id) return; - setLoadingUser(true); - try { - // const result = await DefaultService.getUsersId(id, "all"); - const result = await getUsersId({path: {user_id: id}}) - setUser(result?.data ?? null); - setErrorUser(null); - } catch (err: any) { - console.error(err); - setErrorUser(err?.message || "Failed to fetch user data"); - } finally { - setLoadingUser(false); - } - }; - fetchUser(); - }, [id]); - - // --- Получение списка тайтлов пользователя --- - const fetchPage = async (cursorObj: CursorObj | null) => { - if (!id) return { items: [], nextCursor: null }; - const cursorStr = cursorObj - ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") - : ""; - - try { - const result = await getUserTitles({ - path: { - user_id: id, - }, - query: { - cursor: cursorStr, - sort: sort, - sort_forward: sortForward, - word: search.trim() || undefined, - status: undefined, - watch_status: undefined, - rating: undefined, - my_rate: undefined, - release_year: undefined, - release_season: undefined, - limit: PAGE_SIZE}}) - // const result = await DefaultService.getUserTitles( - // id, - // cursorStr, - // sort, - // sortForward, - // search.trim() || undefined, - // undefined, // status фильтр, можно добавить - // undefined, // watchStatus - // undefined, // rating - // undefined, // myRate - // undefined, // releaseYear - // undefined, // releaseSeason - // PAGE_SIZE, - // "all" - // ); - - if (!result?.data?.data?.length) return { items: [], nextCursor: null }; - - return { items: result.data?.data, nextCursor: result.data?.cursor ?? null }; - } catch (err: any) { - if (err.status === 204) return { items: [], nextCursor: null }; - throw err; - } - }; - - // Инициализация: загружаем сразу две страницы - useEffect(() => { - const initLoad = async () => { - setLoadingTitles(true); - setTitles([]); - setNextPage([]); - setCursor(null); - - const firstPage = await fetchPage(null); - const secondPage = firstPage.nextCursor ? await fetchPage(firstPage.nextCursor) : { items: [], nextCursor: null }; - - setTitles(firstPage.items); - setNextPage(secondPage.items); - setCursor(secondPage.nextCursor); - setLoadingTitles(false); - }; - initLoad(); - }, [id, search, sort, sortForward]); - - const handleLoadMore = async () => { - if (nextPage.length === 0) { - setLoadingMore(false); - return; - } - setLoadingMore(true); - - setTitles(prev => [...prev, ...nextPage]); - setNextPage([]); - - if (cursor) { - try { - const next = await fetchPage(cursor); - if (next.items.length > 0) setNextPage(next.items); - setCursor(next.nextCursor); - } catch (err) { - console.error(err); - } - } - - setLoadingMore(false); - }; - - return ( - <div className="w-full min-h-screen bg-gray-50 p-6 flex flex-col items-center"> - - {/* --- Карточка пользователя --- */} - {loadingUser && <div className="mt-10 text-xl font-medium">Loading user...</div>} - {errorUser && <div className="mt-10 text-red-600 font-medium">{errorUser}</div>} - {user && ( - <div className="bg-white shadow-lg rounded-xl p-6 w-full max-w-sm flex flex-col items-center mb-8"> - <img src={user.image?.image_path} alt={user.nickname} className="w-32 h-32 rounded-full object-cover mb-4" /> - <h2 className="text-2xl font-bold mb-2">{user.disp_name || user.nickname}</h2> - {user.mail && <p className="text-gray-600 mb-2">{user.mail}</p>} - {user.user_desc && <p className="text-gray-700 text-center">{user.user_desc}</p>} - {user.creation_date && <p className="text-gray-400 mt-4 text-sm">Registered: {new Date(user.creation_date).toLocaleDateString()}</p>} - </div> - )} - - {/* --- Панель поиска, сортировки и лейаута --- */} - <div className="w-full sm:w-4/5 flex flex-col sm:flex-row gap-4 mb-6 items-center"> - <SearchBar placeholder="Search titles..." search={search} setSearch={setSearch} /> - <LayoutSwitch layout={layout} setLayout={setLayout} /> - <TitlesSortBox sort={sort} setSort={setSort} sortForward={sortForward} setSortForward={setSortForward} /> - </div> - - {/* --- Список тайтлов --- */} - {loadingTitles && <div className="mt-6 font-medium text-black">Loading titles...</div>} - {!loadingTitles && titles.length === 0 && <div className="mt-6 font-medium text-black">No titles found.</div>} - - {titles.length > 0 && ( - <> - <ListView<UserTitle> - items={titles} - layout={layout} - hasMore={!!cursor || nextPage.length > 1} - loadingMore={loadingMore} - onLoadMore={handleLoadMore} - renderItem={(title, layout) => ( - <Link to={`/titles/${title.title?.id}`} key={title.title?.id} className="block"> - {layout === "square" ? <UserTitleCardSquare title={title} /> : <UserTitleCardHorizontal title={title} />} - </Link> - )} - /> - - {!cursor && nextPage.length === 0 && ( - <div className="mt-6 font-medium text-black"> - Результатов больше нет, было найдено {titles.length} тайтлов. - </div> - )} - </> - )} - </div> - ); -} diff --git a/modules/frontend/src/pages/UsersPage/UsersPage.tsx b/modules/frontend/src/pages/UsersPage/UsersPage.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/modules/frontend/src/types/list.ts b/modules/frontend/src/types/list.ts deleted file mode 100644 index 582da39..0000000 --- a/modules/frontend/src/types/list.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface PaginatedResult<TItem> { - items: TItem[]; - nextCursor?: string; -} - -export interface FetchParams { - search: string; - cursor?: string; -} - -export type FetchFunction<TItem> = - (params: FetchParams) => Promise<PaginatedResult<TItem>>; diff --git a/modules/frontend/tsconfig.app.json b/modules/frontend/tsconfig.app.json index 2f416e5..a9b5a59 100644 --- a/modules/frontend/tsconfig.app.json +++ b/modules/frontend/tsconfig.app.json @@ -20,7 +20,7 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": false, + "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, diff --git a/modules/frontend/tsconfig.node.json b/modules/frontend/tsconfig.node.json index 3439137..8a67f62 100644 --- a/modules/frontend/tsconfig.node.json +++ b/modules/frontend/tsconfig.node.json @@ -18,7 +18,7 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": false, + "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, diff --git a/modules/frontend/vite.config.ts b/modules/frontend/vite.config.ts index 554d630..4cfbdd0 100644 --- a/modules/frontend/vite.config.ts +++ b/modules/frontend/vite.config.ts @@ -1,15 +1,11 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [ - react(), - tailwindcss() - ], + plugins: [react()], server: { - host: '0.0.0.0', + host: '127.0.0.1', port: 8083, }, }) diff --git a/sql/migrations/000001_init.up.sql b/sql/migrations/000001_init.up.sql index 369e455..0b7fa33 100644 --- a/sql/migrations/000001_init.up.sql +++ b/sql/migrations/000001_init.up.sql @@ -1,3 +1,6 @@ +-- TODO: +-- maybe jsonb constraints +-- clean unused images CREATE TYPE usertitle_status_t AS ENUM ('finished', 'planned', 'dropped', 'in-progress'); CREATE TYPE storage_type_t AS ENUM ('local', 's3'); CREATE TYPE title_status_t AS ENUM ('finished', 'ongoing', 'planned'); @@ -11,7 +14,6 @@ CREATE TABLE providers ( CREATE TABLE tags ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - -- example: { "ru": "Сёдзё", "en": "Shojo", "jp": "少女"} tag_names jsonb NOT NULL ); @@ -23,32 +25,28 @@ CREATE TABLE images ( CREATE TABLE users ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - avatar_id bigint REFERENCES images (id) ON DELETE SET NULL, + avatar_id bigint REFERENCES images (id), passhash text NOT NULL, - mail text CHECK (mail ~ '^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+$'), - nickname text UNIQUE NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]{3,}$'), + mail text CHECK (mail ~ '[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+'), + nickname text NOT NULL CHECK (nickname ~ '^[a-zA-Z0-9_-]+$'), disp_name text, user_desc text, - creation_date timestamptz NOT NULL DEFAULT NOW(), + creation_date timestamptz NOT NULL, last_login timestamptz ); CREATE TABLE studios ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - studio_name text NOT NULL UNIQUE, - illust_id bigint REFERENCES images (id) ON DELETE SET NULL, + studio_name text UNIQUE, + illust_id bigint REFERENCES images (id), studio_desc text ); CREATE TABLE titles ( - -- // TODO: anime type (film, season etc) id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - -- example {"ru": ["Атака титанов", "Атака Титанов"],"en": ["Attack on Titan", "AoT"],"ja": ["進撃の巨人", "しんげきのきょじん"]} title_names jsonb NOT NULL, - -- example {"ru": "Кулинарное аниме как правильно приготовить людей.","en": "A culinary anime about how to cook people properly."} - title_desc jsonb, studio_id bigint NOT NULL REFERENCES studios (id), - poster_id bigint REFERENCES images (id) ON DELETE SET NULL, + poster_id bigint REFERENCES images (id), title_status title_status_t NOT NULL, rating float CHECK (rating >= 0 AND rating <= 10), rating_count int CHECK (rating_count >= 0), @@ -57,43 +55,26 @@ CREATE TABLE titles ( season int CHECK (season >= 0), episodes_aired int CHECK (episodes_aired >= 0), episodes_all int CHECK (episodes_all >= 0), - -- example { "1": "50.50", "2": "23.23"} episodes_len jsonb, CHECK ((episodes_aired IS NULL AND episodes_all IS NULL) OR (episodes_aired IS NOT NULL AND episodes_all IS NOT NULL AND episodes_aired <= episodes_all)) ); -CREATE TABLE reviews ( - id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - data text NOT NULL, - rating int CHECK (rating >= 0 AND rating <= 10), - user_id bigint REFERENCES users (id) ON DELETE SET NULL, - title_id bigint REFERENCES titles (id) ON DELETE CASCADE, - created_at timestamptz DEFAULT NOW() -); - -CREATE TABLE review_images ( - PRIMARY KEY (review_id, image_id), - review_id bigint NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, - image_id bigint NOT NULL REFERENCES images(id) ON DELETE CASCADE -); - CREATE TABLE usertitles ( PRIMARY KEY (user_id, title_id), - user_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE, - title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE, + user_id bigint NOT NULL REFERENCES users (id), + title_id bigint NOT NULL REFERENCES titles (id), status usertitle_status_t NOT NULL, rate int CHECK (rate > 0 AND rate <= 10), - review_id bigint REFERENCES reviews (id) ON DELETE SET NULL, - ctime timestamptz NOT NULL DEFAULT now() - -- // TODO: series status + review_text text, + review_date timestamptz ); CREATE TABLE title_tags ( PRIMARY KEY (title_id, tag_id), - title_id bigint NOT NULL REFERENCES titles (id) ON DELETE CASCADE, - tag_id bigint NOT NULL REFERENCES tags (id) ON DELETE CASCADE + title_id bigint NOT NULL REFERENCES titles (id), + tag_id bigint NOT NULL REFERENCES tags (id) ); CREATE TABLE signals ( @@ -104,18 +85,6 @@ CREATE TABLE signals ( pending boolean NOT NULL ); -CREATE TABLE external_services ( - id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - name text UNIQUE NOT NULL, - auth_token text -); - -CREATE TABLE external_ids ( - user_id bigint NOT NULL REFERENCES users (id), - service_id bigint NOT NULL REFERENCES external_services (id), - external_id text NOT NULL -); - -- Functions CREATE OR REPLACE FUNCTION update_title_rating() RETURNS TRIGGER AS $$ diff --git a/sql/models.go b/sql/models.go index c299609..928d5ac 100644 --- a/sql/models.go +++ b/sql/models.go @@ -6,7 +6,6 @@ package sqlc import ( "database/sql/driver" - "encoding/json" "fmt" "time" @@ -186,18 +185,6 @@ func (ns NullUsertitleStatusT) Value() (driver.Value, error) { return string(ns.UsertitleStatusT), nil } -type ExternalID struct { - UserID int64 `json:"user_id"` - ServiceID int64 `json:"service_id"` - ExternalID string `json:"external_id"` -} - -type ExternalService struct { - ID int64 `json:"id"` - Name string `json:"name"` - AuthToken *string `json:"auth_token"` -} - type Image struct { ID int64 `json:"id"` StorageType StorageTypeT `json:"storage_type"` @@ -210,55 +197,40 @@ type Provider struct { Credentials []byte `json:"credentials"` } -type Review struct { - ID int64 `json:"id"` - Data string `json:"data"` - Rating *int32 `json:"rating"` - UserID *int64 `json:"user_id"` - TitleID *int64 `json:"title_id"` - CreatedAt pgtype.Timestamptz `json:"created_at"` -} - -type ReviewImage struct { - ReviewID int64 `json:"review_id"` - ImageID int64 `json:"image_id"` -} - type Signal struct { - ID int64 `json:"id"` - TitleID *int64 `json:"title_id"` - RawData json.RawMessage `json:"raw_data"` - ProviderID int64 `json:"provider_id"` - Pending bool `json:"pending"` + ID int64 `json:"id"` + TitleID *int64 `json:"title_id"` + RawData []byte `json:"raw_data"` + ProviderID int64 `json:"provider_id"` + Pending bool `json:"pending"` } type Studio struct { ID int64 `json:"id"` - StudioName string `json:"studio_name"` + StudioName *string `json:"studio_name"` IllustID *int64 `json:"illust_id"` StudioDesc *string `json:"studio_desc"` } type Tag struct { - ID int64 `json:"id"` - TagNames json.RawMessage `json:"tag_names"` + ID int64 `json:"id"` + TagNames []byte `json:"tag_names"` } type Title struct { - ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` - TitleDesc []byte `json:"title_desc"` - StudioID int64 `json:"studio_id"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - EpisodesLen []byte `json:"episodes_len"` + ID int64 `json:"id"` + TitleNames []byte `json:"title_names"` + StudioID int64 `json:"studio_id"` + PosterID *int64 `json:"poster_id"` + TitleStatus TitleStatusT `json:"title_status"` + Rating *float64 `json:"rating"` + RatingCount *int32 `json:"rating_count"` + ReleaseYear *int32 `json:"release_year"` + ReleaseSeason NullReleaseSeasonT `json:"release_season"` + Season *int32 `json:"season"` + EpisodesAired *int32 `json:"episodes_aired"` + EpisodesAll *int32 `json:"episodes_all"` + EpisodesLen []byte `json:"episodes_len"` } type TitleTag struct { @@ -279,10 +251,10 @@ type User struct { } type Usertitle struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` - Status UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - ReviewID *int64 `json:"review_id"` - Ctime time.Time `json:"ctime"` + UserID int64 `json:"user_id"` + TitleID int64 `json:"title_id"` + Status UsertitleStatusT `json:"status"` + Rate *int32 `json:"rate"` + ReviewText *string `json:"review_text"` + ReviewDate pgtype.Timestamptz `json:"review_date"` } diff --git a/sql/queries.sql.go b/sql/queries.sql.go index 0384ccd..8f92c2a 100644 --- a/sql/queries.sql.go +++ b/sql/queries.sql.go @@ -7,10 +7,7 @@ package sqlc import ( "context" - "encoding/json" "time" - - "github.com/jackc/pgx/v5/pgtype" ) const createImage = `-- name: CreateImage :one @@ -31,283 +28,33 @@ func (q *Queries) CreateImage(ctx context.Context, arg CreateImageParams) (Image return i, err } -const createNewUser = `-- name: CreateNewUser :one -INSERT -INTO users (passhash, nickname) -VALUES ($1, $2) -RETURNING id -` - -type CreateNewUserParams struct { - Passhash string `json:"passhash"` - Nickname string `json:"nickname"` -} - -func (q *Queries) CreateNewUser(ctx context.Context, arg CreateNewUserParams) (int64, error) { - row := q.db.QueryRow(ctx, createNewUser, arg.Passhash, arg.Nickname) - var id int64 - err := row.Scan(&id) - return id, err -} - -const deleteUserTitle = `-- name: DeleteUserTitle :one -DELETE FROM usertitles -WHERE user_id = $1 - AND title_id = $2 -RETURNING user_id, title_id, status, rate, review_id, ctime -` - -type DeleteUserTitleParams struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` -} - -func (q *Queries) DeleteUserTitle(ctx context.Context, arg DeleteUserTitleParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, deleteUserTitle, arg.UserID, arg.TitleID) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, - ) - return i, err -} - -const getExternalServiceByToken = `-- name: GetExternalServiceByToken :one -SELECT id, name, auth_token -FROM external_services -WHERE auth_token = $1 -` - -func (q *Queries) GetExternalServiceByToken(ctx context.Context, authToken *string) (ExternalService, error) { - row := q.db.QueryRow(ctx, getExternalServiceByToken, authToken) - var i ExternalService - err := row.Scan(&i.ID, &i.Name, &i.AuthToken) - return i, err -} - const getImageByID = `-- name: GetImageByID :one SELECT id, storage_type, image_path FROM images -WHERE id = $1::bigint +WHERE id = $1 ` -func (q *Queries) GetImageByID(ctx context.Context, illustID int64) (Image, error) { - row := q.db.QueryRow(ctx, getImageByID, illustID) +func (q *Queries) GetImageByID(ctx context.Context, id int64) (Image, error) { + row := q.db.QueryRow(ctx, getImageByID, id) var i Image err := row.Scan(&i.ID, &i.StorageType, &i.ImagePath) return i, err } -const getReviewByID = `-- name: GetReviewByID :one - -SELECT id, data, rating, user_id, title_id, created_at -FROM reviews -WHERE review_id = $1::bigint -` - -// 100 is default limit -func (q *Queries) GetReviewByID(ctx context.Context, reviewID int64) (Review, error) { - row := q.db.QueryRow(ctx, getReviewByID, reviewID) - var i Review - err := row.Scan( - &i.ID, - &i.Data, - &i.Rating, - &i.UserID, - &i.TitleID, - &i.CreatedAt, - ) - return i, err -} - -const getStudioByID = `-- name: GetStudioByID :one -SELECT id, studio_name, illust_id, studio_desc -FROM studios -WHERE id = $1::bigint -` - -func (q *Queries) GetStudioByID(ctx context.Context, studioID int64) (Studio, error) { - row := q.db.QueryRow(ctx, getStudioByID, studioID) - var i Studio - err := row.Scan( - &i.ID, - &i.StudioName, - &i.IllustID, - &i.StudioDesc, - ) - return i, err -} - -const getTitleByID = `-- name: GetTitleByID :one -SELECT - t.id, t.title_names, t.title_desc, t.studio_id, t.poster_id, t.title_status, t.rating, t.rating_count, t.release_year, t.release_season, t.season, t.episodes_aired, t.episodes_all, t.episodes_len, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name, - s.illust_id as studio_illust_id, - s.studio_desc as studio_desc, - si.storage_type as studio_storage_type, - si.image_path as studio_image_path - -FROM titles as t -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) -LEFT JOIN images as si ON (s.illust_id = si.id) - -WHERE t.id = $1::bigint -GROUP BY - t.id, i.id, s.id, si.id -` - -type GetTitleByIDRow struct { - ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` - TitleDesc []byte `json:"title_desc"` - StudioID int64 `json:"studio_id"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - EpisodesLen []byte `json:"episodes_len"` - TitleStorageType *StorageTypeT `json:"title_storage_type"` - TitleImagePath *string `json:"title_image_path"` - TagNames json.RawMessage `json:"tag_names"` - StudioName *string `json:"studio_name"` - StudioIllustID *int64 `json:"studio_illust_id"` - StudioDesc *string `json:"studio_desc"` - StudioStorageType *StorageTypeT `json:"studio_storage_type"` - StudioImagePath *string `json:"studio_image_path"` -} - -// sqlc.struct: TitlesFull -func (q *Queries) GetTitleByID(ctx context.Context, titleID int64) (GetTitleByIDRow, error) { - row := q.db.QueryRow(ctx, getTitleByID, titleID) - var i GetTitleByIDRow - err := row.Scan( - &i.ID, - &i.TitleNames, - &i.TitleDesc, - &i.StudioID, - &i.PosterID, - &i.TitleStatus, - &i.Rating, - &i.RatingCount, - &i.ReleaseYear, - &i.ReleaseSeason, - &i.Season, - &i.EpisodesAired, - &i.EpisodesAll, - &i.EpisodesLen, - &i.TitleStorageType, - &i.TitleImagePath, - &i.TagNames, - &i.StudioName, - &i.StudioIllustID, - &i.StudioDesc, - &i.StudioStorageType, - &i.StudioImagePath, - ) - return i, err -} - -const getTitleTags = `-- name: GetTitleTags :many -SELECT - tag_names -FROM tags as g -JOIN title_tags as t ON(t.tag_id = g.id) -WHERE t.title_id = $1::bigint -` - -func (q *Queries) GetTitleTags(ctx context.Context, titleID int64) ([]json.RawMessage, error) { - rows, err := q.db.Query(ctx, getTitleTags, titleID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []json.RawMessage{} - for rows.Next() { - var tag_names json.RawMessage - if err := rows.Scan(&tag_names); err != nil { - return nil, err - } - items = append(items, tag_names) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const getUserByExternalServiceId = `-- name: GetUserByExternalServiceId :one -SELECT u.id, u.avatar_id, u.passhash, u.mail, u.nickname, u.disp_name, u.user_desc, u.creation_date, u.last_login -FROM users u -LEFT JOIN external_ids ei ON eu.user_id = u.id -WHERE ei.external_id = $1 AND ei.service_id = $2 -` - -type GetUserByExternalServiceIdParams struct { - ExternalID string `json:"external_id"` - ServiceID int64 `json:"service_id"` -} - -func (q *Queries) GetUserByExternalServiceId(ctx context.Context, arg GetUserByExternalServiceIdParams) (User, error) { - row := q.db.QueryRow(ctx, getUserByExternalServiceId, arg.ExternalID, arg.ServiceID) - var i User - err := row.Scan( - &i.ID, - &i.AvatarID, - &i.Passhash, - &i.Mail, - &i.Nickname, - &i.DispName, - &i.UserDesc, - &i.CreationDate, - &i.LastLogin, - ) - return i, err -} - const getUserByID = `-- name: GetUserByID :one -SELECT - t.id as id, - t.avatar_id as avatar_id, - t.mail as mail, - t.nickname as nickname, - t.disp_name as disp_name, - t.user_desc as user_desc, - t.creation_date as creation_date, - i.storage_type as storage_type, - i.image_path as image_path -FROM users as t -LEFT JOIN images as i ON (t.avatar_id = i.id) -WHERE t.id = $1::bigint +SELECT id, avatar_id, mail, nickname, disp_name, user_desc, creation_date +FROM users +WHERE id = $1 ` type GetUserByIDRow struct { - ID int64 `json:"id"` - AvatarID *int64 `json:"avatar_id"` - Mail *string `json:"mail"` - Nickname string `json:"nickname"` - DispName *string `json:"disp_name"` - UserDesc *string `json:"user_desc"` - CreationDate time.Time `json:"creation_date"` - StorageType *StorageTypeT `json:"storage_type"` - ImagePath *string `json:"image_path"` + ID int64 `json:"id"` + AvatarID *int64 `json:"avatar_id"` + Mail *string `json:"mail"` + Nickname string `json:"nickname"` + DispName *string `json:"disp_name"` + UserDesc *string `json:"user_desc"` + CreationDate time.Time `json:"creation_date"` } func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error) { @@ -321,782 +68,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, er &i.DispName, &i.UserDesc, &i.CreationDate, - &i.StorageType, - &i.ImagePath, - ) - return i, err -} - -const getUserByNickname = `-- name: GetUserByNickname :one -SELECT id, avatar_id, passhash, mail, nickname, disp_name, user_desc, creation_date, last_login -FROM users -WHERE nickname = $1 -` - -func (q *Queries) GetUserByNickname(ctx context.Context, nickname string) (User, error) { - row := q.db.QueryRow(ctx, getUserByNickname, nickname) - var i User - err := row.Scan( - &i.ID, - &i.AvatarID, - &i.Passhash, - &i.Mail, - &i.Nickname, - &i.DispName, - &i.UserDesc, - &i.CreationDate, - &i.LastLogin, - ) - return i, err -} - -const getUserTitleByID = `-- name: GetUserTitleByID :one -SELECT - ut.user_id, ut.title_id, ut.status, ut.rate, ut.review_id, ut.ctime -FROM usertitles as ut -WHERE ut.title_id = $1::bigint AND ut.user_id = $2::bigint -` - -type GetUserTitleByIDParams struct { - TitleID int64 `json:"title_id"` - UserID int64 `json:"user_id"` -} - -func (q *Queries) GetUserTitleByID(ctx context.Context, arg GetUserTitleByIDParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, getUserTitleByID, arg.TitleID, arg.UserID) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, - ) - return i, err -} - -const insertStudio = `-- name: InsertStudio :one -INSERT INTO studios (studio_name, illust_id, studio_desc) -VALUES ( - $1::text, - $2::bigint, - $3::text) -RETURNING id, studio_name, illust_id, studio_desc -` - -type InsertStudioParams struct { - StudioName string `json:"studio_name"` - IllustID *int64 `json:"illust_id"` - StudioDesc *string `json:"studio_desc"` -} - -func (q *Queries) InsertStudio(ctx context.Context, arg InsertStudioParams) (Studio, error) { - row := q.db.QueryRow(ctx, insertStudio, arg.StudioName, arg.IllustID, arg.StudioDesc) - var i Studio - err := row.Scan( - &i.ID, - &i.StudioName, - &i.IllustID, - &i.StudioDesc, - ) - return i, err -} - -const insertTag = `-- name: InsertTag :one -INSERT INTO tags (tag_names) -VALUES ( - $1::jsonb) -RETURNING id, tag_names -` - -func (q *Queries) InsertTag(ctx context.Context, tagNames json.RawMessage) (Tag, error) { - row := q.db.QueryRow(ctx, insertTag, tagNames) - var i Tag - err := row.Scan(&i.ID, &i.TagNames) - return i, err -} - -const insertTitleTags = `-- name: InsertTitleTags :one -INSERT INTO title_tags (title_id, tag_id) -VALUES ( - $1::bigint, - $2::bigint) -RETURNING title_id, tag_id -` - -type InsertTitleTagsParams struct { - TitleID int64 `json:"title_id"` - TagID int64 `json:"tag_id"` -} - -func (q *Queries) InsertTitleTags(ctx context.Context, arg InsertTitleTagsParams) (TitleTag, error) { - row := q.db.QueryRow(ctx, insertTitleTags, arg.TitleID, arg.TagID) - var i TitleTag - err := row.Scan(&i.TitleID, &i.TagID) - return i, err -} - -const insertUserTitle = `-- name: InsertUserTitle :one -INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ctime) -VALUES ( - $1::bigint, - $2::bigint, - $3::usertitle_status_t, - $4::int, - $5::bigint, - $6::timestamptz -) -RETURNING user_id, title_id, status, rate, review_id, ctime -` - -type InsertUserTitleParams struct { - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` - Status UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - ReviewID *int64 `json:"review_id"` - Ftime pgtype.Timestamptz `json:"ftime"` -} - -func (q *Queries) InsertUserTitle(ctx context.Context, arg InsertUserTitleParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, insertUserTitle, - arg.UserID, - arg.TitleID, - arg.Status, - arg.Rate, - arg.ReviewID, - arg.Ftime, - ) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, - ) - return i, err -} - -const searchTitles = `-- name: SearchTitles :many -SELECT - t.id as id, - t.title_names as title_names, - t.poster_id as poster_id, - t.title_status as title_status, - t.rating as rating, - t.rating_count as rating_count, - t.release_year as release_year, - t.release_season as release_season, - t.season as season, - t.episodes_aired as episodes_aired, - t.episodes_all as episodes_all, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name - -FROM titles as t -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) - -WHERE - CASE - WHEN $1::boolean THEN - -- forward: greater than cursor (next page) - CASE $2::text - WHEN 'year' THEN - ($3::int IS NULL) OR - (t.release_year > $3::int) OR - (t.release_year = $3::int AND t.id > $4::bigint) - - WHEN 'rating' THEN - ($5::float IS NULL) OR - (t.rating > $5::float) OR - (t.rating = $5::float AND t.id > $4::bigint) - - WHEN 'id' THEN - ($4::bigint IS NULL) OR - (t.id > $4::bigint) - - ELSE true -- fallback - END - - ELSE - -- backward: less than cursor (prev page) - CASE $2::text - WHEN 'year' THEN - ($3::int IS NULL) OR - (t.release_year < $3::int) OR - (t.release_year = $3::int AND t.id < $4::bigint) - - WHEN 'rating' THEN - ($5::float IS NULL) OR - (t.rating < $5::float) OR - (t.rating = $5::float AND t.id < $4::bigint) - - WHEN 'id' THEN - ($4::bigint IS NULL) OR - (t.id < $4::bigint) - - ELSE true - END - END - - AND ( - CASE - WHEN $6::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array($6::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true - END - ) - - AND ( - $7::title_status_t[] IS NULL - OR array_length($7::title_status_t[], 1) IS NULL - OR array_length($7::title_status_t[], 1) = 0 - OR t.title_status = ANY($7::title_status_t[]) - ) - AND ($8::float IS NULL OR t.rating >= $8::float) - AND ($9::int IS NULL OR t.release_year = $9::int) - AND ($10::release_season_t IS NULL OR t.release_season = $10::release_season_t) - -GROUP BY - t.id, i.id, s.id - -ORDER BY - CASE WHEN $1::boolean THEN - CASE - WHEN $2::text = 'id' THEN t.id - WHEN $2::text = 'year' THEN t.release_year - WHEN $2::text = 'rating' THEN t.rating - END - END ASC, - CASE WHEN NOT $1::boolean THEN - CASE - WHEN $2::text = 'id' THEN t.id - WHEN $2::text = 'year' THEN t.release_year - WHEN $2::text = 'rating' THEN t.rating - END - END DESC, - - CASE WHEN $2::text <> 'id' THEN t.id END ASC - -LIMIT COALESCE($11::int, 100) -` - -type SearchTitlesParams struct { - Forward bool `json:"forward"` - SortBy string `json:"sort_by"` - CursorYear *int32 `json:"cursor_year"` - CursorID *int64 `json:"cursor_id"` - CursorRating *float64 `json:"cursor_rating"` - Word *string `json:"word"` - TitleStatuses []TitleStatusT `json:"title_statuses"` - Rating *float64 `json:"rating"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Limit *int32 `json:"limit"` -} - -type SearchTitlesRow struct { - ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - TitleStorageType *StorageTypeT `json:"title_storage_type"` - TitleImagePath *string `json:"title_image_path"` - TagNames json.RawMessage `json:"tag_names"` - StudioName *string `json:"studio_name"` -} - -func (q *Queries) SearchTitles(ctx context.Context, arg SearchTitlesParams) ([]SearchTitlesRow, error) { - rows, err := q.db.Query(ctx, searchTitles, - arg.Forward, - arg.SortBy, - arg.CursorYear, - arg.CursorID, - arg.CursorRating, - arg.Word, - arg.TitleStatuses, - arg.Rating, - arg.ReleaseYear, - arg.ReleaseSeason, - arg.Limit, - ) - if err != nil { - return nil, err - } - defer rows.Close() - items := []SearchTitlesRow{} - for rows.Next() { - var i SearchTitlesRow - if err := rows.Scan( - &i.ID, - &i.TitleNames, - &i.PosterID, - &i.TitleStatus, - &i.Rating, - &i.RatingCount, - &i.ReleaseYear, - &i.ReleaseSeason, - &i.Season, - &i.EpisodesAired, - &i.EpisodesAll, - &i.TitleStorageType, - &i.TitleImagePath, - &i.TagNames, - &i.StudioName, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const searchUser = `-- name: SearchUser :many -SELECT - u.id AS id, - u.avatar_id AS avatar_id, - u.mail AS mail, - u.nickname AS nickname, - u.disp_name AS disp_name, - u.user_desc AS user_desc, - u.creation_date AS creation_date, - i.storage_type AS storage_type, - i.image_path AS image_path -FROM users AS u -LEFT JOIN images AS i ON u.avatar_id = i.id -WHERE - ( - $1::text IS NULL - OR ( - SELECT bool_and( - u.nickname ILIKE ('%' || term || '%') - OR u.disp_name ILIKE ('%' || term || '%') - ) - FROM unnest(string_to_array(trim($1::text), ' ')) AS term - WHERE term <> '' - ) - ) - AND ( - $2::int IS NULL - OR u.id > $2::int - ) -ORDER BY u.id ASC -LIMIT COALESCE($3::int, 20) -` - -type SearchUserParams struct { - Word *string `json:"word"` - Cursor *int32 `json:"cursor"` - Limit *int32 `json:"limit"` -} - -type SearchUserRow struct { - ID int64 `json:"id"` - AvatarID *int64 `json:"avatar_id"` - Mail *string `json:"mail"` - Nickname string `json:"nickname"` - DispName *string `json:"disp_name"` - UserDesc *string `json:"user_desc"` - CreationDate time.Time `json:"creation_date"` - StorageType *StorageTypeT `json:"storage_type"` - ImagePath *string `json:"image_path"` -} - -func (q *Queries) SearchUser(ctx context.Context, arg SearchUserParams) ([]SearchUserRow, error) { - rows, err := q.db.Query(ctx, searchUser, arg.Word, arg.Cursor, arg.Limit) - if err != nil { - return nil, err - } - defer rows.Close() - items := []SearchUserRow{} - for rows.Next() { - var i SearchUserRow - if err := rows.Scan( - &i.ID, - &i.AvatarID, - &i.Mail, - &i.Nickname, - &i.DispName, - &i.UserDesc, - &i.CreationDate, - &i.StorageType, - &i.ImagePath, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const searchUserTitles = `-- name: SearchUserTitles :many - -SELECT - t.id as id, - t.title_names as title_names, - t.poster_id as poster_id, - t.title_status as title_status, - t.rating as rating, - t.rating_count as rating_count, - t.release_year as release_year, - t.release_season as release_season, - t.season as season, - t.episodes_aired as episodes_aired, - t.episodes_all as episodes_all, - u.user_id as user_id, - u.status as usertitle_status, - u.rate as user_rate, - u.review_id as review_id, - u.ctime as user_ctime, - i.storage_type as title_storage_type, - i.image_path as title_image_path, - COALESCE( - jsonb_agg(g.tag_names) FILTER (WHERE g.tag_names IS NOT NULL), - '[]'::jsonb - )::jsonb as tag_names, - s.studio_name as studio_name - -FROM usertitles as u -JOIN titles as t ON (u.title_id = t.id) -LEFT JOIN images as i ON (t.poster_id = i.id) -LEFT JOIN title_tags as tt ON (t.id = tt.title_id) -LEFT JOIN tags as g ON (tt.tag_id = g.id) -LEFT JOIN studios as s ON (t.studio_id = s.id) - -WHERE - u.user_id = $1::bigint - AND - CASE - WHEN $2::boolean THEN - -- forward: greater than cursor (next page) - CASE $3::text - WHEN 'year' THEN - ($4::int IS NULL) OR - (t.release_year > $4::int) OR - (t.release_year = $4::int AND t.id > $5::bigint) - - WHEN 'rating' THEN - ($6::float IS NULL) OR - (t.rating > $6::float) OR - (t.rating = $6::float AND t.id > $5::bigint) - - WHEN 'id' THEN - ($5::bigint IS NULL) OR - (t.id > $5::bigint) - - ELSE true -- fallback - END - - ELSE - -- backward: less than cursor (prev page) - CASE $3::text - WHEN 'year' THEN - ($4::int IS NULL) OR - (t.release_year < $4::int) OR - (t.release_year = $4::int AND t.id < $5::bigint) - - WHEN 'rating' THEN - ($6::float IS NULL) OR - (t.rating < $6::float) OR - (t.rating = $6::float AND t.id < $5::bigint) - - WHEN 'id' THEN - ($5::bigint IS NULL) OR - (t.id < $5::bigint) - - ELSE true - END - END - - AND ( - CASE - WHEN $7::text IS NOT NULL THEN - ( - SELECT bool_and( - EXISTS ( - SELECT 1 - FROM jsonb_each_text(t.title_names) AS t(key, val) - WHERE val ILIKE pattern - ) - ) - FROM unnest( - ARRAY( - SELECT '%' || trim(w) || '%' - FROM unnest(string_to_array($7::text, ' ')) AS w - WHERE trim(w) <> '' - ) - ) AS pattern - ) - ELSE true - END - ) - - AND ( - $8::title_status_t[] IS NULL - OR array_length($8::title_status_t[], 1) IS NULL - OR array_length($8::title_status_t[], 1) = 0 - OR t.title_status = ANY($8::title_status_t[]) - ) - AND ( - $9::usertitle_status_t[] IS NULL - OR array_length($9::usertitle_status_t[], 1) IS NULL - OR array_length($9::usertitle_status_t[], 1) = 0 - OR u.status = ANY($9::usertitle_status_t[]) - ) - AND ($10::int IS NULL OR u.rate >= $10::int) - AND ($11::float IS NULL OR t.rating >= $11::float) - AND ($12::int IS NULL OR t.release_year = $12::int) - AND ($13::release_season_t IS NULL OR t.release_season = $13::release_season_t) - -GROUP BY - t.id, u.user_id, u.status, u.rate, u.review_id, u.ctime, i.id, s.id - -ORDER BY - CASE WHEN $2::boolean THEN - CASE - WHEN $3::text = 'id' THEN t.id - WHEN $3::text = 'year' THEN t.release_year - WHEN $3::text = 'rating' THEN t.rating - WHEN $3::text = 'rate' THEN u.rate - END - END ASC, - CASE WHEN NOT $2::boolean THEN - CASE - WHEN $3::text = 'id' THEN t.id - WHEN $3::text = 'year' THEN t.release_year - WHEN $3::text = 'rating' THEN t.rating - WHEN $3::text = 'rate' THEN u.rate - END - END DESC, - - CASE WHEN $3::text <> 'id' THEN t.id END ASC - -LIMIT COALESCE($14::int, 100) -` - -type SearchUserTitlesParams struct { - UserID int64 `json:"user_id"` - Forward bool `json:"forward"` - SortBy string `json:"sort_by"` - CursorYear *int32 `json:"cursor_year"` - CursorID *int64 `json:"cursor_id"` - CursorRating *float64 `json:"cursor_rating"` - Word *string `json:"word"` - TitleStatuses []TitleStatusT `json:"title_statuses"` - UsertitleStatuses []UsertitleStatusT `json:"usertitle_statuses"` - Rate *int32 `json:"rate"` - Rating *float64 `json:"rating"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Limit *int32 `json:"limit"` -} - -type SearchUserTitlesRow struct { - ID int64 `json:"id"` - TitleNames json.RawMessage `json:"title_names"` - PosterID *int64 `json:"poster_id"` - TitleStatus TitleStatusT `json:"title_status"` - Rating *float64 `json:"rating"` - RatingCount *int32 `json:"rating_count"` - ReleaseYear *int32 `json:"release_year"` - ReleaseSeason *ReleaseSeasonT `json:"release_season"` - Season *int32 `json:"season"` - EpisodesAired *int32 `json:"episodes_aired"` - EpisodesAll *int32 `json:"episodes_all"` - UserID int64 `json:"user_id"` - UsertitleStatus UsertitleStatusT `json:"usertitle_status"` - UserRate *int32 `json:"user_rate"` - ReviewID *int64 `json:"review_id"` - UserCtime time.Time `json:"user_ctime"` - TitleStorageType *StorageTypeT `json:"title_storage_type"` - TitleImagePath *string `json:"title_image_path"` - TagNames json.RawMessage `json:"tag_names"` - StudioName *string `json:"studio_name"` -} - -// 100 is default limit -func (q *Queries) SearchUserTitles(ctx context.Context, arg SearchUserTitlesParams) ([]SearchUserTitlesRow, error) { - rows, err := q.db.Query(ctx, searchUserTitles, - arg.UserID, - arg.Forward, - arg.SortBy, - arg.CursorYear, - arg.CursorID, - arg.CursorRating, - arg.Word, - arg.TitleStatuses, - arg.UsertitleStatuses, - arg.Rate, - arg.Rating, - arg.ReleaseYear, - arg.ReleaseSeason, - arg.Limit, - ) - if err != nil { - return nil, err - } - defer rows.Close() - items := []SearchUserTitlesRow{} - for rows.Next() { - var i SearchUserTitlesRow - if err := rows.Scan( - &i.ID, - &i.TitleNames, - &i.PosterID, - &i.TitleStatus, - &i.Rating, - &i.RatingCount, - &i.ReleaseYear, - &i.ReleaseSeason, - &i.Season, - &i.EpisodesAired, - &i.EpisodesAll, - &i.UserID, - &i.UsertitleStatus, - &i.UserRate, - &i.ReviewID, - &i.UserCtime, - &i.TitleStorageType, - &i.TitleImagePath, - &i.TagNames, - &i.StudioName, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateUser = `-- name: UpdateUser :one -UPDATE users -SET - avatar_id = COALESCE($1, avatar_id), - disp_name = COALESCE($2, disp_name), - user_desc = COALESCE($3, user_desc), - mail = COALESCE($4, mail) -WHERE id = $5 -RETURNING id, avatar_id, nickname, disp_name, user_desc, creation_date, mail -` - -type UpdateUserParams struct { - AvatarID *int64 `json:"avatar_id"` - DispName *string `json:"disp_name"` - UserDesc *string `json:"user_desc"` - Mail *string `json:"mail"` - UserID int64 `json:"user_id"` -} - -type UpdateUserRow struct { - ID int64 `json:"id"` - AvatarID *int64 `json:"avatar_id"` - Nickname string `json:"nickname"` - DispName *string `json:"disp_name"` - UserDesc *string `json:"user_desc"` - CreationDate time.Time `json:"creation_date"` - Mail *string `json:"mail"` -} - -func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) { - row := q.db.QueryRow(ctx, updateUser, - arg.AvatarID, - arg.DispName, - arg.UserDesc, - arg.Mail, - arg.UserID, - ) - var i UpdateUserRow - err := row.Scan( - &i.ID, - &i.AvatarID, - &i.Nickname, - &i.DispName, - &i.UserDesc, - &i.CreationDate, - &i.Mail, - ) - return i, err -} - -const updateUserTitle = `-- name: UpdateUserTitle :one -UPDATE usertitles -SET - status = COALESCE($1::usertitle_status_t, status), - rate = COALESCE($2::int, rate), - ctime = COALESCE($3::timestamptz, ctime) -WHERE - user_id = $4 - AND title_id = $5 -RETURNING user_id, title_id, status, rate, review_id, ctime -` - -type UpdateUserTitleParams struct { - Status *UsertitleStatusT `json:"status"` - Rate *int32 `json:"rate"` - Ftime pgtype.Timestamptz `json:"ftime"` - UserID int64 `json:"user_id"` - TitleID int64 `json:"title_id"` -} - -// Fails with sql.ErrNoRows if (user_id, title_id) not found -func (q *Queries) UpdateUserTitle(ctx context.Context, arg UpdateUserTitleParams) (Usertitle, error) { - row := q.db.QueryRow(ctx, updateUserTitle, - arg.Status, - arg.Rate, - arg.Ftime, - arg.UserID, - arg.TitleID, - ) - var i Usertitle - err := row.Scan( - &i.UserID, - &i.TitleID, - &i.Status, - &i.Rate, - &i.ReviewID, - &i.Ctime, ) return i, err } diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 904abaf..f44761e 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -3,7 +3,6 @@ sql: - engine: "postgresql" queries: - "../modules/backend/queries.sql" - - "../modules/auth/queries.sql" schema: "migrations" gen: go: @@ -13,20 +12,7 @@ sql: sql_driver: "github.com/jackc/pgx/v5" emit_json_tags: true emit_pointers_for_null_types: true - emit_empty_slices: true #slices returned by :many queries will be empty instead of nil overrides: - - db_type: "usertitle_status_t" - nullable: true - go_type: - type: "UsertitleStatusT" - pointer: true - - db_type: "storage_type_t" - nullable: true - go_type: - type: "StorageTypeT" - pointer: true - - db_type: "jsonb" - go_type: "encoding/json.RawMessage" - db_type: "uuid" nullable: false go_type: @@ -38,14 +24,4 @@ sql: nullable: false go_type: import: "time" - type: "Time" - - db_type: "title_status_t" - nullable: true - go_type: - pointer: true - type: "TitleStatusT" - - db_type: "release_season_t" - nullable: true - go_type: - pointer: true - type: "ReleaseSeasonT" \ No newline at end of file + type: "Time" \ No newline at end of file