refactor: updated swagger;
feat: helper function in errors for checking postgres error types; feat: sql query method for finding users by their email; feat: registration begin/complete with checking existing username/email; refactor: error handling in controller
This commit is contained in:
@@ -11,7 +11,7 @@ const docTemplate = `{
|
|||||||
"title": "{{.Title}}",
|
"title": "{{.Title}}",
|
||||||
"contact": {},
|
"contact": {},
|
||||||
"license": {
|
"license": {
|
||||||
"name": "GPL 3.0"
|
"name": "GPL-3.0"
|
||||||
},
|
},
|
||||||
"version": "{{.Version}}"
|
"version": "{{.Version}}"
|
||||||
},
|
},
|
||||||
@@ -139,7 +139,11 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {}
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/registrationComplete": {
|
"/auth/registrationComplete": {
|
||||||
@@ -154,7 +158,25 @@ const docTemplate = `{
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Confirm with code, finish creating the account",
|
"summary": "Confirm with code, finish creating the account",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "desc",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationCompleteRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "desc",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationCompleteResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/profile": {
|
"/profile": {
|
||||||
@@ -305,13 +327,16 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"maxLength": 100
|
||||||
},
|
},
|
||||||
"totp": {
|
"totp": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"maxLength": 20,
|
||||||
|
"minLength": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -346,6 +371,46 @@ const docTemplate = `{
|
|||||||
"minLength": 3
|
"minLength": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"models.RegistrationCompleteRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"username",
|
||||||
|
"verification_code"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"avatar_url": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"birthday": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 75
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 20,
|
||||||
|
"minLength": 3
|
||||||
|
},
|
||||||
|
"verification_code": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RegistrationCompleteResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"title": "Easywish client API",
|
"title": "Easywish client API",
|
||||||
"contact": {},
|
"contact": {},
|
||||||
"license": {
|
"license": {
|
||||||
"name": "GPL 3.0"
|
"name": "GPL-3.0"
|
||||||
},
|
},
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
},
|
},
|
||||||
@@ -135,7 +135,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {}
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/registrationComplete": {
|
"/auth/registrationComplete": {
|
||||||
@@ -150,7 +154,25 @@
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Confirm with code, finish creating the account",
|
"summary": "Confirm with code, finish creating the account",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "desc",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationCompleteRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "desc",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationCompleteResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/profile": {
|
"/profile": {
|
||||||
@@ -301,13 +323,16 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"maxLength": 100
|
||||||
},
|
},
|
||||||
"totp": {
|
"totp": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"maxLength": 20,
|
||||||
|
"minLength": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -342,6 +367,46 @@
|
|||||||
"minLength": 3
|
"minLength": 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"models.RegistrationCompleteRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"username",
|
||||||
|
"verification_code"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"avatar_url": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"birthday": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 75
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 20,
|
||||||
|
"minLength": 3
|
||||||
|
},
|
||||||
|
"verification_code": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RegistrationCompleteResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ definitions:
|
|||||||
models.LoginRequest:
|
models.LoginRequest:
|
||||||
properties:
|
properties:
|
||||||
password:
|
password:
|
||||||
|
maxLength: 100
|
||||||
type: string
|
type: string
|
||||||
totp:
|
totp:
|
||||||
type: string
|
type: string
|
||||||
username:
|
username:
|
||||||
|
maxLength: 20
|
||||||
|
minLength: 3
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- password
|
- password
|
||||||
@@ -39,11 +42,39 @@ definitions:
|
|||||||
- password
|
- password
|
||||||
- username
|
- username
|
||||||
type: object
|
type: object
|
||||||
|
models.RegistrationCompleteRequest:
|
||||||
|
properties:
|
||||||
|
avatar_url:
|
||||||
|
maxLength: 255
|
||||||
|
type: string
|
||||||
|
birthday:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
maxLength: 75
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
maxLength: 20
|
||||||
|
minLength: 3
|
||||||
|
type: string
|
||||||
|
verification_code:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- username
|
||||||
|
- verification_code
|
||||||
|
type: object
|
||||||
|
models.RegistrationCompleteResponse:
|
||||||
|
properties:
|
||||||
|
access_token:
|
||||||
|
type: string
|
||||||
|
refresh_token:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
info:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
description: Easy and feature-rich wishlist.
|
description: Easy and feature-rich wishlist.
|
||||||
license:
|
license:
|
||||||
name: GPL 3.0
|
name: GPL-3.0
|
||||||
title: Easywish client API
|
title: Easywish client API
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
paths:
|
paths:
|
||||||
@@ -124,7 +155,9 @@ paths:
|
|||||||
$ref: '#/definitions/models.RegistrationBeginRequest'
|
$ref: '#/definitions/models.RegistrationBeginRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: desc
|
||||||
summary: Register an account
|
summary: Register an account
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
@@ -132,9 +165,20 @@ paths:
|
|||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: desc
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.RegistrationCompleteRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: desc
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.RegistrationCompleteResponse'
|
||||||
summary: Confirm with code, finish creating the account
|
summary: Confirm with code, finish creating the account
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
|
|||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
|||||||
@@ -18,11 +18,13 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errs "easywish/internal/errors"
|
||||||
"easywish/internal/middleware"
|
"easywish/internal/middleware"
|
||||||
"easywish/internal/models"
|
"easywish/internal/models"
|
||||||
"easywish/internal/services"
|
"easywish/internal/services"
|
||||||
"easywish/internal/utils"
|
"easywish/internal/utils"
|
||||||
"easywish/internal/utils/enums"
|
"easywish/internal/utils/enums"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -96,6 +98,8 @@ func (a *authControllerImpl) Refresh(c *gin.Context) {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param request body models.RegistrationBeginRequest true "desc"
|
// @Param request body models.RegistrationBeginRequest true "desc"
|
||||||
|
// @Success 200 "Account is created and awaiting verification"
|
||||||
|
// @Success 409 "Username or email is already taken"
|
||||||
// @Router /auth/registrationBegin [post]
|
// @Router /auth/registrationBegin [post]
|
||||||
func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
|
func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
|
||||||
|
|
||||||
@@ -108,11 +112,15 @@ func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
|
|||||||
_, err := a.authService.RegistrationBegin(request.Body)
|
_, err := a.authService.RegistrationBegin(request.Body)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, err.Error())
|
if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) {
|
||||||
|
c.Status(http.StatusConflict)
|
||||||
|
} else {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Status(http.StatusAccepted)
|
c.Status(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,16 +129,37 @@ func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
|
|||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
// @Param request body models.RegistrationCompleteRequest true "desc"
|
||||||
|
// @Success 200 {object} models.RegistrationCompleteResponse "desc"
|
||||||
// @Router /auth/registrationComplete [post]
|
// @Router /auth/registrationComplete [post]
|
||||||
func (a *authControllerImpl) RegistrationComplete(c *gin.Context) {
|
func (a *authControllerImpl) RegistrationComplete(c *gin.Context) {
|
||||||
c.Status(http.StatusNotImplemented)
|
request, ok := utils.GetRequest[models.RegistrationCompleteRequest](c)
|
||||||
|
if !ok {
|
||||||
|
c.Status(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := a.authService.RegistrationComplete(request.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errs.ErrForbidden) {
|
||||||
|
c.Status(http.StatusForbidden)
|
||||||
|
} else if errors.Is(err, errs.ErrUnauthorized) {
|
||||||
|
c.Status(http.StatusUnauthorized)
|
||||||
|
} else {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *authControllerImpl) RegisterRoutes(group *gin.RouterGroup) {
|
func (a *authControllerImpl) RegisterRoutes(group *gin.RouterGroup) {
|
||||||
group.POST("/registrationBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.RegistrationBegin)
|
group.POST("/registrationBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.RegistrationBegin)
|
||||||
group.POST("/registrationComplete", a.RegistrationComplete)
|
group.POST("/registrationComplete", middleware.RequestMiddleware[models.RegistrationCompleteRequest](enums.GuestRole), a.RegistrationComplete)
|
||||||
group.POST("/login", a.Login)
|
group.POST("/login", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.Login)
|
||||||
group.POST("/refresh", a.Refresh)
|
group.POST("/refresh", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.UserRole), a.Refresh)
|
||||||
group.POST("/passwordResetBegin", a.PasswordResetBegin)
|
group.POST("/passwordResetBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.PasswordResetBegin)
|
||||||
group.POST("/passwordResetComplete", a.PasswordResetComplete)
|
group.POST("/passwordResetComplete", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.PasswordResetComplete)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -531,6 +531,25 @@ func (q *Queries) GetUserBansByUsername(ctx context.Context, username string) ([
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||||
|
SELECT users.id, users.username, users.verified, users.registration_date, users.deleted FROM users
|
||||||
|
JOIN login_informations linfo ON linfo.user_id = users.id
|
||||||
|
WHERE linfo.email = $1::text
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getUserByEmail, email)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.Verified,
|
||||||
|
&i.RegistrationDate,
|
||||||
|
&i.Deleted,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getUserByLoginCredentials = `-- name: GetUserByLoginCredentials :one
|
const getUserByLoginCredentials = `-- name: GetUserByLoginCredentials :one
|
||||||
SELECT
|
SELECT
|
||||||
users.id,
|
users.id,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
ErrUnauthorized = errors.New("User is not authorized")
|
ErrUnauthorized = errors.New("User is not authorized")
|
||||||
ErrUsernameTaken = errors.New("Provided username is already in use")
|
ErrUsernameTaken = errors.New("Provided username is already in use")
|
||||||
|
ErrEmailTaken = errors.New("Provided email is already in use")
|
||||||
ErrUserNotFound = errors.New("User was not found")
|
ErrUserNotFound = errors.New("User was not found")
|
||||||
ErrInvalidCredentials = errors.New("Invalid username, password or TOTP code")
|
ErrInvalidCredentials = errors.New("Invalid username, password or TOTP code")
|
||||||
ErrInvalidToken = errors.New("Token is invalid or expired")
|
ErrInvalidToken = errors.New("Token is invalid or expired")
|
||||||
|
|||||||
34
backend/internal/errors/postgres.go
Normal file
34
backend/internal/errors/postgres.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
|
//
|
||||||
|
// This file is part of Easywish
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||||
|
// the GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsPgErr(err error, code string) bool {
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
if errors.As(err, &pgErr) {
|
||||||
|
if pgErr.Code == code {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"easywish/internal/utils"
|
"easywish/internal/utils"
|
||||||
"easywish/internal/utils/enums"
|
"easywish/internal/utils/enums"
|
||||||
|
|
||||||
|
"github.com/jackc/pgerrcode"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -55,11 +56,40 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if user, err = db.TXQueries.CreateUser(db.CTX, request.Username); err != nil {
|
if user, err = db.TXQueries.CreateUser(db.CTX, request.Username); err != nil {
|
||||||
|
|
||||||
|
if errs.IsPgErr(err, pgerrcode.UniqueViolation) {
|
||||||
|
a.log.Warn(
|
||||||
|
"Attempted registration for a taken username",
|
||||||
|
zap.String("username", request.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
return false, errs.ErrUsernameTaken
|
||||||
|
}
|
||||||
|
|
||||||
a.log.Error("Failed to add user to database", zap.Error(err))
|
a.log.Error("Failed to add user to database", zap.Error(err))
|
||||||
return false, errs.ErrServerError
|
return false, errs.ErrServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
a.log.Info("Registraion of a new user", zap.String("username", user.Username), zap.Int64("id", user.ID))
|
if _, err := db.TXQueries.GetUserByEmail(db.CTX, *request.Email); err == nil {
|
||||||
|
a.log.Warn(
|
||||||
|
"Attempted registration for a taken email",
|
||||||
|
zap.String("email", *request.Email))
|
||||||
|
return false, errs.ErrEmailTaken
|
||||||
|
|
||||||
|
} else if !errs.IsPgErr(err, pgerrcode.NoData) {
|
||||||
|
a.log.Error(
|
||||||
|
"Failed to check if email is not taken",
|
||||||
|
zap.String("email", *request.Email),
|
||||||
|
zap.Error(err))
|
||||||
|
return false, errs.ErrServerError
|
||||||
|
|
||||||
|
} else {
|
||||||
|
a.log.Debug("Verified that email is not taken", zap.String("email", *request.Email))
|
||||||
|
}
|
||||||
|
|
||||||
|
a.log.Info(
|
||||||
|
"Registraion of a new user",
|
||||||
|
zap.String("username", user.Username),
|
||||||
|
zap.Int64("id", user.ID))
|
||||||
|
|
||||||
if _, err = db.TXQueries.CreateLoginInformation(db.CTX, database.CreateLoginInformationParams{
|
if _, err = db.TXQueries.CreateLoginInformation(db.CTX, database.CreateLoginInformationParams{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
@@ -109,11 +139,19 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
|
|||||||
user, err = db.TXQueries.GetUserByUsername(db.CTX, request.Username)
|
user, err = db.TXQueries.GetUserByUsername(db.CTX, request.Username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errs.IsPgErr(err, pgerrcode.NoData) {
|
||||||
|
a.log.Warn(
|
||||||
|
"Could not find user attempting to complete registration with given username",
|
||||||
|
zap.String("username", request.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
a.log.Error(
|
a.log.Error(
|
||||||
"Failed to find user attempting to complete registration",
|
"Failed to get user",
|
||||||
zap.String("username", request.Username),
|
zap.String("username", request.Username),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
return nil, errs.ErrUserNotFound
|
return nil, errs.ErrServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmationCode, err = db.TXQueries.GetConfirmationCodeByCode(db.CTX, database.GetConfirmationCodeByCodeParams{
|
confirmationCode, err = db.TXQueries.GetConfirmationCodeByCode(db.CTX, database.GetConfirmationCodeByCodeParams{
|
||||||
@@ -123,11 +161,20 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.log.Warn(
|
if errs.IsPgErr(err, pgerrcode.NoData) {
|
||||||
"User supplied wrong confirmation code for completing registration",
|
a.log.Warn(
|
||||||
|
"User supplied unexistent confirmation code for completing registration",
|
||||||
|
zap.String("username", user.Username),
|
||||||
|
zap.String("code", request.VerificationCode),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrForbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
a.log.Error(
|
||||||
|
"Failed to acquire specified registration code",
|
||||||
zap.String("username", user.Username),
|
zap.String("username", user.Username),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
return nil, errs.ErrForbidden
|
return nil, errs.ErrServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{
|
err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{
|
||||||
@@ -140,8 +187,7 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
|
|||||||
"Failed to update the user's registration code used state",
|
"Failed to update the user's registration code used state",
|
||||||
zap.String("username", user.Username),
|
zap.String("username", user.Username),
|
||||||
zap.Int64("confirmation_code_id", confirmationCode.ID),
|
zap.Int64("confirmation_code_id", confirmationCode.ID),
|
||||||
zap.Error(err),
|
zap.Error(err))
|
||||||
)
|
|
||||||
return nil, errs.ErrServerError
|
return nil, errs.ErrServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +225,7 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
|
|||||||
return nil, errs.ErrServerError
|
return nil, errs.ErrServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: session info
|
||||||
session, err = db.TXQueries.CreateSession(db.CTX, database.CreateSessionParams{
|
session, err = db.TXQueries.CreateSession(db.CTX, database.CreateSessionParams{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
Name: utils.NewPointer("First device"),
|
Name: utils.NewPointer("First device"),
|
||||||
@@ -206,6 +253,10 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
|
|||||||
|
|
||||||
helper.Commit()
|
helper.Commit()
|
||||||
|
|
||||||
|
a.log.Info(
|
||||||
|
"User verified registration",
|
||||||
|
zap.String("username", request.Username))
|
||||||
|
|
||||||
response := models.RegistrationCompleteResponse{Tokens: models.Tokens{
|
response := models.RegistrationCompleteResponse{Tokens: models.Tokens{
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ services:
|
|||||||
POSTGRES_URL: ${POSTGRES_URL}
|
POSTGRES_URL: ${POSTGRES_URL}
|
||||||
REDIS_URL: ${REDIS_URL}
|
REDIS_URL: ${REDIS_URL}
|
||||||
MINIO_URL: ${MINIO_URL}
|
MINIO_URL: ${MINIO_URL}
|
||||||
|
ENVIRONMENT: ${ENVIRONMENT}
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ WHERE id = $1;
|
|||||||
SELECT * FROM users
|
SELECT * FROM users
|
||||||
WHERE username = $1;
|
WHERE username = $1;
|
||||||
|
|
||||||
|
;-- name: GetUserByEmail :one
|
||||||
|
SELECT users.* FROM users
|
||||||
|
JOIN login_informations linfo ON linfo.user_id = users.id
|
||||||
|
WHERE linfo.email = @email::text;
|
||||||
|
|
||||||
;-- name: GetUserByLoginCredentials :one
|
;-- name: GetUserByLoginCredentials :one
|
||||||
SELECT
|
SELECT
|
||||||
users.id,
|
users.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user