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:
2025-07-03 04:33:25 +03:00
parent d08db300fc
commit 0a51727af8
12 changed files with 348 additions and 31 deletions

View File

@@ -18,11 +18,13 @@
package controllers
import (
errs "easywish/internal/errors"
"easywish/internal/middleware"
"easywish/internal/models"
"easywish/internal/services"
"easywish/internal/utils"
"easywish/internal/utils/enums"
"errors"
"net/http"
"github.com/gin-gonic/gin"
@@ -96,6 +98,8 @@ func (a *authControllerImpl) Refresh(c *gin.Context) {
// @Accept json
// @Produce json
// @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]
func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
@@ -108,11 +112,15 @@ func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
_, err := a.authService.RegistrationBegin(request.Body)
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
}
c.Status(http.StatusAccepted)
c.Status(http.StatusOK)
return
}
@@ -121,16 +129,37 @@ func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body models.RegistrationCompleteRequest true "desc"
// @Success 200 {object} models.RegistrationCompleteResponse "desc"
// @Router /auth/registrationComplete [post]
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) {
group.POST("/registrationBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.RegistrationBegin)
group.POST("/registrationComplete", a.RegistrationComplete)
group.POST("/login", a.Login)
group.POST("/refresh", a.Refresh)
group.POST("/passwordResetBegin", a.PasswordResetBegin)
group.POST("/passwordResetComplete", a.PasswordResetComplete)
group.POST("/registrationComplete", middleware.RequestMiddleware[models.RegistrationCompleteRequest](enums.GuestRole), a.RegistrationComplete)
group.POST("/login", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.Login)
group.POST("/refresh", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.UserRole), a.Refresh)
group.POST("/passwordResetBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.PasswordResetBegin)
group.POST("/passwordResetComplete", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.PasswordResetComplete)
}

View File

@@ -531,6 +531,25 @@ func (q *Queries) GetUserBansByUsername(ctx context.Context, username string) ([
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
SELECT
users.id,

View File

@@ -24,6 +24,7 @@ import (
var (
ErrUnauthorized = errors.New("User is not authorized")
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")
ErrInvalidCredentials = errors.New("Invalid username, password or TOTP code")
ErrInvalidToken = errors.New("Token is invalid or expired")

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

View File

@@ -24,6 +24,7 @@ import (
"easywish/internal/utils"
"easywish/internal/utils/enums"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5"
"go.uber.org/zap"
)
@@ -55,11 +56,40 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
var err error
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))
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{
UserID: user.ID,
@@ -109,11 +139,19 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
user, err = db.TXQueries.GetUserByUsername(db.CTX, request.Username)
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(
"Failed to find user attempting to complete registration",
zap.String("username", request.Username),
"Failed to get user",
zap.String("username", request.Username),
zap.Error(err))
return nil, errs.ErrUserNotFound
return nil, errs.ErrServerError
}
confirmationCode, err = db.TXQueries.GetConfirmationCodeByCode(db.CTX, database.GetConfirmationCodeByCodeParams{
@@ -123,11 +161,20 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
})
if err != nil {
a.log.Warn(
"User supplied wrong confirmation code for completing registration",
zap.String("username", user.Username),
if errs.IsPgErr(err, pgerrcode.NoData) {
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.Error(err))
return nil, errs.ErrForbidden
return nil, errs.ErrServerError
}
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",
zap.String("username", user.Username),
zap.Int64("confirmation_code_id", confirmationCode.ID),
zap.Error(err),
)
zap.Error(err))
return nil, errs.ErrServerError
}
@@ -179,6 +225,7 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
return nil, errs.ErrServerError
}
// TODO: session info
session, err = db.TXQueries.CreateSession(db.CTX, database.CreateSessionParams{
UserID: user.ID,
Name: utils.NewPointer("First device"),
@@ -206,6 +253,10 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
helper.Commit()
a.log.Info(
"User verified registration",
zap.String("username", request.Username))
response := models.RegistrationCompleteResponse{Tokens: models.Tokens{
AccessToken: accessToken,
RefreshToken: refreshToken,