feat: service/controller prototype

This commit is contained in:
2025-06-19 18:37:19 +03:00
parent 912470b6e2
commit 87878f15a3
12 changed files with 258 additions and 12 deletions

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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

View File

@@ -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

View File

@@ -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})

View File

@@ -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,

View File

@@ -0,0 +1,9 @@
package errors
import (
"errors"
)
var (
ErrUnauthorized = errors.New("User is not authorized")
)

View File

@@ -0,0 +1,11 @@
package errors
import (
"errors"
)
var (
ErrNotImplemented = errors.New("Feature is not implemented")
ErrBadRequest = errors.New("Bad request")
)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
--: }}}