diff --git a/.forgejo/workflows/dev-ars.yml b/.forgejo/workflows/dev-ars.yml deleted file mode 100644 index 3bcfb7b..0000000 --- a/.forgejo/workflows/dev-ars.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Build (backend build only) - -on: - push: - branches: - - dev-ars - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - # Build backend - - uses: actions/setup-go@v6 - with: - go-version: '^1.25' - - - name: Build backend - run: | - cd modules/backend - go build -o nyanimedb . - tar -czvf nyanimedb-backend.tar.gz nyanimedb - - - name: Upload built backend to artifactory - uses: actions/upload-artifact@v3 - with: - name: nyanimedb-backend.tar.gz - path: modules/backend/nyanimedb-backend.tar.gz - - # Build Docker images - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ vars.REGISTRY }} - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_TOKEN }} - - - name: Build and push backend image - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfiles/Dockerfile_backend - push: true - tags: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest \ No newline at end of file diff --git a/.forgejo/workflows/front.yml b/.forgejo/workflows/front.yml deleted file mode 100644 index 08a24ec..0000000 --- a/.forgejo/workflows/front.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Build (frontend build only) - -on: - push: - branches: - - front - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - # Build frontend - - uses: actions/setup-node@v5 - with: - node-version-file: modules/frontend/package.json - cache: npm - cache-dependency-path: modules/frontend/package-lock.json - - - name: Build frontend - env: - VITE_BACKEND_API_BASE_URL: ${{ vars.BACKEND_API_BASE_URL }} - run: | - cd modules/frontend - npm install - npm run build - tar -czvf nyanimedb-frontend.tar.gz dist/ - - - name: Upload built frontend to artifactory - uses: actions/upload-artifact@v3 - with: - name: nyanimedb-frontend.tar.gz - path: modules/frontend/nyanimedb-frontend.tar.gz - - # Build Docker images - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ vars.REGISTRY }} - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_TOKEN }} - - - name: Build and push frontend image - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfiles/Dockerfile_frontend - push: true - tags: meowgit.nekoea.red/nihonium/nyanimedb-frontend:latest \ No newline at end of file diff --git a/Dockerfiles/Dockerfile_etl b/Dockerfiles/Dockerfile_etl deleted file mode 100644 index 7ddbfe2..0000000 --- a/Dockerfiles/Dockerfile_etl +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3.12-slim - -WORKDIR /app/modules/anime_etl -COPY modules/anime_etl/pyproject.toml modules/anime_etl/uv.lock ./ - -RUN pip install --no-cache-dir uv \ - && uv sync --frozen --no-dev - -COPY modules/anime_etl ./ - -ENV NYANIMEDB_MEDIA_ROOT=/media - -CMD ["uv", "run", "python", "-m", "rabbit_worker"] diff --git a/Dockerfiles/Dockerfile_image_storage b/Dockerfiles/Dockerfile_image_storage deleted file mode 100644 index e0f60b5..0000000 --- a/Dockerfiles/Dockerfile_image_storage +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3.12-slim - -WORKDIR /app/modules/image_storage -COPY modules/image_storage/pyproject.toml modules/image_storage/uv.lock ./ - -RUN pip install --no-cache-dir uv \ - && uv sync --frozen --no-dev - -COPY modules/image_storage ./ - -ENV NYANIMEDB_MEDIA_ROOT=/media - -EXPOSE 8000 - -CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/api/_build/openapi.yaml b/api/_build/openapi.yaml index 738fdde..7f483fa 100644 --- a/api/_build/openapi.yaml +++ b/api/_build/openapi.yaml @@ -395,9 +395,6 @@ paths: rate: type: integer format: int32 - ftime: - type: string - format: date-time required: - title_id - status @@ -481,9 +478,6 @@ paths: rate: type: integer format: int32 - ftime: - type: string - format: date-time responses: '200': description: Title successfully updated @@ -533,42 +527,6 @@ paths: 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: @@ -689,11 +647,6 @@ components: 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: @@ -800,7 +753,7 @@ components: review_id: type: integer format: int64 - ftime: + ctime: type: string format: date-time required: @@ -824,7 +777,7 @@ components: review_id: type: integer format: int64 - ftime: + ctime: type: string format: date-time required: diff --git a/api/api.gen.go b/api/api.gen.go index 39c7080..4fa16f4 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" @@ -116,9 +113,6 @@ type Title struct { // 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"` @@ -156,7 +150,7 @@ type User struct { // UserTitle defines model for UserTitle. type UserTitle struct { - Ftime *time.Time `json:"ftime,omitempty"` + Ctime *time.Time `json:"ctime,omitempty"` Rate *int32 `json:"rate,omitempty"` ReviewId *int64 `json:"review_id,omitempty"` @@ -168,7 +162,7 @@ type UserTitle struct { // UserTitleMini defines model for UserTitleMini. type UserTitleMini struct { - Ftime *time.Time `json:"ftime,omitempty"` + Ctime *time.Time `json:"ctime,omitempty"` Rate *int32 `json:"rate,omitempty"` ReviewId *int64 `json:"review_id,omitempty"` @@ -184,9 +178,6 @@ 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"` @@ -262,8 +253,7 @@ type GetUserTitlesParams struct { // AddUserTitleJSONBody defines parameters for AddUserTitle. type AddUserTitleJSONBody struct { - Ftime *time.Time `json:"ftime,omitempty"` - Rate *int32 `json:"rate,omitempty"` + Rate *int32 `json:"rate,omitempty"` // Status User's title status Status UserTitleStatus `json:"status"` @@ -272,16 +262,12 @@ type AddUserTitleJSONBody struct { // UpdateUserTitleJSONBody defines parameters for UpdateUserTitle. type UpdateUserTitleJSONBody struct { - Ftime *time.Time `json:"ftime,omitempty"` - Rate *int32 `json:"rate,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 @@ -293,9 +279,6 @@ 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) @@ -337,19 +320,6 @@ 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) { @@ -881,7 +851,6 @@ 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) @@ -894,49 +863,6 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.PATCH(options.BaseURL+"/users/:user_id/titles/:title_id", wrapper.UpdateUserTitle) } -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 } @@ -1474,9 +1400,6 @@ func (response UpdateUserTitle500Response) VisitUpdateUserTitleResponse(w http.R // 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) @@ -1521,43 +1444,6 @@ 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 diff --git a/api/openapi.yaml b/api/openapi.yaml index 26813fc..0759a54 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -19,9 +19,7 @@ paths: $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" - + components: parameters: $ref: "./parameters/_index.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/users-id-titles-id.yaml b/api/paths/users-id-titles-id.yaml index 20a174f..1da2b81 100644 --- a/api/paths/users-id-titles-id.yaml +++ b/api/paths/users-id-titles-id.yaml @@ -61,9 +61,6 @@ patch: rate: type: integer format: int32 - ftime: - type: string - format: date-time responses: '200': description: Title successfully updated diff --git a/api/paths/users-id-titles.yaml b/api/paths/users-id-titles.yaml index f1e5e95..75f5461 100644 --- a/api/paths/users-id-titles.yaml +++ b/api/paths/users-id-titles.yaml @@ -122,9 +122,6 @@ post: rate: type: integer format: int32 - ftime: - type: string - format: date-time responses: '200': description: Title successfully added to user diff --git a/api/schemas/Title.yaml b/api/schemas/Title.yaml index fac4a3f..877ee24 100644 --- a/api/schemas/Title.yaml +++ b/api/schemas/Title.yaml @@ -30,11 +30,6 @@ properties: - Титаны ja: - 進撃の巨人 - title_desc: - type: object - description: Localized description. Key = language (ISO 639-1), value = description. - additionalProperties: - type: string studio: $ref: ./Studio.yaml tags: diff --git a/api/schemas/UserTitle.yaml b/api/schemas/UserTitle.yaml index 0b114a3..ef619cb 100644 --- a/api/schemas/UserTitle.yaml +++ b/api/schemas/UserTitle.yaml @@ -17,6 +17,6 @@ properties: review_id: type: integer format: int64 - ftime: + ctime: type: string format: date-time diff --git a/api/schemas/UserTitleMini.yaml b/api/schemas/UserTitleMini.yaml index 0467252..e1a5a74 100644 --- a/api/schemas/UserTitleMini.yaml +++ b/api/schemas/UserTitleMini.yaml @@ -18,6 +18,6 @@ properties: review_id: type: integer format: int64 - ftime: + ctime: type: string format: date-time diff --git a/auth/auth.gen.go b/auth/auth.gen.go index ebef832..b7cd839 100644 --- a/auth/auth.gen.go +++ b/auth/auth.gen.go @@ -13,23 +13,6 @@ 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 { Nickname string `json:"nickname"` @@ -42,9 +25,6 @@ type PostSignUpJSONBody struct { Pass string `json:"pass"` } -// GetImpersonationTokenJSONRequestBody defines body for GetImpersonationToken for application/json ContentType. -type GetImpersonationTokenJSONRequestBody GetImpersonationTokenJSONBody - // PostSignInJSONRequestBody defines body for PostSignIn for application/json ContentType. type PostSignInJSONRequestBody PostSignInJSONBody @@ -53,15 +33,6 @@ type PostSignUpJSONRequestBody PostSignUpJSONBody // ServerInterface represents all server handlers. type ServerInterface interface { - // Get service impersontaion token - // (POST /get-impersonation-token) - GetImpersonationToken(c *gin.Context) - // Logs out the user - // (POST /logout) - Logout(c *gin.Context) - // Refreshes access_token and refresh_token - // (GET /refresh-tokens) - RefreshTokens(c *gin.Context) // Sign in a user and return JWT // (POST /sign-in) PostSignIn(c *gin.Context) @@ -79,47 +50,6 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(c *gin.Context) -// GetImpersonationToken operation middleware -func (siw *ServerInterfaceWrapper) GetImpersonationToken(c *gin.Context) { - - c.Set(BearerAuthScopes, []string{}) - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.GetImpersonationToken(c) -} - -// Logout operation middleware -func (siw *ServerInterfaceWrapper) Logout(c *gin.Context) { - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.Logout(c) -} - -// RefreshTokens operation middleware -func (siw *ServerInterfaceWrapper) RefreshTokens(c *gin.Context) { - - for _, middleware := range siw.HandlerMiddlewares { - middleware(c) - if c.IsAborted() { - return - } - } - - siw.Handler.RefreshTokens(c) -} - // PostSignIn operation middleware func (siw *ServerInterfaceWrapper) PostSignIn(c *gin.Context) { @@ -173,107 +103,10 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options ErrorHandler: errorHandler, } - router.POST(options.BaseURL+"/get-impersonation-token", wrapper.GetImpersonationToken) - router.POST(options.BaseURL+"/logout", wrapper.Logout) - router.GET(options.BaseURL+"/refresh-tokens", wrapper.RefreshTokens) router.POST(options.BaseURL+"/sign-in", wrapper.PostSignIn) router.POST(options.BaseURL+"/sign-up", wrapper.PostSignUp) } -type ClientErrorResponse struct { -} - -type ServerErrorResponse struct { -} - -type UnauthorizedErrorResponse struct { -} - -type GetImpersonationTokenRequestObject struct { - Body *GetImpersonationTokenJSONRequestBody -} - -type GetImpersonationTokenResponseObject interface { - VisitGetImpersonationTokenResponse(w http.ResponseWriter) error -} - -type GetImpersonationToken200JSONResponse struct { - // AccessToken JWT access token - AccessToken string `json:"access_token"` -} - -func (response GetImpersonationToken200JSONResponse) VisitGetImpersonationTokenResponse(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 LogoutRequestObject struct { -} - -type LogoutResponseObject interface { - VisitLogoutResponse(w http.ResponseWriter) error -} - -type Logout200Response struct { -} - -func (response Logout200Response) VisitLogoutResponse(w http.ResponseWriter) error { - w.WriteHeader(200) - return nil -} - -type Logout500Response = ServerErrorResponse - -func (response Logout500Response) VisitLogoutResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - -type RefreshTokensRequestObject struct { -} - -type RefreshTokensResponseObject interface { - VisitRefreshTokensResponse(w http.ResponseWriter) error -} - -type RefreshTokens200Response struct { -} - -func (response RefreshTokens200Response) VisitRefreshTokensResponse(w http.ResponseWriter) error { - w.WriteHeader(200) - return nil -} - -type RefreshTokens400Response = ClientErrorResponse - -func (response RefreshTokens400Response) VisitRefreshTokensResponse(w http.ResponseWriter) error { - w.WriteHeader(400) - return nil -} - -type RefreshTokens401Response = UnauthorizedErrorResponse - -func (response RefreshTokens401Response) VisitRefreshTokensResponse(w http.ResponseWriter) error { - w.WriteHeader(401) - return nil -} - -type RefreshTokens500Response = ServerErrorResponse - -func (response RefreshTokens500Response) VisitRefreshTokensResponse(w http.ResponseWriter) error { - w.WriteHeader(500) - return nil -} - type PostSignInRequestObject struct { Body *PostSignInJSONRequestBody } @@ -294,11 +127,15 @@ func (response PostSignIn200JSONResponse) VisitPostSignInResponse(w http.Respons return json.NewEncoder(w).Encode(response) } -type PostSignIn401Response = UnauthorizedErrorResponse +type PostSignIn401JSONResponse struct { + Error *string `json:"error,omitempty"` +} -func (response PostSignIn401Response) VisitPostSignInResponse(w http.ResponseWriter) error { +func (response PostSignIn401JSONResponse) VisitPostSignInResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(401) - return nil + + return json.NewEncoder(w).Encode(response) } type PostSignUpRequestObject struct { @@ -322,15 +159,6 @@ 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) - // Logs out the user - // (POST /logout) - Logout(ctx context.Context, request LogoutRequestObject) (LogoutResponseObject, error) - // Refreshes access_token and refresh_token - // (GET /refresh-tokens) - RefreshTokens(ctx context.Context, request RefreshTokensRequestObject) (RefreshTokensResponseObject, error) // Sign in a user and return JWT // (POST /sign-in) PostSignIn(ctx context.Context, request PostSignInRequestObject) (PostSignInResponseObject, error) @@ -351,89 +179,6 @@ type strictHandler struct { middlewares []StrictMiddlewareFunc } -// GetImpersonationToken operation middleware -func (sh *strictHandler) GetImpersonationToken(ctx *gin.Context) { - var request GetImpersonationTokenRequestObject - - var body GetImpersonationTokenJSONRequestBody - 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.GetImpersonationToken(ctx, request.(GetImpersonationTokenRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "GetImpersonationToken") - } - - response, err := handler(ctx, request) - - 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 { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// Logout operation middleware -func (sh *strictHandler) Logout(ctx *gin.Context) { - var request LogoutRequestObject - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.Logout(ctx, request.(LogoutRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "Logout") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(LogoutResponseObject); ok { - if err := validResponse.VisitLogoutResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - -// RefreshTokens operation middleware -func (sh *strictHandler) RefreshTokens(ctx *gin.Context) { - var request RefreshTokensRequestObject - - handler := func(ctx *gin.Context, request interface{}) (interface{}, error) { - return sh.ssi.RefreshTokens(ctx, request.(RefreshTokensRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "RefreshTokens") - } - - response, err := handler(ctx, request) - - if err != nil { - ctx.Error(err) - ctx.Status(http.StatusInternalServerError) - } else if validResponse, ok := response.(RefreshTokensResponseObject); ok { - if err := validResponse.VisitRefreshTokensResponse(ctx.Writer); err != nil { - ctx.Error(err) - } - } else if response != nil { - ctx.Error(fmt.Errorf("unexpected response type: %T", response)) - } -} - // PostSignIn operation middleware func (sh *strictHandler) PostSignIn(ctx *gin.Context) { var request PostSignInRequestObject diff --git a/auth/claims.go b/auth/claims.go deleted file mode 100644 index 6a97483..0000000 --- a/auth/claims.go +++ /dev/null @@ -1,9 +0,0 @@ -package auth - -import "github.com/golang-jwt/jwt/v5" - -type TokenClaims struct { - Type string `json:"type"` - ImpID *string `json:"imp_id,omitempty"` - jwt.RegisteredClaims -} diff --git a/auth/openapi-auth.yaml b/auth/openapi-auth.yaml index 8603423..5f3ebd6 100644 --- a/auth/openapi-auth.yaml +++ b/auth/openapi-auth.yaml @@ -10,7 +10,6 @@ paths: /sign-up: post: summary: Sign up a new user - operationId: postSignUp tags: [Auth] requestBody: required: true @@ -42,7 +41,6 @@ paths: /sign-in: post: summary: Sign in a user and return JWT - operationId: postSignIn tags: [Auth] requestBody: required: true @@ -75,83 +73,88 @@ paths: user_name: type: string "401": - $ref: '#/components/responses/UnauthorizedError' - - /get-impersonation-token: - post: - summary: Get service impersontaion token - operationId: getImpersonationToken - tags: [Auth] - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - user_id: - type: integer - format: int64 - external_id: - type: integer - format: int64 - oneOf: - - required: ["user_id"] - - required: ["external_id"] - responses: - "200": - description: Generated impersonation access token + description: Access denied due to invalid credentials content: application/json: schema: type: object - required: - - access_token properties: - access_token: + error: type: string - description: JWT access token - "401": - $ref: '#/components/responses/UnauthorizedError' - - /refresh-tokens: - get: - summary: Refreshes access_token and refresh_token - operationId: refreshTokens - tags: [Auth] - responses: - # This one sets two cookies: access_token and refresh_token - "200": - description: Refresh success - "400": - $ref: '#/components/responses/ClientError' - "401": - $ref: '#/components/responses/UnauthorizedError' - "500": - $ref: '#/components/responses/ServerError' - - /logout: - post: - summary: Logs out the user - operationId: logout - tags: [Auth] - responses: - "200": - description: Logout success - "500": - $ref: '#/components/responses/ServerError' - -components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - responses: - UnauthorizedError: - description: Access token is missing or invalid - ServerError: - description: ServerError - ClientError: - description: ClientError \ No newline at end of file + example: "Access denied" + # /auth/verify-token: + # post: + # summary: Verify JWT validity + # tags: [Auth] + # requestBody: + # required: true + # content: + # application/json: + # schema: + # type: object + # required: [token] + # properties: + # token: + # type: string + # description: JWT token to validate + # responses: + # "200": + # description: Token validation result + # content: + # application/json: + # schema: + # type: object + # properties: + # valid: + # type: boolean + # description: True if token is valid + # user_id: + # type: string + # nullable: true + # description: User ID extracted from token if valid + # error: + # type: string + # nullable: true + # description: Error message if token is invalid + # /auth/refresh-token: + # post: + # summary: Refresh JWT using a refresh token + # tags: [Auth] + # requestBody: + # required: true + # content: + # application/json: + # schema: + # type: object + # required: [refresh_token] + # properties: + # refresh_token: + # type: string + # description: JWT refresh token obtained from sign-in + # responses: + # "200": + # description: New access (and optionally refresh) token + # content: + # application/json: + # schema: + # type: object + # properties: + # valid: + # type: boolean + # description: True if refresh token was valid + # user_id: + # type: string + # nullable: true + # description: User ID extracted from refresh token + # access_token: + # type: string + # description: New access token + # nullable: true + # refresh_token: + # type: string + # description: New refresh token (optional) + # nullable: true + # error: + # type: string + # nullable: true + # description: Error message if refresh token is invalid diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 9e62c13..aa4c065 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -40,22 +40,6 @@ services: 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 - nyanimedb-backend: image: meowgit.nekoea.red/nihonium/nyanimedb-backend:latest container_name: nyanimedb-backend @@ -70,8 +54,6 @@ services: IMAGES_BASE_URL: http://nyanimedb-images:8000 ports: - "8080:8080" - # ports: - # - "8080:8080" depends_on: - postgres - rabbitmq @@ -87,8 +69,8 @@ services: DATABASE_URL: ${DATABASE_URL} SERVICE_ADDRESS: ${SERVICE_ADDRESS} JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY} - # ports: - # - "8082:8082" + ports: + - "8082:8082" depends_on: - postgres networks: @@ -142,7 +124,6 @@ volumes: postgres_data: rabbitmq_data: media_data: - redis_data: networks: nyanimedb-network: diff --git a/go.mod b/go.mod index 08a3dc1..6662bc1 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,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 @@ -43,13 +42,12 @@ require ( 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..520a22b 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,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= @@ -105,18 +103,10 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y 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= @@ -124,15 +114,11 @@ 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= @@ -145,8 +131,6 @@ 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= @@ -160,16 +144,12 @@ 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= diff --git a/modules/anime_etl/db/repository.py b/modules/anime_etl/db/repository.py index 7c09329..4c5caee 100644 --- a/modules/anime_etl/db/repository.py +++ b/modules/anime_etl/db/repository.py @@ -80,7 +80,7 @@ async def get_or_create_image( VALUES (%s, %s) RETURNING id """, - ("local", rel_path), + ("image-service", rel_path), ) row = await cur.fetchone() return row["id"] diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go index 0569b59..ac55abe 100644 --- a/modules/auth/handlers/handlers.go +++ b/modules/auth/handlers/handlers.go @@ -47,35 +47,10 @@ func CheckPassword(password, hash string) (bool, error) { return argon2id.ComparePasswordAndHash(password, hash) } -func (s *Server) generateImpersonationToken(userID string, impersonatedBy string) (string, error) { - now := time.Now() - claims := auth.TokenClaims{ - ImpID: &impersonatedBy, - Type: "access", - RegisteredClaims: jwt.RegisteredClaims{ - Subject: userID, - IssuedAt: jwt.NewNumericDate(now), - ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)), - ID: generateJTI(), - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(s.JwtPrivateKey)) -} - -func (s *Server) generateTokens(userID string) (accessToken string, refreshToken string, csrfToken string, err error) { - now := time.Now() - - // Access token (15 мин) - accessClaims := auth.TokenClaims{ - Type: "access", - RegisteredClaims: jwt.RegisteredClaims{ - Subject: userID, - IssuedAt: jwt.NewNumericDate(now), - ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)), - ID: generateJTI(), - }, +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(), } at := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) accessToken, err = at.SignedString([]byte(s.JwtPrivateKey)) @@ -83,15 +58,9 @@ func (s *Server) generateTokens(userID string) (accessToken string, refreshToken return "", "", "", err } - // Refresh token (7 дней) - refreshClaims := auth.TokenClaims{ - Type: "refresh", - RegisteredClaims: jwt.RegisteredClaims{ - Subject: userID, - IssuedAt: jwt.NewNumericDate(now), - ExpiresAt: jwt.NewNumericDate(now.Add(7 * 24 * time.Hour)), - ID: generateJTI(), - }, + 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)) @@ -99,7 +68,6 @@ func (s *Server) generateTokens(userID string) (accessToken string, refreshToken return "", "", "", err } - // CSRF token csrfBytes := make([]byte, 32) _, err = rand.Read(csrfBytes) if err != nil { @@ -151,10 +119,13 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject // TODO: return 500 } if !ok { - return auth.PostSignIn401Response{}, nil + err_msg := "invalid credentials" + return auth.PostSignIn401JSONResponse{ + Error: &err_msg, + }, nil } - accessToken, refreshToken, csrfToken, err := s.generateTokens(fmt.Sprintf("%d", user.ID)) + 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 @@ -173,144 +144,95 @@ func (s Server) PostSignIn(ctx context.Context, req auth.PostSignInRequestObject return result, 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) +// 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 accessSecret, nil +// }) - 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 - } +// if err != nil { +// e := err.Error() +// errStr = &e +// return auth.PostAuthVerifyToken200JSONResponse{ +// Valid: &valid, +// UserId: userID, +// Error: errStr, +// }, nil +// } - var user_id string = "" +// if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { +// if uid, ok := claims["user_id"].(string); ok { +// valid = true +// userID = &uid +// } else { +// e := "user_id not found in token" +// errStr = &e +// } +// } else { +// e := "invalid token claims" +// errStr = &e +// } - 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 - } +// return auth.PostAuthVerifyToken200JSONResponse{ +// Valid: &valid, +// UserId: userID, +// Error: errStr, +// }, nil +// } - user_id = fmt.Sprintf("%d", user.ID) - } +// func (s Server) PostAuthRefreshToken(ctx context.Context, req auth.PostAuthRefreshTokenRequestObject) (auth.PostAuthRefreshTokenResponseObject, error) { +// valid := false +// var userID *string +// var errStr *string - 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 - } else { - user_id = fmt.Sprintf("%d", *req.Body.UserId) - } - } +// 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 +// }) - 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 - } +// if err != nil { +// e := err.Error() +// errStr = &e +// return auth.PostAuthVerifyToken200JSONResponse{ +// Valid: &valid, +// UserId: userID, +// Error: errStr, +// }, nil +// } - return auth.GetImpersonationToken200JSONResponse{AccessToken: accessToken}, 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 +// } -func (s Server) RefreshTokens(ctx context.Context, req auth.RefreshTokensRequestObject) (auth.RefreshTokensResponseObject, error) { - ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) - if !ok { - log.Print("failed to get gin context") - return auth.RefreshTokens500Response{}, fmt.Errorf("failed to get gin.Context from context.Context") - } - - rtCookie, err := ginCtx.Request.Cookie("refresh_token") - if err != nil { - log.Print("failed to get refresh_token cookie") - return auth.RefreshTokens400Response{}, fmt.Errorf("failed to get refresh_token cookie") - } - - refreshToken := rtCookie.Value - - token, err := jwt.ParseWithClaims(refreshToken, &auth.TokenClaims{}, func(t *jwt.Token) (interface{}, error) { - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method") - } - return []byte(s.JwtPrivateKey), nil - }) - if err != nil || !token.Valid { - log.Print("invalid refresh token") - return auth.RefreshTokens401Response{}, nil - } - - claims, ok := token.Claims.(*auth.TokenClaims) - if !ok || claims.Subject == "" { - log.Print("invalid refresh token claims") - return auth.RefreshTokens401Response{}, nil - } - if claims.Type != "refresh" { - log.Errorf("token is not a refresh token") - return auth.RefreshTokens401Response{}, nil - } - - accessToken, refreshToken, csrfToken, err := s.generateTokens(claims.Subject) - if err != nil { - log.Errorf("failed to generate tokens for user %s: %v", claims.Subject, err) - return auth.RefreshTokens500Response{}, nil - } - - // 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) - - return auth.RefreshTokens200Response{}, nil -} - -func (s Server) Logout(ctx context.Context, req auth.LogoutRequestObject) (auth.LogoutResponseObject, error) { - // TODO: get current tokens and add them to block list - ginCtx, ok := ctx.Value(gin.ContextKey).(*gin.Context) - if !ok { - log.Print("failed to get gin context") - return auth.Logout500Response{}, fmt.Errorf("failed to get gin.Context from context.Context") - } - - // Delete cookies by setting MaxAge negative - // TODO: change secure to true - ginCtx.SetCookie("access_token", "", -1, "/api", "", false, true) - ginCtx.SetCookie("refresh_token", "", -1, "/auth", "", false, true) - ginCtx.SetCookie("xsrf_token", "", -1, "/", "", false, false) - - return auth.Logout200Response{}, 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 -} - -func generateJTI() string { - b := make([]byte, 16) - _, _ = rand.Read(b) - return base64.RawURLEncoding.EncodeToString(b) -} +// return auth.PostAuthVerifyToken200JSONResponse{ +// Valid: &valid, +// UserId: userID, +// Error: errStr, +// }, nil +// } diff --git a/modules/auth/main.go b/modules/auth/main.go index bbeb014..7305b7d 100644 --- a/modules/auth/main.go +++ b/modules/auth/main.go @@ -46,7 +46,7 @@ func main() { log.Info("allow origins:", AppConfig.ServiceAddress) r.Use(cors.New(cors.Config{ - AllowOrigins: []string{AppConfig.ServiceAddress}, + AllowOrigins: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, ExposeHeaders: []string{"Content-Length"}, diff --git a/modules/auth/queries.sql b/modules/auth/queries.sql index 0b9b941..828d2af 100644 --- a/modules/auth/queries.sql +++ b/modules/auth/queries.sql @@ -9,13 +9,3 @@ 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/backend/handlers/common.go b/modules/backend/handlers/common.go index 7f2807f..58862e1 100644 --- a/modules/backend/handlers/common.go +++ b/modules/backend/handlers/common.go @@ -73,14 +73,6 @@ func (s Server) mapTitle(title sqlc.GetTitleByIDRow) (oapi.Title, error) { } 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) 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/users.go b/modules/backend/handlers/users.go index 100835a..995d5af 100644 --- a/modules/backend/handlers/users.go +++ b/modules/backend/handlers/users.go @@ -69,19 +69,6 @@ func sqlDate2oapi(p_date pgtype.Timestamptz) *time.Time { return nil } -func oapiDate2sql(t *time.Time) pgtype.Timestamptz { - if t == nil { - return pgtype.Timestamptz{ - Time: time.Now(), - Valid: true, - } - } - return pgtype.Timestamptz{ - Time: *t, - Valid: true, - } -} - // func UserTitleStatus2Sqlc(s *[]oapi.UserTitleStatus) (*SqlcUserStatus, error) { // var sqlc_status SqlcUserStatus // if s == nil { @@ -170,7 +157,7 @@ func UserTitleStatus2Sqlc1(s *oapi.UserTitleStatus) (*sqlc.UsertitleStatusT, err func (s Server) mapUsertitle(ctx context.Context, t sqlc.SearchUserTitlesRow) (oapi.UserTitle, error) { oapi_usertitle := oapi.UserTitle{ - Ftime: &t.UserFtime, + Ctime: &t.UserCtime, Rate: t.UserRate, ReviewId: t.ReviewID, // Status: , @@ -378,7 +365,6 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque TitleID: request.Body.TitleId, Status: *status, Rate: request.Body.Rate, - Ftime: oapiDate2sql(request.Body.Ftime), } user_title, err := s.db.InsertUserTitle(ctx, params) @@ -389,9 +375,6 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque // 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 } } else { log.Errorf("%v", err) @@ -404,7 +387,7 @@ func (s Server) AddUserTitle(ctx context.Context, request oapi.AddUserTitleReque return oapi.AddUserTitle500Response{}, nil } oapi_usertitle := oapi.UserTitleMini{ - Ftime: &user_title.Ftime, + Ctime: &user_title.Ctime, Rate: user_title.Rate, ReviewId: user_title.ReviewID, Status: oapi_status, @@ -445,7 +428,6 @@ func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitl Rate: request.Body.Rate, UserID: request.UserId, TitleID: request.TitleId, - Ftime: oapiDate2sql(request.Body.Ftime), } user_title, err := s.db.UpdateUserTitle(ctx, params) @@ -463,7 +445,7 @@ func (s Server) UpdateUserTitle(ctx context.Context, request oapi.UpdateUserTitl } oapi_usertitle := oapi.UserTitleMini{ - Ftime: &user_title.Ftime, + Ctime: &user_title.Ctime, Rate: user_title.Rate, ReviewId: user_title.ReviewID, Status: oapi_status, @@ -493,7 +475,7 @@ func (s Server) GetUserTitle(ctx context.Context, request oapi.GetUserTitleReque return oapi.GetUserTitle500Response{}, nil } oapi_usertitle := oapi.UserTitleMini{ - Ftime: &user_title.Ftime, + Ctime: &user_title.Ctime, Rate: user_title.Rate, ReviewId: user_title.ReviewID, Status: oapi_status, diff --git a/modules/backend/middlewares/access.go b/modules/backend/middlewares/access.go index 9b15f8f..73200e8 100644 --- a/modules/backend/middlewares/access.go +++ b/modules/backend/middlewares/access.go @@ -3,11 +3,8 @@ package middleware import ( "context" "errors" - "fmt" "net/http" - "nyanimedb/auth" - "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) @@ -40,18 +37,12 @@ func JWTAuthMiddleware(secret string) gin.HandlerFunc { } // 2. Парсим токен с MapClaims - token, err := jwt.ParseWithClaims(tokenStr, &auth.TokenClaims{}, func(t *jwt.Token) (interface{}, error) { - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method") + 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 + return []byte(secret), nil // ← конвертируем string → []byte }) - // 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 @@ -64,23 +55,20 @@ func JWTAuthMiddleware(secret string) gin.HandlerFunc { } // 4. Извлекаем user_id из claims - claims, ok := token.Claims.(*auth.TokenClaims) + claims, ok := token.Claims.(jwt.MapClaims) if !ok { abortWithJSON(c, http.StatusUnauthorized, "invalid claims format") return } - if claims.Subject == "" { + userID, ok := claims["user_id"].(string) + if !ok || userID == "" { abortWithJSON(c, http.StatusUnauthorized, "user_id claim missing or invalid") return } - if claims.Type != "access" { - abortWithJSON(c, http.StatusUnauthorized, "token type is not access") - return - } // 5. Сохраняем в контексте - c.Set("user_id", claims.Subject) + c.Set("user_id", userID) // 6. Для oapi-codegen — кладём gin.Context в request context GinContextToContext(c) diff --git a/modules/backend/queries.sql b/modules/backend/queries.sql index 7117456..03502c4 100644 --- a/modules/backend/queries.sql +++ b/modules/backend/queries.sql @@ -268,7 +268,7 @@ SELECT u.status as usertitle_status, u.rate as user_rate, u.review_id as review_id, - u.ftime as user_ftime, + u.ctime as user_ctime, i.storage_type as title_storage_type, i.image_path as title_image_path, COALESCE( @@ -370,7 +370,7 @@ WHERE 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.ftime, i.id, s.id + 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 @@ -400,24 +400,22 @@ FROM reviews WHERE review_id = sqlc.arg('review_id')::bigint; -- name: InsertUserTitle :one -INSERT INTO usertitles (user_id, title_id, status, rate, review_id, ftime) +INSERT INTO usertitles (user_id, title_id, status, rate, review_id) 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 + sqlc.narg('review_id')::bigint ) -RETURNING user_id, title_id, status, rate, review_id, ftime; +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), - ftime = COALESCE(sqlc.narg('ftime')::timestamptz, ftime) + rate = COALESCE(sqlc.narg('rate')::int, rate) WHERE user_id = sqlc.arg('user_id') AND title_id = sqlc.arg('title_id') diff --git a/modules/frontend/nginx-default.conf b/modules/frontend/nginx-default.conf index 6075999..c3a851f 100644 --- a/modules/frontend/nginx-default.conf +++ b/modules/frontend/nginx-default.conf @@ -28,16 +28,6 @@ server { proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } - - location /media/ { - rewrite ^/media/(.*)$ /$1 break; - proxy_pass http://nyanimedb-images:8000/; - 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/src/App.tsx b/modules/frontend/src/App.tsx index de7101c..84c9086 100644 --- a/modules/frontend/src/App.tsx +++ b/modules/frontend/src/App.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { useState, useEffect } from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import UserPage from "./pages/UserPage/UserPage"; import TitlesPage from "./pages/TitlesPage/TitlesPage"; @@ -12,26 +11,12 @@ import { Header } from "./components/Header/Header"; // OpenAPI.WITH_CREDENTIALS = true const App: React.FC = () => { - const [userId, setUserId] = useState(localStorage.getItem("user_id")); - - // 2. Listen for the same event the Header uses - useEffect(() => { - const handleAuthChange = () => { - setUserId(localStorage.getItem("user_id")); - }; - - window.addEventListener("storage", handleAuthChange); - window.addEventListener("local-storage-update", handleAuthChange); - - return () => { - window.removeEventListener("storage", handleAuthChange); - window.removeEventListener("local-storage-update", handleAuthChange); - }; - }, []); + const username = localStorage.getItem("username") || undefined; + const userId = localStorage.getItem("userId"); return ( -
+
{/* auth */} } /> diff --git a/modules/frontend/src/api/client.gen.ts b/modules/frontend/src/api/client.gen.ts index 952c663..2de06ac 100644 --- a/modules/frontend/src/api/client.gen.ts +++ b/modules/frontend/src/api/client.gen.ts @@ -13,4 +13,4 @@ import type { ClientOptions as ClientOptions2 } from './types.gen'; */ export type CreateClientConfig = (override?: Config) => Config & T>; -export const client = createClient(createConfig({ baseUrl: '/api/v1' })); +export const client = createClient(createConfig({ baseUrl: 'http://10.1.0.65:8081/api/v1' })); diff --git a/modules/frontend/src/api/sdk.gen.ts b/modules/frontend/src/api/sdk.gen.ts index 24153db..5359156 100644 --- a/modules/frontend/src/api/sdk.gen.ts +++ b/modules/frontend/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ 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, PostMediaUploadData, PostMediaUploadErrors, PostMediaUploadResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen'; +import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen'; export type Options = Options2 & { /** @@ -32,11 +32,6 @@ export const getTitles = (options?: Option */ 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 */ @@ -113,18 +108,3 @@ export const updateUserTitle = (options: O ...options.headers } }); - -/** - * Upload an image (PNG, JPEG, or WebP) - * - * Uploads a single image file. Supported formats: **PNG**, **JPEG/JPG**, **WebP**. - * - */ -export const postMediaUpload = (options: Options) => (options.client ?? client).post({ - url: '/media/upload', - ...options, - headers: { - 'Content-Type': 'encoding', - ...options.headers - } -}); diff --git a/modules/frontend/src/api/types.gen.ts b/modules/frontend/src/api/types.gen.ts index 1352aa6..ce4db4b 100644 --- a/modules/frontend/src/api/types.gen.ts +++ b/modules/frontend/src/api/types.gen.ts @@ -60,12 +60,6 @@ export type Title = { 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; @@ -125,7 +119,7 @@ export type UserTitle = { status: UserTitleStatus; rate?: number; review_id?: number; - ftime?: string; + ctime?: string; }; export type UserTitleMini = { @@ -134,7 +128,7 @@ export type UserTitleMini = { status: UserTitleStatus; rate?: number; review_id?: number; - ftime?: string; + ctime?: string; }; export type Review = { @@ -237,50 +231,6 @@ export type GetTitleResponses = { 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; - cursor: number; - }; - /** - * No users found - */ - 204: void; -}; - -export type GetUsersResponse = GetUsersResponses[keyof GetUsersResponses]; - export type GetUsersIdData = { body?: never; path: { @@ -453,7 +403,6 @@ export type AddUserTitleData = { title_id: number; status: UserTitleStatus; rate?: number; - ftime?: string; }; path: { /** @@ -579,7 +528,6 @@ export type UpdateUserTitleData = { body: { status?: UserTitleStatus; rate?: number; - ftime?: string; }; path: { user_id: number; @@ -620,38 +568,3 @@ export type UpdateUserTitleResponses = { }; export type UpdateUserTitleResponse = UpdateUserTitleResponses[keyof UpdateUserTitleResponses]; - -export type PostMediaUploadData = { - body: unknown; - path?: never; - query?: never; - url: '/media/upload'; -}; - -export type PostMediaUploadErrors = { - /** - * Bad request — e.g., invalid/malformed image, empty file - */ - 400: string; - /** - * 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) - * - */ - 415: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type PostMediaUploadError = PostMediaUploadErrors[keyof PostMediaUploadErrors]; - -export type PostMediaUploadResponses = { - /** - * Image uploaded successfully - */ - 200: Image; -}; - -export type PostMediaUploadResponse = PostMediaUploadResponses[keyof PostMediaUploadResponses]; diff --git a/modules/frontend/src/auth/client.gen.ts b/modules/frontend/src/auth/client.gen.ts deleted file mode 100644 index ba4855c..0000000 --- a/modules/frontend/src/auth/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: '/auth' })); diff --git a/modules/frontend/src/auth/client/client.gen.ts b/modules/frontend/src/auth/client/client.gen.ts deleted file mode 100644 index c2a5190..0000000 --- a/modules/frontend/src/auth/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/auth/client/index.ts b/modules/frontend/src/auth/client/index.ts deleted file mode 100644 index b295ede..0000000 --- a/modules/frontend/src/auth/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/auth/client/types.gen.ts b/modules/frontend/src/auth/client/types.gen.ts deleted file mode 100644 index b4a499c..0000000 --- a/modules/frontend/src/auth/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/auth/client/utils.gen.ts b/modules/frontend/src/auth/client/utils.gen.ts deleted file mode 100644 index 4c48a9e..0000000 --- a/modules/frontend/src/auth/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/auth/core/ApiError.ts new file mode 100644 index 0000000..ec7b16a --- /dev/null +++ b/modules/frontend/src/auth/core/ApiError.ts @@ -0,0 +1,25 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; + +export class ApiError extends Error { + public readonly url: string; + public readonly status: number; + public readonly statusText: string; + public readonly body: any; + public readonly request: ApiRequestOptions; + + constructor(request: ApiRequestOptions, response: ApiResult, message: string) { + super(message); + + this.name = 'ApiError'; + this.url = response.url; + this.status = response.status; + this.statusText = response.statusText; + this.body = response.body; + this.request = request; + } +} diff --git a/modules/frontend/src/auth/core/ApiRequestOptions.ts b/modules/frontend/src/auth/core/ApiRequestOptions.ts new file mode 100644 index 0000000..93143c3 --- /dev/null +++ b/modules/frontend/src/auth/core/ApiRequestOptions.ts @@ -0,0 +1,17 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiRequestOptions = { + readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; + readonly url: string; + readonly path?: Record; + readonly cookies?: Record; + readonly headers?: Record; + readonly query?: Record; + readonly formData?: Record; + readonly body?: any; + readonly mediaType?: string; + readonly responseHeader?: string; + readonly errors?: Record; +}; diff --git a/modules/frontend/src/auth/core/ApiResult.ts b/modules/frontend/src/auth/core/ApiResult.ts new file mode 100644 index 0000000..ee1126e --- /dev/null +++ b/modules/frontend/src/auth/core/ApiResult.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiResult = { + readonly url: string; + readonly ok: boolean; + readonly status: number; + readonly statusText: string; + readonly body: any; +}; diff --git a/modules/frontend/src/auth/core/CancelablePromise.ts b/modules/frontend/src/auth/core/CancelablePromise.ts new file mode 100644 index 0000000..d70de92 --- /dev/null +++ b/modules/frontend/src/auth/core/CancelablePromise.ts @@ -0,0 +1,131 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export class CancelError extends Error { + + constructor(message: string) { + super(message); + this.name = 'CancelError'; + } + + public get isCancelled(): boolean { + return true; + } +} + +export interface OnCancel { + readonly isResolved: boolean; + readonly isRejected: boolean; + readonly isCancelled: boolean; + + (cancelHandler: () => void): void; +} + +export class CancelablePromise implements Promise { + #isResolved: boolean; + #isRejected: boolean; + #isCancelled: boolean; + readonly #cancelHandlers: (() => void)[]; + readonly #promise: Promise; + #resolve?: (value: T | PromiseLike) => void; + #reject?: (reason?: any) => void; + + constructor( + executor: ( + resolve: (value: T | PromiseLike) => void, + reject: (reason?: any) => void, + onCancel: OnCancel + ) => void + ) { + this.#isResolved = false; + this.#isRejected = false; + this.#isCancelled = false; + this.#cancelHandlers = []; + this.#promise = new Promise((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; + + const onResolve = (value: T | PromiseLike): void => { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#isResolved = true; + if (this.#resolve) this.#resolve(value); + }; + + const onReject = (reason?: any): void => { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#isRejected = true; + if (this.#reject) this.#reject(reason); + }; + + const onCancel = (cancelHandler: () => void): void => { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#cancelHandlers.push(cancelHandler); + }; + + Object.defineProperty(onCancel, 'isResolved', { + get: (): boolean => this.#isResolved, + }); + + Object.defineProperty(onCancel, 'isRejected', { + get: (): boolean => this.#isRejected, + }); + + Object.defineProperty(onCancel, 'isCancelled', { + get: (): boolean => this.#isCancelled, + }); + + return executor(onResolve, onReject, onCancel as OnCancel); + }); + } + + get [Symbol.toStringTag]() { + return "Cancellable Promise"; + } + + public then( + onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onRejected?: ((reason: any) => TResult2 | PromiseLike) | null + ): Promise { + return this.#promise.then(onFulfilled, onRejected); + } + + public catch( + onRejected?: ((reason: any) => TResult | PromiseLike) | null + ): Promise { + return this.#promise.catch(onRejected); + } + + public finally(onFinally?: (() => void) | null): Promise { + return this.#promise.finally(onFinally); + } + + public cancel(): void { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#isCancelled = true; + if (this.#cancelHandlers.length) { + try { + for (const cancelHandler of this.#cancelHandlers) { + cancelHandler(); + } + } catch (error) { + console.warn('Cancellation threw an error', error); + return; + } + } + this.#cancelHandlers.length = 0; + if (this.#reject) this.#reject(new CancelError('Request aborted')); + } + + public get isCancelled(): boolean { + return this.#isCancelled; + } +} diff --git a/modules/frontend/src/auth/core/OpenAPI.ts b/modules/frontend/src/auth/core/OpenAPI.ts new file mode 100644 index 0000000..79aa305 --- /dev/null +++ b/modules/frontend/src/auth/core/OpenAPI.ts @@ -0,0 +1,32 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; + +type Resolver = (options: ApiRequestOptions) => Promise; +type Headers = Record; + +export type OpenAPIConfig = { + BASE: string; + VERSION: string; + WITH_CREDENTIALS: boolean; + CREDENTIALS: 'include' | 'omit' | 'same-origin'; + TOKEN?: string | Resolver | undefined; + USERNAME?: string | Resolver | undefined; + PASSWORD?: string | Resolver | undefined; + HEADERS?: Headers | Resolver | undefined; + ENCODE_PATH?: ((path: string) => string) | undefined; +}; + +export const OpenAPI: OpenAPIConfig = { + BASE: 'http://10.1.0.65:8081/auth', + VERSION: '1.0.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'include', + TOKEN: undefined, + USERNAME: undefined, + PASSWORD: undefined, + HEADERS: undefined, + ENCODE_PATH: undefined, +}; diff --git a/modules/frontend/src/auth/core/auth.gen.ts b/modules/frontend/src/auth/core/auth.gen.ts deleted file mode 100644 index f8a7326..0000000 --- a/modules/frontend/src/auth/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/auth/core/bodySerializer.gen.ts b/modules/frontend/src/auth/core/bodySerializer.gen.ts deleted file mode 100644 index 552b50f..0000000 --- a/modules/frontend/src/auth/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/auth/core/params.gen.ts b/modules/frontend/src/auth/core/params.gen.ts deleted file mode 100644 index 602715c..0000000 --- a/modules/frontend/src/auth/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/auth/core/pathSerializer.gen.ts b/modules/frontend/src/auth/core/pathSerializer.gen.ts deleted file mode 100644 index 8d99931..0000000 --- a/modules/frontend/src/auth/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/auth/core/queryKeySerializer.gen.ts b/modules/frontend/src/auth/core/queryKeySerializer.gen.ts deleted file mode 100644 index d3bb683..0000000 --- a/modules/frontend/src/auth/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/auth/core/request.ts new file mode 100644 index 0000000..1dc6fef --- /dev/null +++ b/modules/frontend/src/auth/core/request.ts @@ -0,0 +1,323 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import axios from 'axios'; +import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios'; +import FormData from 'form-data'; + +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { CancelablePromise } from './CancelablePromise'; +import type { OnCancel } from './CancelablePromise'; +import type { OpenAPIConfig } from './OpenAPI'; + +export const isDefined = (value: T | null | undefined): value is Exclude => { + return value !== undefined && value !== null; +}; + +export const isString = (value: any): value is string => { + return typeof value === 'string'; +}; + +export const isStringWithValue = (value: any): value is string => { + return isString(value) && value !== ''; +}; + +export const isBlob = (value: any): value is Blob => { + return ( + typeof value === 'object' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + typeof value.arrayBuffer === 'function' && + typeof value.constructor === 'function' && + typeof value.constructor.name === 'string' && + /^(Blob|File)$/.test(value.constructor.name) && + /^(Blob|File)$/.test(value[Symbol.toStringTag]) + ); +}; + +export const isFormData = (value: any): value is FormData => { + return value instanceof FormData; +}; + +export const isSuccess = (status: number): boolean => { + return status >= 200 && status < 300; +}; + +export const base64 = (str: string): string => { + try { + return btoa(str); + } catch (err) { + // @ts-ignore + return Buffer.from(str).toString('base64'); + } +}; + +export const getQueryString = (params: Record): string => { + const qs: string[] = []; + + const append = (key: string, value: any) => { + qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; + + const process = (key: string, value: any) => { + if (isDefined(value)) { + if (Array.isArray(value)) { + value.forEach(v => { + process(key, v); + }); + } else if (typeof value === 'object') { + Object.entries(value).forEach(([k, v]) => { + process(`${key}[${k}]`, v); + }); + } else { + append(key, value); + } + } + }; + + Object.entries(params).forEach(([key, value]) => { + process(key, value); + }); + + if (qs.length > 0) { + return `?${qs.join('&')}`; + } + + return ''; +}; + +const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { + const encoder = config.ENCODE_PATH || encodeURI; + + const path = options.url + .replace('{api-version}', config.VERSION) + .replace(/{(.*?)}/g, (substring: string, group: string) => { + if (options.path?.hasOwnProperty(group)) { + return encoder(String(options.path[group])); + } + return substring; + }); + + const url = `${config.BASE}${path}`; + if (options.query) { + return `${url}${getQueryString(options.query)}`; + } + return url; +}; + +export const getFormData = (options: ApiRequestOptions): FormData | undefined => { + if (options.formData) { + const formData = new FormData(); + + const process = (key: string, value: any) => { + if (isString(value) || isBlob(value)) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }; + + Object.entries(options.formData) + .filter(([_, value]) => isDefined(value)) + .forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach(v => process(key, v)); + } else { + process(key, value); + } + }); + + return formData; + } + return undefined; +}; + +type Resolver = (options: ApiRequestOptions) => Promise; + +export const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => { + if (typeof resolver === 'function') { + return (resolver as Resolver)(options); + } + return resolver; +}; + +export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise> => { + const [token, username, password, additionalHeaders] = await Promise.all([ + resolve(options, config.TOKEN), + resolve(options, config.USERNAME), + resolve(options, config.PASSWORD), + resolve(options, config.HEADERS), + ]); + + const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {} + + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + ...formHeaders, + }) + .filter(([_, value]) => isDefined(value)) + .reduce((headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), {} as Record); + + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + + if (options.body !== undefined) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } + + return headers; +}; + +export const getRequestBody = (options: ApiRequestOptions): any => { + if (options.body) { + return options.body; + } + return undefined; +}; + +export const sendRequest = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Record, + onCancel: OnCancel, + axiosClient: AxiosInstance +): Promise> => { + const source = axios.CancelToken.source(); + + const requestConfig: AxiosRequestConfig = { + url, + headers, + data: body ?? formData, + method: options.method, + withCredentials: config.WITH_CREDENTIALS, + withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false, + cancelToken: source.token, + }; + + onCancel(() => source.cancel('The user aborted a request.')); + + try { + return await axiosClient.request(requestConfig); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + return axiosError.response; + } + throw error; + } +}; + +export const getResponseHeader = (response: AxiosResponse, responseHeader?: string): string | undefined => { + if (responseHeader) { + const content = response.headers[responseHeader]; + if (isString(content)) { + return content; + } + } + return undefined; +}; + +export const getResponseBody = (response: AxiosResponse): any => { + if (response.status !== 204) { + return response.data; + } + return undefined; +}; + +export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { + const errors: Record = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + ...options.errors, + } + + const error = errors[result.status]; + if (error) { + throw new ApiError(options, result, error); + } + + if (!result.ok) { + const errorStatus = result.status ?? 'unknown'; + const errorStatusText = result.statusText ?? 'unknown'; + const errorBody = (() => { + try { + return JSON.stringify(result.body, null, 2); + } catch (e) { + return undefined; + } + })(); + + throw new ApiError(options, result, + `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` + ); + } +}; + +/** + * Request method + * @param config The OpenAPI configuration object + * @param options The request options from the service + * @param axiosClient The axios client instance to use + * @returns CancelablePromise + * @throws ApiError + */ +export const request = (config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options, formData); + + if (!onCancel.isCancelled) { + const response = await sendRequest(config, options, url, body, formData, headers, onCancel, axiosClient); + const responseBody = getResponseBody(response); + const responseHeader = getResponseHeader(response, options.responseHeader); + + const result: ApiResult = { + url, + ok: isSuccess(response.status), + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve(result.body); + } + } catch (error) { + reject(error); + } + }); +}; diff --git a/modules/frontend/src/auth/core/serverSentEvents.gen.ts b/modules/frontend/src/auth/core/serverSentEvents.gen.ts deleted file mode 100644 index f8fd78e..0000000 --- a/modules/frontend/src/auth/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/auth/core/types.gen.ts b/modules/frontend/src/auth/core/types.gen.ts deleted file mode 100644 index 643c070..0000000 --- a/modules/frontend/src/auth/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/auth/core/utils.gen.ts b/modules/frontend/src/auth/core/utils.gen.ts deleted file mode 100644 index 0b5389d..0000000 --- a/modules/frontend/src/auth/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/auth/index.ts b/modules/frontend/src/auth/index.ts index c352c10..b0989c4 100644 --- a/modules/frontend/src/auth/index.ts +++ b/modules/frontend/src/auth/index.ts @@ -1,4 +1,10 @@ -// 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 { AuthService } from './services/AuthService'; diff --git a/modules/frontend/src/auth/sdk.gen.ts b/modules/frontend/src/auth/sdk.gen.ts deleted file mode 100644 index f69153e..0000000 --- a/modules/frontend/src/auth/sdk.gen.ts +++ /dev/null @@ -1,66 +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 { GetImpersonationTokenData, GetImpersonationTokenErrors, GetImpersonationTokenResponses, LogoutData, LogoutErrors, LogoutResponses, PostSignInData, PostSignInErrors, PostSignInResponses, PostSignUpData, PostSignUpResponses, RefreshTokensData, RefreshTokensErrors, RefreshTokensResponses } 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; -}; - -/** - * Sign up a new user - */ -export const postSignUp = (options: Options) => (options.client ?? client).post({ - url: '/sign-up', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Sign in a user and return JWT - */ -export const postSignIn = (options: Options) => (options.client ?? client).post({ - url: '/sign-in', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Get service impersontaion token - */ -export const getImpersonationToken = (options: Options) => (options.client ?? client).post({ - security: [{ scheme: 'bearer', type: 'http' }], - url: '/get-impersonation-token', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Refreshes access_token and refresh_token - */ -export const refreshTokens = (options?: Options) => (options?.client ?? client).get({ url: '/refresh-tokens', ...options }); - -/** - * Logs out the user - */ -export const logout = (options?: Options) => (options?.client ?? client).post({ url: '/logout', ...options }); diff --git a/modules/frontend/src/auth/services/AuthService.ts b/modules/frontend/src/auth/services/AuthService.ts new file mode 100644 index 0000000..74a8fa7 --- /dev/null +++ b/modules/frontend/src/auth/services/AuthService.ts @@ -0,0 +1,55 @@ +/* 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/auth/types.gen.ts b/modules/frontend/src/auth/types.gen.ts deleted file mode 100644 index 5c0fdc0..0000000 --- a/modules/frontend/src/auth/types.gen.ts +++ /dev/null @@ -1,136 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: `${string}://${string}/auth` | (string & {}); -}; - -export type PostSignUpData = { - body: { - nickname: string; - pass: string; - }; - path?: never; - query?: never; - url: '/sign-up'; -}; - -export type PostSignUpResponses = { - /** - * Sign-up result - */ - 200: { - user_id: number; - }; -}; - -export type PostSignUpResponse = PostSignUpResponses[keyof PostSignUpResponses]; - -export type PostSignInData = { - body: { - nickname: string; - pass: string; - }; - path?: never; - query?: never; - url: '/sign-in'; -}; - -export type PostSignInErrors = { - /** - * Access token is missing or invalid - */ - 401: unknown; -}; - -export type PostSignInResponses = { - /** - * Sign-in result with JWT - */ - 200: { - user_id: number; - user_name: string; - }; -}; - -export type PostSignInResponse = PostSignInResponses[keyof PostSignInResponses]; - -export type GetImpersonationTokenData = { - body: unknown & { - user_id?: number; - external_id?: number; - }; - path?: never; - query?: never; - url: '/get-impersonation-token'; -}; - -export type GetImpersonationTokenErrors = { - /** - * Access token is missing or invalid - */ - 401: unknown; -}; - -export type GetImpersonationTokenResponses = { - /** - * Generated impersonation access token - */ - 200: { - /** - * JWT access token - */ - access_token: string; - }; -}; - -export type GetImpersonationTokenResponse = GetImpersonationTokenResponses[keyof GetImpersonationTokenResponses]; - -export type RefreshTokensData = { - body?: never; - path?: never; - query?: never; - url: '/refresh-tokens'; -}; - -export type RefreshTokensErrors = { - /** - * ClientError - */ - 400: unknown; - /** - * Access token is missing or invalid - */ - 401: unknown; - /** - * ServerError - */ - 500: unknown; -}; - -export type RefreshTokensResponses = { - /** - * Refresh success - */ - 200: unknown; -}; - -export type LogoutData = { - body?: never; - path?: never; - query?: never; - url: '/logout'; -}; - -export type LogoutErrors = { - /** - * ServerError - */ - 500: unknown; -}; - -export type LogoutResponses = { - /** - * Logout success - */ - 200: unknown; -}; diff --git a/modules/frontend/src/components/Header/Header.tsx b/modules/frontend/src/components/Header/Header.tsx index 36cbd5a..26f1658 100644 --- a/modules/frontend/src/components/Header/Header.tsx +++ b/modules/frontend/src/components/Header/Header.tsx @@ -1,106 +1,72 @@ -import React, { useState, useEffect, useRef } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import React, { useState } from "react"; +import { Link } from "react-router-dom"; import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid"; -import { logout } from "../../auth"; -export const Header: React.FC = () => { - const navigate = useNavigate(); - const [username, setUsername] = useState(localStorage.getItem("user_name")); +type HeaderProps = { + username?: string; +}; + +export const Header: React.FC = ({ username }) => { const [menuOpen, setMenuOpen] = useState(false); - const [dropdownOpen, setDropdownOpen] = useState(false); - const dropdownRef = useRef(null); - - // Listen for localStorage changes to update username dynamically - useEffect(() => { - const handleStorage = () => setUsername(localStorage.getItem("user_name")); - // This catches changes from OTHER tabs - window.addEventListener("storage", handleStorage); - - // This catches changes in the CURRENT tab if you use dispatchEvent - window.addEventListener("local-storage-update", handleStorage); - - return () => { - window.removeEventListener("storage", handleStorage); - window.removeEventListener("local-storage-update", handleStorage); - }; - }, []); - - const handleLogout = async () => { - try { - await logout(); - localStorage.removeItem("user_id"); - localStorage.removeItem("user_name"); - setUsername(null); - navigate("/login"); - } catch (err) { - console.error(err); - } - }; - - // Close dropdown on click outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setDropdownOpen(false); - } - }; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); + const toggleMenu = () => setMenuOpen(!menuOpen); return (
- {/* Logo */} + {/* Левый блок — логотип / название */}
NyanimeDB
- {/* Navigation (desktop) */} + {/* Центр — ссылки на разделы (desktop) */} - {/* Profile / login */} -
+ {/* Правый блок — профиль */} +
{username ? ( -
- - {dropdownOpen && ( -
- setDropdownOpen(false)}>Profile - -
- )} -
+ + {username} + ) : ( - Login + + Login + )}
- {/* Mobile burger */} + {/* Бургер для мобильного */}
-
- {/* Mobile menu */} + {/* Мобильное меню */} {menuOpen && (
diff --git a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx index fc652af..98fa868 100644 --- a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx +++ b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx @@ -27,7 +27,7 @@ export function TitleStatusControls({ titleId }: { titleId: number }) { const [currentStatus, setCurrentStatus] = useState(null); const [loading, setLoading] = useState(false); - const userIdStr = localStorage.getItem("user_id"); + const userIdStr = localStorage.getItem("userId"); const userId = userIdStr ? Number(userIdStr) : null; // --- Load initial status --- diff --git a/modules/frontend/src/components/cards/TitleCardHorizontal.tsx b/modules/frontend/src/components/cards/TitleCardHorizontal.tsx index 4a020b7..b848702 100644 --- a/modules/frontend/src/components/cards/TitleCardHorizontal.tsx +++ b/modules/frontend/src/components/cards/TitleCardHorizontal.tsx @@ -10,7 +10,7 @@ export function TitleCardHorizontal({ title }: { title: Title }) { borderRadius: 8 }}> {title.poster?.image_path && ( - + )}

{title.title_names["en"]}

diff --git a/modules/frontend/src/components/cards/TitleCardSquare.tsx b/modules/frontend/src/components/cards/TitleCardSquare.tsx index 6a7a071..0bcb49d 100644 --- a/modules/frontend/src/components/cards/TitleCardSquare.tsx +++ b/modules/frontend/src/components/cards/TitleCardSquare.tsx @@ -10,7 +10,7 @@ export function TitleCardSquare({ title }: { title: Title }) { textAlign: "center" }}> {title.poster?.image_path && ( - + )}

{title.title_names["en"]}

diff --git a/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx b/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx index aec30d2..ad7d5df 100644 --- a/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx +++ b/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx @@ -10,7 +10,7 @@ export function UserTitleCardHorizontal({ title }: { title: UserTitle }) { borderRadius: 8 }}> {title.title?.poster?.image_path && ( - + )}

{title.title?.title_names["en"]}

diff --git a/modules/frontend/src/components/cards/UserTitleCardSquare.tsx b/modules/frontend/src/components/cards/UserTitleCardSquare.tsx index a9424f6..edcf1d5 100644 --- a/modules/frontend/src/components/cards/UserTitleCardSquare.tsx +++ b/modules/frontend/src/components/cards/UserTitleCardSquare.tsx @@ -10,7 +10,7 @@ export function UserTitleCardSquare({ title }: { title: UserTitle }) { textAlign: "center" }}> {title.title?.poster?.image_path && ( - + )}

{title.title?.title_names["en"]}

diff --git a/modules/frontend/src/pages/LoginPage/LoginPage.tsx b/modules/frontend/src/pages/LoginPage/LoginPage.tsx index 55ae730..928766e 100644 --- a/modules/frontend/src/pages/LoginPage/LoginPage.tsx +++ b/modules/frontend/src/pages/LoginPage/LoginPage.tsx @@ -1,10 +1,10 @@ import React, { useState } from "react"; -import { postSignIn, postSignUp } from "../../auth"; +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); + const [isLogin, setIsLogin] = useState(true); // true = login, false = signup const [nickname, setNickname] = useState(""); const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); @@ -17,32 +17,27 @@ export const LoginPage: React.FC = () => { try { if (isLogin) { - const res = await postSignIn({ body: { nickname, pass: password } }); - if (res.data?.user_id && res.data?.user_name) { - localStorage.setItem("user_id", res.data.user_id.toString()); - localStorage.setItem("user_name", res.data.user_name); - window.dispatchEvent(new Event("storage")); - navigate("/profile"); + 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 { - // Sign up - const res = await postSignUp({ body: { nickname, pass: password } }); - if (res.data?.user_id) { - // Auto-login after sign-up - const loginRes = await postSignIn({ body: { nickname, pass: password } }); - if (loginRes.data?.user_id && loginRes.data?.user_name) { - localStorage.setItem("user_id", loginRes.data.user_id.toString()); - localStorage.setItem("user_name", loginRes.data.user_name); - window.dispatchEvent(new Event("storage")); - navigate("/profile"); - } + // 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); @@ -52,26 +47,39 @@ export const LoginPage: React.FC = () => { return (
-

{isLogin ? "Login" : "Sign Up"}

+

+ {isLogin ? "Login" : "Sign Up"} +

+ {error &&
{error}
}
- setNickname(e.target.value)} - placeholder="Nickname" - className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" - required - /> - setPassword(e.target.value)} - placeholder="Password" - className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" - required - /> +
+ + 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 + /> +
+ +
+ + 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 + /> +
+ + <> + Don't have an account?{" "} + + ) : ( - <>Already have an account? + <> + Already have an account?{" "} + + )}
diff --git a/modules/frontend/src/pages/SettingsPage/SettingsPage.tsx b/modules/frontend/src/pages/SettingsPage/SettingsPage.tsx deleted file mode 100644 index 16c7e9e..0000000 --- a/modules/frontend/src/pages/SettingsPage/SettingsPage.tsx +++ /dev/null @@ -1,154 +0,0 @@ -// import React, { useEffect, useState } from "react"; -// import { updateUser, getUsersId } from "../../api"; -// import { useNavigate } from "react-router-dom"; - -// export const SettingsPage: React.FC = () => { -// const navigate = useNavigate(); - -// const userId = Number(localStorage.getItem("user_id")); -// const initialNickname = localStorage.getItem("user_name") || ""; -// const [mail, setMail] = useState(""); -// const [nickname, setNickname] = useState(initialNickname); -// const [dispName, setDispName] = useState(""); -// const [userDesc, setUserDesc] = useState(""); -// const [avatarId, setAvatarId] = useState(null); - -// const [loading, setLoading] = useState(false); -// const [success, setSuccess] = useState(null); -// const [error, setError] = useState(null); - -// useEffect(() => { -// const fetch = async () => { -// const res = await getUsersId({ -// path: { user_id: String(userId) }, -// }); - -// setProfile(res.data); -// }; - -// fetch(); -// }, [userId]); - -// const saveSettings = async (e: React.FormEvent) => { -// e.preventDefault(); -// setLoading(true); -// setSuccess(null); -// setError(null); - -// try { -// const res = await updateUser({ -// path: { user_id: userId }, -// body: { -// ...(mail ? { mail } : {}), -// ...(nickname ? { nickname } : {}), -// ...(dispName ? { disp_name: dispName } : {}), -// ...(userDesc ? { user_desc: userDesc } : {}), -// ...(avatarId !== undefined ? { avatar_id: avatarId } : {}), -// } -// }); - -// // Обновляем локальное отображение username -// if (nickname) { -// localStorage.setItem("user_name", nickname); -// window.dispatchEvent(new Event("storage")); // чтобы Header обновился -// } - -// setSuccess("Settings updated!"); -// setTimeout(() => navigate("/profile"), 800); - -// } catch (err: any) { -// console.error(err); -// setError(err?.message || "Failed to update settings"); -// } finally { -// setLoading(false); -// } -// }; - -// return ( -//
-//

User Settings

- -// {success &&
{success}
} -// {error &&
{error}
} - -// -// {/* Email */} -//
-// -// setMail(e.target.value)} -// placeholder="example@mail.com" -// className="w-full px-4 py-2 border rounded-lg" -// /> -//
- -// {/* Nickname */} -//
-// -// setNickname(e.target.value)} -// className="w-full px-4 py-2 border rounded-lg" -// /> -//
- -// {/* Display name */} -//
-// -// setDispName(e.target.value)} -// placeholder="Shown name" -// className="w-full px-4 py-2 border rounded-lg" -// /> -//
- -// {/* Bio */} -//
-// -//