From 87878f15a3729235d0a6affccd0161c8abef960c Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Thu, 19 Jun 2025 18:37:19 +0300 Subject: [PATCH] feat: service/controller prototype --- backend/docs/docs.go | 47 ++++++++++++++++++++++- backend/docs/swagger.json | 47 ++++++++++++++++++++++- backend/docs/swagger.yaml | 31 ++++++++++++++- backend/internal/controllers/auth.go | 23 +++++++++++- backend/internal/controllers/service.go | 2 +- backend/internal/database/query.sql.go | 6 +-- backend/internal/errors/auth.go | 9 +++++ backend/internal/errors/general.go | 11 ++++++ backend/internal/models/auth.go | 24 ++++++++++++ backend/internal/services/auth.go | 50 +++++++++++++++++++++++++ backend/internal/utils/db.go | 18 +++++++++ sqlc/query.sql | 2 +- 12 files changed, 258 insertions(+), 12 deletions(-) create mode 100644 backend/internal/errors/auth.go create mode 100644 backend/internal/errors/general.go create mode 100644 backend/internal/models/auth.go create mode 100644 backend/internal/services/auth.go create mode 100644 backend/internal/utils/db.go diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 50d5999..42b259c 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -50,7 +50,25 @@ const docTemplate = `{ "Auth" ], "summary": "Acquire tokens via login credentials (and 2FA code if needed)", - "responses": {} + "parameters": [ + { + "description": "desc", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "desc", + "schema": { + "$ref": "#/definitions/models.LoginResponse" + } + } + } } }, "/auth/passwordResetBegin": { @@ -230,7 +248,7 @@ const docTemplate = `{ "summary": "Get health status", "responses": { "200": { - "description": "desc", + "description": "Says whether it's healthy or not", "schema": { "$ref": "#/definitions/controllers.HealthStatus" } @@ -247,6 +265,31 @@ const docTemplate = `{ "type": "boolean" } } + }, + "models.LoginRequest": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "totp": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.LoginResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 4ef2237..3679bc5 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -46,7 +46,25 @@ "Auth" ], "summary": "Acquire tokens via login credentials (and 2FA code if needed)", - "responses": {} + "parameters": [ + { + "description": "desc", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "desc", + "schema": { + "$ref": "#/definitions/models.LoginResponse" + } + } + } } }, "/auth/passwordResetBegin": { @@ -226,7 +244,7 @@ "summary": "Get health status", "responses": { "200": { - "description": "desc", + "description": "Says whether it's healthy or not", "schema": { "$ref": "#/definitions/controllers.HealthStatus" } @@ -243,6 +261,31 @@ "type": "boolean" } } + }, + "models.LoginRequest": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "totp": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.LoginResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 8774380..e66c72b 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -5,6 +5,22 @@ definitions: healthy: type: boolean type: object + models.LoginRequest: + properties: + password: + type: string + totp: + type: string + username: + type: string + type: object + models.LoginResponse: + properties: + access_token: + type: string + refresh_token: + type: string + type: object info: contact: {} description: Easy and feature-rich wishlist. @@ -29,9 +45,20 @@ paths: post: consumes: - application/json + parameters: + - description: desc + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.LoginRequest' produces: - application/json - responses: {} + responses: + "200": + description: desc + schema: + $ref: '#/definitions/models.LoginResponse' summary: Acquire tokens via login credentials (and 2FA code if needed) tags: - Auth @@ -148,7 +175,7 @@ paths: - application/json responses: "200": - description: desc + description: Says whether it's healthy or not schema: $ref: '#/definitions/controllers.HealthStatus' summary: Get health status diff --git a/backend/internal/controllers/auth.go b/backend/internal/controllers/auth.go index 2f9b41f..fb11f20 100644 --- a/backend/internal/controllers/auth.go +++ b/backend/internal/controllers/auth.go @@ -1,6 +1,8 @@ package controllers import ( + "easywish/internal/models" + "easywish/internal/services" "net/http" "github.com/gin-gonic/gin" @@ -28,9 +30,28 @@ func RegistrationComplete(c *gin.Context) { // @Tags Auth // @Accept json // @Produce json +// @Param request body models.LoginRequest true "desc" +// @Success 200 {object} models.LoginResponse "desc" // @Router /auth/login [post] func Login(c *gin.Context) { - c.Status(http.StatusNotImplemented) + + var request models.LoginRequest + + if err := c.ShouldBindJSON(&request); err != nil { + c.Status(http.StatusBadRequest) + return + } + + auths := services.NewAuthService() + + result, err := auths.Login(request) + + if err != nil { + c.Status(http.StatusTeapot) + return + } + + c.JSON(http.StatusFound, result) } // @Summary Receive new tokens via refresh token diff --git a/backend/internal/controllers/service.go b/backend/internal/controllers/service.go index 396ce1d..698a3c5 100644 --- a/backend/internal/controllers/service.go +++ b/backend/internal/controllers/service.go @@ -15,7 +15,7 @@ type HealthStatus struct { // @Tags Service // @Accept json // @Produce json -// @Success 200 {object} HealthStatus "desc" +// @Success 200 {object} HealthStatus "Says whether it's healthy or not" // @Router /service/health [get] func HealthCheck(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"healthy": true}) diff --git a/backend/internal/database/query.sql.go b/backend/internal/database/query.sql.go index 0edf659..953ac5f 100644 --- a/backend/internal/database/query.sql.go +++ b/backend/internal/database/query.sql.go @@ -546,12 +546,12 @@ WHERE users.verified IS TRUE AND -- Verified users.deleted IS FALSE AND -- Not deleted banned.user_id IS NULL AND -- Not banned - linfo.password_hash = crypt($2, linfo.password_hash) + linfo.password_hash = crypt($2::text, linfo.password_hash) ` type GetUserByLoginCredentialsParams struct { Username string - Crypt string + Password string } type GetUserByLoginCredentialsRow struct { @@ -562,7 +562,7 @@ type GetUserByLoginCredentialsRow struct { } func (q *Queries) GetUserByLoginCredentials(ctx context.Context, arg GetUserByLoginCredentialsParams) (GetUserByLoginCredentialsRow, error) { - row := q.db.QueryRow(ctx, getUserByLoginCredentials, arg.Username, arg.Crypt) + row := q.db.QueryRow(ctx, getUserByLoginCredentials, arg.Username, arg.Password) var i GetUserByLoginCredentialsRow err := row.Scan( &i.ID, diff --git a/backend/internal/errors/auth.go b/backend/internal/errors/auth.go new file mode 100644 index 0000000..d7f6c90 --- /dev/null +++ b/backend/internal/errors/auth.go @@ -0,0 +1,9 @@ +package errors + +import ( + "errors" +) + +var ( + ErrUnauthorized = errors.New("User is not authorized") +) diff --git a/backend/internal/errors/general.go b/backend/internal/errors/general.go new file mode 100644 index 0000000..cbb6213 --- /dev/null +++ b/backend/internal/errors/general.go @@ -0,0 +1,11 @@ +package errors + +import ( + "errors" +) + +var ( + ErrNotImplemented = errors.New("Feature is not implemented") + ErrBadRequest = errors.New("Bad request") +) + diff --git a/backend/internal/models/auth.go b/backend/internal/models/auth.go new file mode 100644 index 0000000..97fb8df --- /dev/null +++ b/backend/internal/models/auth.go @@ -0,0 +1,24 @@ +package models + +type Tokens struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` + TOTP *string `json:"totp"` +} + +type LoginResponse struct { + Tokens +} + +type RefreshRequest struct { + RefreshToken string `json:"refresh_token"` +} + +type RefreshResponse struct { + Tokens +} diff --git a/backend/internal/services/auth.go b/backend/internal/services/auth.go new file mode 100644 index 0000000..1b9636c --- /dev/null +++ b/backend/internal/services/auth.go @@ -0,0 +1,50 @@ +package services + +import ( + "easywish/internal/database" + "easywish/internal/errors" + errs "easywish/internal/errors" + "easywish/internal/models" + "easywish/internal/utils" +) + +type AuthService interface { + Login(request models.LoginRequest) (*models.LoginResponse, error) + Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) +} + +type authServiceImpl struct { + +} + +func NewAuthService() AuthService { + return &authServiceImpl{} +} + +func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginResponse, error) { + conn, ctx, err := utils.GetDbConn() + if err != nil { + return nil, err + } + defer conn.Close(ctx) + + queries := database.New(conn) + + user, err := queries.GetUserByLoginCredentials(ctx, database.GetUserByLoginCredentialsParams{ + Username: request.Username, + Password: request.Password, + }) + + if err != nil { + return nil, errs.ErrUnauthorized + } + + accessToken, refreshToken, err := utils.GenerateTokens(user.Username) + + return &models.LoginResponse{Tokens: models.Tokens{AccessToken: accessToken, RefreshToken: refreshToken}}, nil +} + + +func (a *authServiceImpl) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) { + return nil, errors.ErrNotImplemented +} diff --git a/backend/internal/utils/db.go b/backend/internal/utils/db.go new file mode 100644 index 0000000..3c735b9 --- /dev/null +++ b/backend/internal/utils/db.go @@ -0,0 +1,18 @@ +package utils + +import ( + "context" + "easywish/config" + + "github.com/jackc/pgx/v5" +) + +func GetDbConn() (*pgx.Conn, context.Context, error) { + ctx := context.Background() + conn, err := pgx.Connect(ctx, config.GetConfig().DatabaseUrl) + if err != nil { + return nil, nil, err + } + + return conn, ctx, nil +} diff --git a/sqlc/query.sql b/sqlc/query.sql index d8f7d0f..87fb266 100644 --- a/sqlc/query.sql +++ b/sqlc/query.sql @@ -46,7 +46,7 @@ WHERE users.verified IS TRUE AND -- Verified users.deleted IS FALSE AND -- Not deleted banned.user_id IS NULL AND -- Not banned - linfo.password_hash = crypt($2, linfo.password_hash); -- Password hash matches + linfo.password_hash = crypt(@password::text, linfo.password_hash); -- Password hash matches --: }}}