Compare commits

7 Commits

Author SHA1 Message Date
be9aee7145 chore: GPL-3.0 license propagated into *.go files in backend 2025-06-24 01:37:47 +03:00
cfe60cfb8e chore: corrected misleading error descriptions, removed redundant comments 2025-06-24 01:11:49 +03:00
e5d245519a feat: preparing structures for validation features;
feat: config variables for password requirements;
feat: util function for validating passwords
2025-06-24 00:25:59 +03:00
0a00a5ee2b feat: registrationBegin method without email;
fix: missing sqlc query parameter name;
feat: util for generating security codes;
feat: enums package
2025-06-23 16:23:46 +03:00
1b55498b00 refactor: a better DI-friendy logger implementation that doesn't suck 2025-06-23 14:18:25 +03:00
ea3743cb04 fixed: error handling in commit;
refactor: exposed untransactional queries for transactional db helper again but with a clearer name this time since it still may be useful
2025-06-22 12:41:22 +03:00
613deae8e2 feat: db regular and transactional helpers to reduce boilerplate 2025-06-21 20:04:20 +03:00
31 changed files with 806 additions and 158 deletions

View File

@@ -1,7 +1,24 @@
// 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/>.
// @title Easywish client API
// @version 1.0
// @description Easy and feature-rich wishlist.
// @license.name GPL 3.0
// @license.name GPL-3.0
// @BasePath /api/
// @Schemes http
@@ -20,6 +37,7 @@ import (
"github.com/gin-gonic/gin"
"go.uber.org/fx"
"go.uber.org/zap"
"easywish/config"
docs "easywish/docs"
@@ -42,6 +60,7 @@ func main() {
fx.New(
fx.Provide(
logger.NewLogger,
logger.NewSyncLogger,
gin.Default,
),
database.Module,
@@ -49,7 +68,7 @@ func main() {
controllers.Module,
routes.Module,
fx.Invoke(func(lc fx.Lifecycle, router *gin.Engine) {
fx.Invoke(func(lc fx.Lifecycle, router *gin.Engine, syncLogger *logger.SyncLogger) {
// Swagger
docs.SwaggerInfo.Schemes = []string{"http"}
@@ -65,6 +84,7 @@ func main() {
OnStart: func(ctx context.Context) error {
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
syncLogger.Fatal("Server failed", zap.Error(err))
}
}()
return nil
@@ -72,11 +92,18 @@ func main() {
OnStop: func(ctx context.Context) error {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return server.Shutdown(shutdownCtx)
if err := server.Shutdown(shutdownCtx); err != nil {
syncLogger.Error("Server shutdown error", zap.Error(err))
}
if err := syncLogger.Close(); err != nil {
syncLogger.Error("Logger sync error", zap.Error(err))
}
return nil
},
})
}),
).Run()
}

View File

@@ -1,3 +1,20 @@
// 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 config
import (
@@ -8,30 +25,47 @@ import (
)
type Config struct {
Hostname string `mapstructure:"HOSTNAME"`
Port string `mapstructure:"PORT"`
DatabaseUrl string `mapstructure:"POSTGRES_URL"`
RedisUrl string `mapstructure:"REDIS_URL"`
MinioUrl string `mapstructure:"MINIO_URL"`
JwtAlgorithm string `mapstructure:"JWT_ALGORITHM"`
JwtSecret string `mapstructure:"JWT_SECRET"`
JwtIssuer string `mapstructure:"JWT_ISSUER"`
JwtAudience string `mapstructure:"JWT_AUDIENCE"`
JwtExpAccess int `mapstructure:"JWT_EXP_ACCESS"`
JwtExpRefresh int `mapstructure:"JWT_EXP_REFRESH"`
Environment string `mapstructure:"ENVIRONMENT"`
Hostname string `mapstructure:"HOSTNAME"`
Port string `mapstructure:"PORT"`
DatabaseUrl string `mapstructure:"POSTGRES_URL"`
RedisUrl string `mapstructure:"REDIS_URL"`
MinioUrl string `mapstructure:"MINIO_URL"`
JwtAlgorithm string `mapstructure:"JWT_ALGORITHM"`
JwtSecret string `mapstructure:"JWT_SECRET"`
JwtIssuer string `mapstructure:"JWT_ISSUER"`
JwtAudience string `mapstructure:"JWT_AUDIENCE"`
JwtExpAccess int `mapstructure:"JWT_EXP_ACCESS"`
JwtExpRefresh int `mapstructure:"JWT_EXP_REFRESH"`
PasswordCheckLength bool `mapstructure:"PASSWORD_CHECK_LENGTH"`
PasswordCheckNumbers bool `mapstructure:"PASSWORD_CHECK_NUMBERS"`
PasswordCheckCases bool `mapstructure:"PASSWORD_CHECK_CASES"`
PasswordCheckSymbols bool `mapstructure:"PASSWORD_CHECK_SYMBOLS"`
PasswordCheckLeaked bool `mapstructure:"PASSWORD_CHECK_LEAKED"`
Environment string `mapstructure:"ENVIRONMENT"`
}
func Load() (*Config, error) {
viper.SetDefault("HOSTNAME", "localhost")
viper.SetDefault("PORT", "8080")
viper.SetDefault("JWT_ALGORITHM", "HS256")
viper.SetDefault("JWT_SECRET", "default_jwt_secret_please_change")
viper.SetDefault("JWT_EXP_ACCESS", 5)
viper.SetDefault("JWT_EXP_REFRESH", 10080)
viper.SetDefault("JWT_AUDIENCE", "easywish")
viper.SetDefault("JWT_ISSUER", "easywish")
viper.SetDefault("PASSWORD_CHECK_LENGTH", true)
viper.SetDefault("PASSWORD_CHECK_NUMBERS", false)
viper.SetDefault("PASSWORD_CHECK_CASES", false)
viper.SetDefault("PASSWORD_CHECK_SYMBOLS", false)
viper.SetDefault("PASSWORD_CHECK_LEAKED", false)
viper.SetDefault("ENVIRONMENT", "production")
viper.AutomaticEnv()
@@ -39,18 +73,28 @@ func Load() (*Config, error) {
// Viper's AutomaticEnv() expects lowercase keys for unmarshalling into structs by default,
// while the environment variables and struct tags are in uppercase.
// Here's the stupidity we have to do to fix it:
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.BindEnv("HOSTNAME")
viper.BindEnv("PORT")
viper.BindEnv("POSTGRES_URL")
viper.BindEnv("REDIS_URL")
viper.BindEnv("MINIO_URL")
viper.BindEnv("JWT_ALGORITHM")
viper.BindEnv("JWT_SECRET")
viper.BindEnv("JWT_ISSUER")
viper.BindEnv("JWT_AUDIENCE")
viper.BindEnv("JWT_EXP_ACCESS")
viper.BindEnv("JWT_EXP_REFRESH")
viper.BindEnv("PASSWORD_CHECK_LENGTH")
viper.BindEnv("PASSWORD_CHECK_NUMBERS")
viper.BindEnv("PASSWORD_CHECK_CASES")
viper.BindEnv("PASSWORD_CHECK_SYMBOLS")
viper.BindEnv("PASSWORD_CHECK_LEAKED")
viper.BindEnv("ENVIRONMENT")
required := []string{

View File

@@ -299,6 +299,10 @@ const docTemplate = `{
},
"models.LoginRequest": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"password": {
"type": "string"
@@ -324,6 +328,10 @@ const docTemplate = `{
},
"models.RegistrationBeginRequest": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"email": {
"type": "string"
@@ -333,7 +341,9 @@ const docTemplate = `{
"type": "string"
},
"username": {
"type": "string"
"type": "string",
"maxLength": 20,
"minLength": 3
}
}
}

View File

@@ -295,6 +295,10 @@
},
"models.LoginRequest": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"password": {
"type": "string"
@@ -320,6 +324,10 @@
},
"models.RegistrationBeginRequest": {
"type": "object",
"required": [
"password",
"username"
],
"properties": {
"email": {
"type": "string"
@@ -329,7 +337,9 @@
"type": "string"
},
"username": {
"type": "string"
"type": "string",
"maxLength": 20,
"minLength": 3
}
}
}

View File

@@ -13,6 +13,9 @@ definitions:
type: string
username:
type: string
required:
- password
- username
type: object
models.LoginResponse:
properties:
@@ -29,7 +32,12 @@ definitions:
description: 'TODO: password checking'
type: string
username:
maxLength: 20
minLength: 3
type: string
required:
- password
- username
type: object
info:
contact: {}

View File

@@ -1,3 +1,20 @@
// 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 controllers
import (
@@ -15,4 +32,3 @@ import (
func ChangePassword(c *gin.Context) {
c.Status(http.StatusNotImplemented)
}

View File

@@ -1,3 +1,20 @@
// 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 controllers
import (

View File

@@ -1,3 +1,20 @@
// 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 controllers
import (

View File

@@ -1,3 +1,20 @@
// 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 controllers
import (

View File

@@ -1,3 +1,20 @@
// 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 controllers
import (

View File

@@ -1,3 +1,20 @@
// 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 controllers
import (

View File

@@ -1,3 +1,20 @@
// 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 database
import (

View File

@@ -0,0 +1,100 @@
// 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 database
import (
"context"
"github.com/jackc/pgx/v5"
)
type DbHelperTransaction interface {
Commit() error
Rollback() error
}
type DbHelper struct {
CTX context.Context
Queries Queries
}
type dbHelperTransactionImpl struct {
CTX context.Context
TXlessQueries Queries
TX pgx.Tx
TXQueries Queries
}
func NewDbHelper(dbContext DbContext) DbHelper {
ctx := context.Background()
queries := New(dbContext)
return DbHelper{
CTX: ctx,
Queries: *queries,
}
}
func NewDbHelperTransaction(dbContext DbContext) (DbHelperTransaction, *dbHelperTransactionImpl, error) {
ctx := context.Background()
queries := New(dbContext)
tx, err := dbContext.BeginTx(ctx)
if err != nil {
return nil, nil, err
}
txQueries := queries.WithTx(tx)
obj := &dbHelperTransactionImpl{
CTX: ctx,
TXlessQueries: *queries,
TX: tx,
TXQueries: *txQueries,
}
return obj, obj, nil
}
// Commit implements DbHelperTransaction.
func (d *dbHelperTransactionImpl) Commit() error {
errCommit := d.TX.Commit(d.CTX)
if errCommit != nil {
errRollback := d.TX.Rollback(d.CTX)
if errRollback != nil {
return errRollback
}
return errCommit
}
return nil
}
// Rollback implements DbHelperTransaction.
func (d *dbHelperTransactionImpl) Rollback() error {
err := d.TX.Rollback(d.CTX)
if err != nil {
return err
}
return nil
}

View File

@@ -12,11 +12,11 @@ type BannedUser struct {
ID int64
UserID int64
Date pgtype.Timestamp
Reason pgtype.Text
Reason *string
ExpiresAt pgtype.Timestamp
BannedBy pgtype.Text
Pardoned pgtype.Bool
PardonedBy pgtype.Text
BannedBy *string
Pardoned *bool
PardonedBy *string
}
type ConfirmationCode struct {
@@ -25,17 +25,17 @@ type ConfirmationCode struct {
CodeType int32
CodeHash string
ExpiresAt pgtype.Timestamp
Used pgtype.Bool
Deleted pgtype.Bool
Used *bool
Deleted *bool
}
type LoginInformation struct {
ID int64
UserID int64
Email pgtype.Text
Email *string
PasswordHash string
TotpEncrypted pgtype.Text
Email2faEnabled pgtype.Bool
TotpEncrypted *string
Email2faEnabled *bool
PasswordChangeDate pgtype.Timestamp
}
@@ -43,41 +43,41 @@ type Profile struct {
ID int64
UserID int64
Name string
Bio pgtype.Text
AvatarUrl pgtype.Text
Bio *string
AvatarUrl *string
Birthday pgtype.Timestamp
Color pgtype.Text
ColorGrad pgtype.Text
Color *string
ColorGrad *string
}
type ProfileSetting struct {
ID int64
ProfileID int64
HideFulfilled pgtype.Bool
HideProfileDetails pgtype.Bool
HideForUnauthenticated pgtype.Bool
HideBirthday pgtype.Bool
HideDates pgtype.Bool
Captcha pgtype.Bool
FollowersOnlyInteraction pgtype.Bool
HideFulfilled *bool
HideProfileDetails *bool
HideForUnauthenticated *bool
HideBirthday *bool
HideDates *bool
Captcha *bool
FollowersOnlyInteraction *bool
}
type Session struct {
ID int64
UserID int64
Guid pgtype.UUID
Name pgtype.Text
Platform pgtype.Text
LatestIp pgtype.Text
Name *string
Platform *string
LatestIp *string
LoginTime pgtype.Timestamp
LastSeenDate pgtype.Timestamp
Terminated pgtype.Bool
Terminated *bool
}
type User struct {
ID int64
Username string
Verified pgtype.Bool
Verified *bool
RegistrationDate pgtype.Timestamp
Deleted pgtype.Bool
Deleted *bool
}

View File

@@ -19,8 +19,8 @@ VALUES ( $1, $2, $3, $4) RETURNING id, user_id, date, reason, expires_at, banned
type CreateBannedUserParams struct {
UserID int64
ExpiresAt pgtype.Timestamp
Reason pgtype.Text
BannedBy pgtype.Text
Reason *string
BannedBy *string
}
func (q *Queries) CreateBannedUser(ctx context.Context, arg CreateBannedUserParams) (BannedUser, error) {
@@ -45,24 +45,18 @@ func (q *Queries) CreateBannedUser(ctx context.Context, arg CreateBannedUserPara
}
const createConfirmationCode = `-- name: CreateConfirmationCode :one
INSERT INTO confirmation_codes(user_id, code_type, code_hash, expires_at)
VALUES ($1, $2, crypt($3, gen_salt('bf')), $4) RETURNING id, user_id, code_type, code_hash, expires_at, used, deleted
INSERT INTO confirmation_codes(user_id, code_type, code_hash)
VALUES ($1, $2, crypt($3::text, gen_salt('bf'))) RETURNING id, user_id, code_type, code_hash, expires_at, used, deleted
`
type CreateConfirmationCodeParams struct {
UserID int64
CodeType int32
Crypt string
ExpiresAt pgtype.Timestamp
UserID int64
CodeType int32
Code string
}
func (q *Queries) CreateConfirmationCode(ctx context.Context, arg CreateConfirmationCodeParams) (ConfirmationCode, error) {
row := q.db.QueryRow(ctx, createConfirmationCode,
arg.UserID,
arg.CodeType,
arg.Crypt,
arg.ExpiresAt,
)
row := q.db.QueryRow(ctx, createConfirmationCode, arg.UserID, arg.CodeType, arg.Code)
var i ConfirmationCode
err := row.Scan(
&i.ID,
@@ -83,7 +77,7 @@ VALUES ( $1, $2, crypt($3::text, gen_salt('bf')) ) RETURNING id, user_id, email,
type CreateLoginInformationParams struct {
UserID int64
Email pgtype.Text
Email *string
Password string
}
@@ -110,11 +104,11 @@ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, user_id, name, bio, avatar_url
type CreateProfileParams struct {
UserID int64
Name string
Bio pgtype.Text
Bio *string
Birthday pgtype.Timestamp
AvatarUrl pgtype.Text
Color pgtype.Text
ColorGrad pgtype.Text
AvatarUrl *string
Color *string
ColorGrad *string
}
func (q *Queries) CreateProfile(ctx context.Context, arg CreateProfileParams) (Profile, error) {
@@ -170,9 +164,9 @@ VALUES ($1, $2, $3, $4) RETURNING id, user_id, guid, name, platform, latest_ip,
type CreateSessionParams struct {
UserID int64
Name pgtype.Text
Platform pgtype.Text
LatestIp pgtype.Text
Name *string
Platform *string
LatestIp *string
}
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
@@ -331,18 +325,18 @@ WHERE users.username = $1 AND ($2 IS FALSE OR profile_settings.hide_for_unauthen
type GetProfileByUsernameRestrictedParams struct {
Username string
Column2 pgtype.Bool
Column2 *bool
}
type GetProfileByUsernameRestrictedRow struct {
Username string
Name string
Birthday pgtype.Timestamp
Bio pgtype.Text
AvatarUrl pgtype.Text
Color pgtype.Text
ColorGrad pgtype.Text
HideProfileDetails pgtype.Bool
Bio *string
AvatarUrl *string
Color *string
ColorGrad *string
HideProfileDetails *bool
}
func (q *Queries) GetProfileByUsernameRestricted(ctx context.Context, arg GetProfileByUsernameRestrictedParams) (GetProfileByUsernameRestrictedRow, error) {
@@ -405,17 +399,17 @@ LIMIT 20 OFFSET 20 * $1
`
type GetProfilesRestrictedParams struct {
Column1 pgtype.Int4
Column2 pgtype.Bool
Column1 *int32
Column2 *bool
}
type GetProfilesRestrictedRow struct {
Username string
Name string
AvatarUrl pgtype.Text
Color pgtype.Text
ColorGrad pgtype.Text
HideProfileDetails pgtype.Bool
AvatarUrl *string
Color *string
ColorGrad *string
HideProfileDetails *bool
}
func (q *Queries) GetProfilesRestricted(ctx context.Context, arg GetProfilesRestrictedParams) ([]GetProfilesRestrictedRow, error) {
@@ -558,7 +552,7 @@ type GetUserByLoginCredentialsRow struct {
ID int64
Username string
PasswordHash string
TotpEncrypted pgtype.Text
TotpEncrypted *string
}
func (q *Queries) GetUserByLoginCredentials(ctx context.Context, arg GetUserByLoginCredentialsParams) (GetUserByLoginCredentialsRow, error) {
@@ -654,11 +648,11 @@ WHERE id = $1
type UpdateBannedUserParams struct {
ID int64
Reason pgtype.Text
Reason *string
ExpiresAt pgtype.Timestamp
BannedBy pgtype.Text
Pardoned pgtype.Bool
PardonedBy pgtype.Text
BannedBy *string
Pardoned *bool
PardonedBy *string
}
func (q *Queries) UpdateBannedUser(ctx context.Context, arg UpdateBannedUserParams) error {
@@ -681,8 +675,8 @@ WHERE id = $1
type UpdateConfirmationCodeParams struct {
ID int64
Used pgtype.Bool
Deleted pgtype.Bool
Used *bool
Deleted *bool
}
func (q *Queries) UpdateConfirmationCode(ctx context.Context, arg UpdateConfirmationCodeParams) error {
@@ -699,10 +693,10 @@ WHERE users.username = $1 AND login_informations.user_id = users.id
type UpdateLoginInformationByUsernameParams struct {
Username string
Email pgtype.Text
Email *string
Password string
TotpEncrypted pgtype.Text
Email2faEnabled pgtype.Bool
TotpEncrypted *string
Email2faEnabled *bool
PasswordChangeDate pgtype.Timestamp
}
@@ -728,11 +722,11 @@ WHERE username = $1
type UpdateProfileByUsernameParams struct {
Username string
Name string
Bio pgtype.Text
Bio *string
Birthday pgtype.Timestamp
AvatarUrl pgtype.Text
Color pgtype.Text
ColorGrad pgtype.Text
AvatarUrl *string
Color *string
ColorGrad *string
}
func (q *Queries) UpdateProfileByUsername(ctx context.Context, arg UpdateProfileByUsernameParams) error {
@@ -763,13 +757,13 @@ WHERE id = $1
type UpdateProfileSettingsParams struct {
ID int64
HideFulfilled pgtype.Bool
HideProfileDetails pgtype.Bool
HideForUnauthenticated pgtype.Bool
HideBirthday pgtype.Bool
HideDates pgtype.Bool
Captcha pgtype.Bool
FollowersOnlyInteraction pgtype.Bool
HideFulfilled *bool
HideProfileDetails *bool
HideForUnauthenticated *bool
HideBirthday *bool
HideDates *bool
Captcha *bool
FollowersOnlyInteraction *bool
}
func (q *Queries) UpdateProfileSettings(ctx context.Context, arg UpdateProfileSettingsParams) error {
@@ -794,12 +788,12 @@ WHERE id = $1
type UpdateSessionParams struct {
ID int64
Name pgtype.Text
Platform pgtype.Text
LatestIp pgtype.Text
Name *string
Platform *string
LatestIp *string
LoginTime pgtype.Timestamp
LastSeenDate pgtype.Timestamp
Terminated pgtype.Bool
Terminated *bool
}
func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) error {
@@ -823,8 +817,8 @@ WHERE id = $1
type UpdateUserParams struct {
ID int64
Verified pgtype.Bool
Deleted pgtype.Bool
Verified *bool
Deleted *bool
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
@@ -840,8 +834,8 @@ WHERE username = $1
type UpdateUserByUsernameParams struct {
Username string
Verified pgtype.Bool
Deleted pgtype.Bool
Verified *bool
Deleted *bool
}
func (q *Queries) UpdateUserByUsername(ctx context.Context, arg UpdateUserByUsernameParams) error {

View File

@@ -1,3 +1,20 @@
// 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 database
import (
@@ -9,5 +26,3 @@ var Module = fx.Module("database",
NewDbContext,
),
)

View File

@@ -1,3 +1,20 @@
// 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 (

View File

@@ -1,3 +1,20 @@
// 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 (
@@ -8,4 +25,3 @@ var (
ErrNotImplemented = errors.New("Feature is not implemented")
ErrBadRequest = errors.New("Bad request")
)

View File

@@ -1,3 +1,20 @@
// 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 logger
import (
@@ -8,40 +25,36 @@ import (
"go.uber.org/zap"
)
type Logger interface {
Get() *zap.Logger
Sync() error
}
type loggerImpl struct {
}
func NewLogger() Logger {
return &loggerImpl{}
}
var (
logger *zap.Logger
once sync.Once
instance *zap.Logger
once sync.Once
)
func (l *loggerImpl) Get() *zap.Logger {
func NewLogger() *zap.Logger {
once.Do(func() {
var err error
cfg := config.GetConfig()
var err error
if cfg.Environment == "production" {
logger, err = zap.NewProduction()
instance, err = zap.NewProduction()
} else {
logger, err = zap.NewDevelopment()
instance, err = zap.NewDevelopment()
}
if err != nil {
panic(err)
panic("failed to initialize logger: " + err.Error())
}
})
return logger
return instance
}
func (l *loggerImpl) Sync() error {
return logger.Sync()
type SyncLogger struct {
*zap.Logger
}
func NewSyncLogger(logger *zap.Logger) *SyncLogger {
return &SyncLogger{logger}
}
func (s *SyncLogger) Close() error {
return s.Sync()
}

View File

@@ -1,3 +1,20 @@
// 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 middleware
import (

View File

@@ -1,31 +1,50 @@
// 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 models
type Tokens struct {
AccessToken string `json:"access_token"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type RegistrationBeginRequest struct {
Username string `json:"username"`
Email *string `json:"email"`
Password string `json:"password"` // TODO: password checking
Username string `json:"username" binding:"required,min=3,max=20"`
Email *string `json:"email" binding:"email"`
Password string `json:"password" binding:"required,password"` // TODO: password checking
}
// TODO: length check
type RegistrationCompleteRequest struct {
Username string `json:"username"`
VerificationCode string `json:"verification_code"`
Name string `json:"name"`
Username string `json:"username" binding:"required"`
VerificationCode string `json:"verification_code" binding:"required"`
Name string `json:"name" binding:"required,max=75"`
Birthday *string `json:"birthday"`
AvatarUrl *string `json:"avatar_url"`
AvatarUrl *string `json:"avatar_url" binding:"http_url,max=255"`
}
type RegistrationCompleteResponse struct {
Tokens
}
// TODO: length check
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
TOTP *string `json:"totp"`
}
@@ -33,8 +52,9 @@ type LoginResponse struct {
Tokens
}
// TODO: length check
type RefreshRequest struct {
RefreshToken string `json:"refresh_token"`
RefreshToken string `json:"refresh_token" binding:"required"`
}
type RefreshResponse struct {

View File

@@ -1,3 +1,20 @@
// 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 routes
import (

View File

@@ -1,3 +1,20 @@
// 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 routes
import (

View File

@@ -1,12 +1,28 @@
// 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 services
import (
"context"
"easywish/internal/database"
errs "easywish/internal/errors"
"easywish/internal/logger"
"easywish/internal/models"
"easywish/internal/utils"
"easywish/internal/utils/enums"
"go.uber.org/zap"
)
@@ -19,25 +35,59 @@ type AuthService interface {
}
type authServiceImpl struct {
log logger.Logger
log *zap.Logger
dbctx database.DbContext
}
func NewAuthService(_log logger.Logger, _dbctx database.DbContext) AuthService {
func NewAuthService(_log *zap.Logger, _dbctx database.DbContext) AuthService {
return &authServiceImpl{log: _log, dbctx: _dbctx}
}
func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) {
ctx := context.Background()
queries := database.New(a.dbctx)
user, err := queries.CreateUser(ctx, request.Username) // TODO: validation
var user database.User
var generatedCode string
if err != nil {
a.log.Get().Error("Failed to add user to database", zap.Error(err))
helper, db, _ := database.NewDbHelperTransaction(a.dbctx)
defer helper.Rollback()
var err error
if user, err = db.TXQueries.CreateUser(db.CTX, request.Username); err != nil { // TODO: validation
a.log.Error("Failed to add user to database", zap.Error(err))
return false, errs.ErrServerError
}
a.log.Get().Info("Registered a new user", zap.String("username", user.Username))
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,
Email: request.Email,
Password: request.Password, // Hashed in database
}); err != nil {
a.log.Error("Failed to add login information for user to database", zap.Error(err))
return false, errs.ErrServerError
}
if generatedCode, err = utils.GenerateSecure6DigitNumber(); err != nil {
a.log.Error("Failed to generate a registration code", zap.Error(err))
return false, errs.ErrServerError
}
if _, err = db.TXQueries.CreateConfirmationCode(db.CTX, database.CreateConfirmationCodeParams{
UserID: user.ID,
CodeType: int32(enums.RegistrationCodeType),
Code: generatedCode, // Hashed in database
}); err != nil {
a.log.Error("Failed to add registration code to database", zap.Error(err))
return false, errs.ErrServerError
}
a.log.Info("Registered a new user", zap.String("username", user.Username))
helper.Commit()
a.log.Debug("Declated registration code for a new user", zap.String("username", user.Username), zap.String("code", generatedCode))
// TODO: Send verification email

View File

@@ -1,3 +1,20 @@
// 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 services
import (
@@ -9,4 +26,3 @@ var Module = fx.Module("services",
NewAuthService,
),
)

View File

@@ -1,3 +1,20 @@
// 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 utils
import (

View File

@@ -0,0 +1,25 @@
// 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 enums
type ConfirmationCodeType int32
const (
RegistrationCodeType ConfirmationCodeType = iota
PasswordResetCodeType
)

View File

@@ -1,3 +1,20 @@
// 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 utils
import (

View File

@@ -0,0 +1,80 @@
// 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 utils
import (
"crypto/rand"
"easywish/config"
"errors"
"fmt"
"io"
"regexp"
)
func GenerateSecure6DigitNumber() (string, error) {
// Generate a random number between 0 and 999999 (inclusive)
// This ensures we get a 6-digit number, including those starting with 0
max := 1000000 // Upper bound (exclusive)
b := make([]byte, 4) // A 4-byte slice is sufficient for a 32-bit integer
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", fmt.Errorf("failed to read random bytes: %w", err)
}
// Convert bytes to an integer
// We use a simple modulo operation to get a number within our desired range.
// While this introduces a slight bias for very large ranges, for 1,000,000
// it's negligible and simpler than more complex methods like rejection sampling.
num := int(uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])) % max
return fmt.Sprintf("%06d", num), nil
}
func ValidatePassword(password string) error {
cfg := config.GetConfig()
if cfg.PasswordCheckLength {
passwordLength := len(password); if passwordLength < 8 || passwordLength > 100 {
return errors.New("Password must be between 8 and 100 characters")
}
}
if cfg.PasswordCheckNumbers {
numbersPresent := regexp.MustCompile(`[0-9]`).MatchString(password); if !numbersPresent {
return errors.New("Password must contain at least 1 number")
}
}
if cfg.PasswordCheckCases {
differentCasesPresent := regexp.MustCompile(`(?=.*[a-z])(?=.*[A-Z])`).MatchString(password); if !differentCasesPresent {
return errors.New("Password must have uppercase and lowercase characters")
}
}
if cfg.PasswordCheckSymbols {
symbolsPresent := regexp.MustCompile(`[.,/;'[\]\-=_+{}:"<>?\/|!@#$%^&*()~]`).MatchString(password); if !symbolsPresent {
return errors.New("Password must contain at least one special symbol")
}
}
if cfg.PasswordCheckLeaked {
// TODO: implement checking leaked passwords via rockme.txt
}
return nil
}

View File

@@ -94,8 +94,8 @@ WHERE users.username = $1;
--: Confirmation Code Object {{{
;-- name: CreateConfirmationCode :one
INSERT INTO confirmation_codes(user_id, code_type, code_hash, expires_at)
VALUES ($1, $2, crypt($3, gen_salt('bf')), $4) RETURNING *;
INSERT INTO confirmation_codes(user_id, code_type, code_hash)
VALUES ($1, $2, crypt(@code::text, gen_salt('bf'))) RETURNING *;
;-- name: GetConfirmationCodeByCode :one
SELECT * FROM confirmation_codes

View File

@@ -8,7 +8,7 @@ sql:
out: "../backend/internal/database"
sql_package: "pgx/v5"
emit_prepared_queries: true
emit_interface: false
emit_pointers_for_null_types: true
database:
# managed: true
uri: "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"