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 // @title Easywish client API
// @version 1.0 // @version 1.0
// @description Easy and feature-rich wishlist. // @description Easy and feature-rich wishlist.
// @license.name GPL 3.0 // @license.name GPL-3.0
// @BasePath /api/ // @BasePath /api/
// @Schemes http // @Schemes http
@@ -20,6 +37,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/fx" "go.uber.org/fx"
"go.uber.org/zap"
"easywish/config" "easywish/config"
docs "easywish/docs" docs "easywish/docs"
@@ -42,6 +60,7 @@ func main() {
fx.New( fx.New(
fx.Provide( fx.Provide(
logger.NewLogger, logger.NewLogger,
logger.NewSyncLogger,
gin.Default, gin.Default,
), ),
database.Module, database.Module,
@@ -49,7 +68,7 @@ func main() {
controllers.Module, controllers.Module,
routes.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 // Swagger
docs.SwaggerInfo.Schemes = []string{"http"} docs.SwaggerInfo.Schemes = []string{"http"}
@@ -65,6 +84,7 @@ func main() {
OnStart: func(ctx context.Context) error { OnStart: func(ctx context.Context) error {
go func() { go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
syncLogger.Fatal("Server failed", zap.Error(err))
} }
}() }()
return nil return nil
@@ -72,11 +92,18 @@ func main() {
OnStop: func(ctx context.Context) error { OnStop: func(ctx context.Context) error {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() 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() ).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 package config
import ( import (
@@ -8,30 +25,47 @@ import (
) )
type Config struct { type Config struct {
Hostname string `mapstructure:"HOSTNAME"` Hostname string `mapstructure:"HOSTNAME"`
Port string `mapstructure:"PORT"` Port string `mapstructure:"PORT"`
DatabaseUrl string `mapstructure:"POSTGRES_URL"`
RedisUrl string `mapstructure:"REDIS_URL"` DatabaseUrl string `mapstructure:"POSTGRES_URL"`
MinioUrl string `mapstructure:"MINIO_URL"` RedisUrl string `mapstructure:"REDIS_URL"`
JwtAlgorithm string `mapstructure:"JWT_ALGORITHM"` MinioUrl string `mapstructure:"MINIO_URL"`
JwtSecret string `mapstructure:"JWT_SECRET"`
JwtIssuer string `mapstructure:"JWT_ISSUER"` JwtAlgorithm string `mapstructure:"JWT_ALGORITHM"`
JwtAudience string `mapstructure:"JWT_AUDIENCE"` JwtSecret string `mapstructure:"JWT_SECRET"`
JwtExpAccess int `mapstructure:"JWT_EXP_ACCESS"` JwtIssuer string `mapstructure:"JWT_ISSUER"`
JwtExpRefresh int `mapstructure:"JWT_EXP_REFRESH"` JwtAudience string `mapstructure:"JWT_AUDIENCE"`
Environment string `mapstructure:"ENVIRONMENT"` 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) { func Load() (*Config, error) {
viper.SetDefault("HOSTNAME", "localhost") viper.SetDefault("HOSTNAME", "localhost")
viper.SetDefault("PORT", "8080") viper.SetDefault("PORT", "8080")
viper.SetDefault("JWT_ALGORITHM", "HS256") viper.SetDefault("JWT_ALGORITHM", "HS256")
viper.SetDefault("JWT_SECRET", "default_jwt_secret_please_change") viper.SetDefault("JWT_SECRET", "default_jwt_secret_please_change")
viper.SetDefault("JWT_EXP_ACCESS", 5) viper.SetDefault("JWT_EXP_ACCESS", 5)
viper.SetDefault("JWT_EXP_REFRESH", 10080) viper.SetDefault("JWT_EXP_REFRESH", 10080)
viper.SetDefault("JWT_AUDIENCE", "easywish") viper.SetDefault("JWT_AUDIENCE", "easywish")
viper.SetDefault("JWT_ISSUER", "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.SetDefault("ENVIRONMENT", "production")
viper.AutomaticEnv() viper.AutomaticEnv()
@@ -39,18 +73,28 @@ func Load() (*Config, error) {
// Viper's AutomaticEnv() expects lowercase keys for unmarshalling into structs by default, // Viper's AutomaticEnv() expects lowercase keys for unmarshalling into structs by default,
// while the environment variables and struct tags are in uppercase. // while the environment variables and struct tags are in uppercase.
// Here's the stupidity we have to do to fix it: // Here's the stupidity we have to do to fix it:
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.BindEnv("HOSTNAME") viper.BindEnv("HOSTNAME")
viper.BindEnv("PORT") viper.BindEnv("PORT")
viper.BindEnv("POSTGRES_URL") viper.BindEnv("POSTGRES_URL")
viper.BindEnv("REDIS_URL") viper.BindEnv("REDIS_URL")
viper.BindEnv("MINIO_URL") viper.BindEnv("MINIO_URL")
viper.BindEnv("JWT_ALGORITHM") viper.BindEnv("JWT_ALGORITHM")
viper.BindEnv("JWT_SECRET") viper.BindEnv("JWT_SECRET")
viper.BindEnv("JWT_ISSUER") viper.BindEnv("JWT_ISSUER")
viper.BindEnv("JWT_AUDIENCE") viper.BindEnv("JWT_AUDIENCE")
viper.BindEnv("JWT_EXP_ACCESS") viper.BindEnv("JWT_EXP_ACCESS")
viper.BindEnv("JWT_EXP_REFRESH") 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") viper.BindEnv("ENVIRONMENT")
required := []string{ required := []string{

View File

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

View File

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

View File

@@ -13,6 +13,9 @@ definitions:
type: string type: string
username: username:
type: string type: string
required:
- password
- username
type: object type: object
models.LoginResponse: models.LoginResponse:
properties: properties:
@@ -29,7 +32,12 @@ definitions:
description: 'TODO: password checking' description: 'TODO: password checking'
type: string type: string
username: username:
maxLength: 20
minLength: 3
type: string type: string
required:
- password
- username
type: object type: object
info: info:
contact: {} 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 package controllers
import ( import (
@@ -15,4 +32,3 @@ import (
func ChangePassword(c *gin.Context) { func ChangePassword(c *gin.Context) {
c.Status(http.StatusNotImplemented) 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 package controllers
import ( 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 package controllers
import ( 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 package controllers
import ( 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 package controllers
import ( 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 package controllers
import ( 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 package database
import ( 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 ID int64
UserID int64 UserID int64
Date pgtype.Timestamp Date pgtype.Timestamp
Reason pgtype.Text Reason *string
ExpiresAt pgtype.Timestamp ExpiresAt pgtype.Timestamp
BannedBy pgtype.Text BannedBy *string
Pardoned pgtype.Bool Pardoned *bool
PardonedBy pgtype.Text PardonedBy *string
} }
type ConfirmationCode struct { type ConfirmationCode struct {
@@ -25,17 +25,17 @@ type ConfirmationCode struct {
CodeType int32 CodeType int32
CodeHash string CodeHash string
ExpiresAt pgtype.Timestamp ExpiresAt pgtype.Timestamp
Used pgtype.Bool Used *bool
Deleted pgtype.Bool Deleted *bool
} }
type LoginInformation struct { type LoginInformation struct {
ID int64 ID int64
UserID int64 UserID int64
Email pgtype.Text Email *string
PasswordHash string PasswordHash string
TotpEncrypted pgtype.Text TotpEncrypted *string
Email2faEnabled pgtype.Bool Email2faEnabled *bool
PasswordChangeDate pgtype.Timestamp PasswordChangeDate pgtype.Timestamp
} }
@@ -43,41 +43,41 @@ type Profile struct {
ID int64 ID int64
UserID int64 UserID int64
Name string Name string
Bio pgtype.Text Bio *string
AvatarUrl pgtype.Text AvatarUrl *string
Birthday pgtype.Timestamp Birthday pgtype.Timestamp
Color pgtype.Text Color *string
ColorGrad pgtype.Text ColorGrad *string
} }
type ProfileSetting struct { type ProfileSetting struct {
ID int64 ID int64
ProfileID int64 ProfileID int64
HideFulfilled pgtype.Bool HideFulfilled *bool
HideProfileDetails pgtype.Bool HideProfileDetails *bool
HideForUnauthenticated pgtype.Bool HideForUnauthenticated *bool
HideBirthday pgtype.Bool HideBirthday *bool
HideDates pgtype.Bool HideDates *bool
Captcha pgtype.Bool Captcha *bool
FollowersOnlyInteraction pgtype.Bool FollowersOnlyInteraction *bool
} }
type Session struct { type Session struct {
ID int64 ID int64
UserID int64 UserID int64
Guid pgtype.UUID Guid pgtype.UUID
Name pgtype.Text Name *string
Platform pgtype.Text Platform *string
LatestIp pgtype.Text LatestIp *string
LoginTime pgtype.Timestamp LoginTime pgtype.Timestamp
LastSeenDate pgtype.Timestamp LastSeenDate pgtype.Timestamp
Terminated pgtype.Bool Terminated *bool
} }
type User struct { type User struct {
ID int64 ID int64
Username string Username string
Verified pgtype.Bool Verified *bool
RegistrationDate pgtype.Timestamp 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 { type CreateBannedUserParams struct {
UserID int64 UserID int64
ExpiresAt pgtype.Timestamp ExpiresAt pgtype.Timestamp
Reason pgtype.Text Reason *string
BannedBy pgtype.Text BannedBy *string
} }
func (q *Queries) CreateBannedUser(ctx context.Context, arg CreateBannedUserParams) (BannedUser, error) { 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 const createConfirmationCode = `-- name: CreateConfirmationCode :one
INSERT INTO confirmation_codes(user_id, code_type, code_hash, expires_at) INSERT INTO confirmation_codes(user_id, code_type, code_hash)
VALUES ($1, $2, crypt($3, gen_salt('bf')), $4) RETURNING id, user_id, code_type, code_hash, expires_at, used, deleted VALUES ($1, $2, crypt($3::text, gen_salt('bf'))) RETURNING id, user_id, code_type, code_hash, expires_at, used, deleted
` `
type CreateConfirmationCodeParams struct { type CreateConfirmationCodeParams struct {
UserID int64 UserID int64
CodeType int32 CodeType int32
Crypt string Code string
ExpiresAt pgtype.Timestamp
} }
func (q *Queries) CreateConfirmationCode(ctx context.Context, arg CreateConfirmationCodeParams) (ConfirmationCode, error) { func (q *Queries) CreateConfirmationCode(ctx context.Context, arg CreateConfirmationCodeParams) (ConfirmationCode, error) {
row := q.db.QueryRow(ctx, createConfirmationCode, row := q.db.QueryRow(ctx, createConfirmationCode, arg.UserID, arg.CodeType, arg.Code)
arg.UserID,
arg.CodeType,
arg.Crypt,
arg.ExpiresAt,
)
var i ConfirmationCode var i ConfirmationCode
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
@@ -83,7 +77,7 @@ VALUES ( $1, $2, crypt($3::text, gen_salt('bf')) ) RETURNING id, user_id, email,
type CreateLoginInformationParams struct { type CreateLoginInformationParams struct {
UserID int64 UserID int64
Email pgtype.Text Email *string
Password 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 { type CreateProfileParams struct {
UserID int64 UserID int64
Name string Name string
Bio pgtype.Text Bio *string
Birthday pgtype.Timestamp Birthday pgtype.Timestamp
AvatarUrl pgtype.Text AvatarUrl *string
Color pgtype.Text Color *string
ColorGrad pgtype.Text ColorGrad *string
} }
func (q *Queries) CreateProfile(ctx context.Context, arg CreateProfileParams) (Profile, error) { 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 { type CreateSessionParams struct {
UserID int64 UserID int64
Name pgtype.Text Name *string
Platform pgtype.Text Platform *string
LatestIp pgtype.Text LatestIp *string
} }
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { 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 { type GetProfileByUsernameRestrictedParams struct {
Username string Username string
Column2 pgtype.Bool Column2 *bool
} }
type GetProfileByUsernameRestrictedRow struct { type GetProfileByUsernameRestrictedRow struct {
Username string Username string
Name string Name string
Birthday pgtype.Timestamp Birthday pgtype.Timestamp
Bio pgtype.Text Bio *string
AvatarUrl pgtype.Text AvatarUrl *string
Color pgtype.Text Color *string
ColorGrad pgtype.Text ColorGrad *string
HideProfileDetails pgtype.Bool HideProfileDetails *bool
} }
func (q *Queries) GetProfileByUsernameRestricted(ctx context.Context, arg GetProfileByUsernameRestrictedParams) (GetProfileByUsernameRestrictedRow, error) { func (q *Queries) GetProfileByUsernameRestricted(ctx context.Context, arg GetProfileByUsernameRestrictedParams) (GetProfileByUsernameRestrictedRow, error) {
@@ -405,17 +399,17 @@ LIMIT 20 OFFSET 20 * $1
` `
type GetProfilesRestrictedParams struct { type GetProfilesRestrictedParams struct {
Column1 pgtype.Int4 Column1 *int32
Column2 pgtype.Bool Column2 *bool
} }
type GetProfilesRestrictedRow struct { type GetProfilesRestrictedRow struct {
Username string Username string
Name string Name string
AvatarUrl pgtype.Text AvatarUrl *string
Color pgtype.Text Color *string
ColorGrad pgtype.Text ColorGrad *string
HideProfileDetails pgtype.Bool HideProfileDetails *bool
} }
func (q *Queries) GetProfilesRestricted(ctx context.Context, arg GetProfilesRestrictedParams) ([]GetProfilesRestrictedRow, error) { func (q *Queries) GetProfilesRestricted(ctx context.Context, arg GetProfilesRestrictedParams) ([]GetProfilesRestrictedRow, error) {
@@ -558,7 +552,7 @@ type GetUserByLoginCredentialsRow struct {
ID int64 ID int64
Username string Username string
PasswordHash string PasswordHash string
TotpEncrypted pgtype.Text TotpEncrypted *string
} }
func (q *Queries) GetUserByLoginCredentials(ctx context.Context, arg GetUserByLoginCredentialsParams) (GetUserByLoginCredentialsRow, error) { func (q *Queries) GetUserByLoginCredentials(ctx context.Context, arg GetUserByLoginCredentialsParams) (GetUserByLoginCredentialsRow, error) {
@@ -654,11 +648,11 @@ WHERE id = $1
type UpdateBannedUserParams struct { type UpdateBannedUserParams struct {
ID int64 ID int64
Reason pgtype.Text Reason *string
ExpiresAt pgtype.Timestamp ExpiresAt pgtype.Timestamp
BannedBy pgtype.Text BannedBy *string
Pardoned pgtype.Bool Pardoned *bool
PardonedBy pgtype.Text PardonedBy *string
} }
func (q *Queries) UpdateBannedUser(ctx context.Context, arg UpdateBannedUserParams) error { func (q *Queries) UpdateBannedUser(ctx context.Context, arg UpdateBannedUserParams) error {
@@ -681,8 +675,8 @@ WHERE id = $1
type UpdateConfirmationCodeParams struct { type UpdateConfirmationCodeParams struct {
ID int64 ID int64
Used pgtype.Bool Used *bool
Deleted pgtype.Bool Deleted *bool
} }
func (q *Queries) UpdateConfirmationCode(ctx context.Context, arg UpdateConfirmationCodeParams) error { 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 { type UpdateLoginInformationByUsernameParams struct {
Username string Username string
Email pgtype.Text Email *string
Password string Password string
TotpEncrypted pgtype.Text TotpEncrypted *string
Email2faEnabled pgtype.Bool Email2faEnabled *bool
PasswordChangeDate pgtype.Timestamp PasswordChangeDate pgtype.Timestamp
} }
@@ -728,11 +722,11 @@ WHERE username = $1
type UpdateProfileByUsernameParams struct { type UpdateProfileByUsernameParams struct {
Username string Username string
Name string Name string
Bio pgtype.Text Bio *string
Birthday pgtype.Timestamp Birthday pgtype.Timestamp
AvatarUrl pgtype.Text AvatarUrl *string
Color pgtype.Text Color *string
ColorGrad pgtype.Text ColorGrad *string
} }
func (q *Queries) UpdateProfileByUsername(ctx context.Context, arg UpdateProfileByUsernameParams) error { func (q *Queries) UpdateProfileByUsername(ctx context.Context, arg UpdateProfileByUsernameParams) error {
@@ -763,13 +757,13 @@ WHERE id = $1
type UpdateProfileSettingsParams struct { type UpdateProfileSettingsParams struct {
ID int64 ID int64
HideFulfilled pgtype.Bool HideFulfilled *bool
HideProfileDetails pgtype.Bool HideProfileDetails *bool
HideForUnauthenticated pgtype.Bool HideForUnauthenticated *bool
HideBirthday pgtype.Bool HideBirthday *bool
HideDates pgtype.Bool HideDates *bool
Captcha pgtype.Bool Captcha *bool
FollowersOnlyInteraction pgtype.Bool FollowersOnlyInteraction *bool
} }
func (q *Queries) UpdateProfileSettings(ctx context.Context, arg UpdateProfileSettingsParams) error { func (q *Queries) UpdateProfileSettings(ctx context.Context, arg UpdateProfileSettingsParams) error {
@@ -794,12 +788,12 @@ WHERE id = $1
type UpdateSessionParams struct { type UpdateSessionParams struct {
ID int64 ID int64
Name pgtype.Text Name *string
Platform pgtype.Text Platform *string
LatestIp pgtype.Text LatestIp *string
LoginTime pgtype.Timestamp LoginTime pgtype.Timestamp
LastSeenDate pgtype.Timestamp LastSeenDate pgtype.Timestamp
Terminated pgtype.Bool Terminated *bool
} }
func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) error { func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) error {
@@ -823,8 +817,8 @@ WHERE id = $1
type UpdateUserParams struct { type UpdateUserParams struct {
ID int64 ID int64
Verified pgtype.Bool Verified *bool
Deleted pgtype.Bool Deleted *bool
} }
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
@@ -840,8 +834,8 @@ WHERE username = $1
type UpdateUserByUsernameParams struct { type UpdateUserByUsernameParams struct {
Username string Username string
Verified pgtype.Bool Verified *bool
Deleted pgtype.Bool Deleted *bool
} }
func (q *Queries) UpdateUserByUsername(ctx context.Context, arg UpdateUserByUsernameParams) error { 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 package database
import ( import (
@@ -9,5 +26,3 @@ var Module = fx.Module("database",
NewDbContext, 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 package errors
import ( 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 package errors
import ( import (
@@ -8,4 +25,3 @@ var (
ErrNotImplemented = errors.New("Feature is not implemented") ErrNotImplemented = errors.New("Feature is not implemented")
ErrBadRequest = errors.New("Bad request") 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 package logger
import ( import (
@@ -8,40 +25,36 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
type Logger interface {
Get() *zap.Logger
Sync() error
}
type loggerImpl struct {
}
func NewLogger() Logger {
return &loggerImpl{}
}
var ( var (
logger *zap.Logger instance *zap.Logger
once sync.Once once sync.Once
) )
func (l *loggerImpl) Get() *zap.Logger { func NewLogger() *zap.Logger {
once.Do(func() { once.Do(func() {
var err error
cfg := config.GetConfig() cfg := config.GetConfig()
var err error
if cfg.Environment == "production" { if cfg.Environment == "production" {
logger, err = zap.NewProduction() instance, err = zap.NewProduction()
} else { } else {
logger, err = zap.NewDevelopment() instance, err = zap.NewDevelopment()
} }
if err != nil { if err != nil {
panic(err) panic("failed to initialize logger: " + err.Error())
} }
}) })
return logger return instance
} }
func (l *loggerImpl) Sync() error { type SyncLogger struct {
return logger.Sync() *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 package middleware
import ( 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 package models
type Tokens struct { type Tokens struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
} }
type RegistrationBeginRequest struct { type RegistrationBeginRequest struct {
Username string `json:"username"` Username string `json:"username" binding:"required,min=3,max=20"`
Email *string `json:"email"` Email *string `json:"email" binding:"email"`
Password string `json:"password"` // TODO: password checking Password string `json:"password" binding:"required,password"` // TODO: password checking
} }
// TODO: length check
type RegistrationCompleteRequest struct { type RegistrationCompleteRequest struct {
Username string `json:"username"` Username string `json:"username" binding:"required"`
VerificationCode string `json:"verification_code"` VerificationCode string `json:"verification_code" binding:"required"`
Name string `json:"name"` Name string `json:"name" binding:"required,max=75"`
Birthday *string `json:"birthday"` Birthday *string `json:"birthday"`
AvatarUrl *string `json:"avatar_url"` AvatarUrl *string `json:"avatar_url" binding:"http_url,max=255"`
} }
type RegistrationCompleteResponse struct { type RegistrationCompleteResponse struct {
Tokens Tokens
} }
// TODO: length check
type LoginRequest struct { type LoginRequest struct {
Username string `json:"username"` Username string `json:"username" binding:"required"`
Password string `json:"password"` Password string `json:"password" binding:"required"`
TOTP *string `json:"totp"` TOTP *string `json:"totp"`
} }
@@ -33,8 +52,9 @@ type LoginResponse struct {
Tokens Tokens
} }
// TODO: length check
type RefreshRequest struct { type RefreshRequest struct {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token" binding:"required"`
} }
type RefreshResponse struct { 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 package routes
import ( 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 package routes
import ( 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 package services
import ( import (
"context"
"easywish/internal/database" "easywish/internal/database"
errs "easywish/internal/errors" errs "easywish/internal/errors"
"easywish/internal/logger"
"easywish/internal/models" "easywish/internal/models"
"easywish/internal/utils" "easywish/internal/utils"
"easywish/internal/utils/enums"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -19,25 +35,59 @@ type AuthService interface {
} }
type authServiceImpl struct { type authServiceImpl struct {
log logger.Logger log *zap.Logger
dbctx database.DbContext 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} return &authServiceImpl{log: _log, dbctx: _dbctx}
} }
func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) { func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) {
ctx := context.Background() var user database.User
queries := database.New(a.dbctx) var generatedCode string
user, err := queries.CreateUser(ctx, request.Username) // TODO: validation
if err != nil { helper, db, _ := database.NewDbHelperTransaction(a.dbctx)
a.log.Get().Error("Failed to add user to database", zap.Error(err)) 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 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 // 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 package services
import ( import (
@@ -9,4 +26,3 @@ var Module = fx.Module("services",
NewAuthService, 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 package utils
import ( 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 package utils
import ( 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 {{{ --: Confirmation Code Object {{{
;-- name: CreateConfirmationCode :one ;-- name: CreateConfirmationCode :one
INSERT INTO confirmation_codes(user_id, code_type, code_hash, expires_at) INSERT INTO confirmation_codes(user_id, code_type, code_hash)
VALUES ($1, $2, crypt($3, gen_salt('bf')), $4) RETURNING *; VALUES ($1, $2, crypt(@code::text, gen_salt('bf'))) RETURNING *;
;-- name: GetConfirmationCodeByCode :one ;-- name: GetConfirmationCodeByCode :one
SELECT * FROM confirmation_codes SELECT * FROM confirmation_codes

View File

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