Compare commits
103 Commits
87878f15a3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b40a05e2d | |||
| f81e4eaa47 | |||
| 14bad8e7ef | |||
| 3198612e16 | |||
| dd2960a742 | |||
| c7a440e38f | |||
| d12162fc3b | |||
| 711b1ad5d1 | |||
| af69c4fe07 | |||
| 5eb90b18d5 | |||
| bd90fb339f | |||
| e4c879db36 | |||
| 6cb64d5f03 | |||
| d7d18f1284 | |||
| b1125d3f6a | |||
| 3bcd8af100 | |||
| b24ffcf3f8 | |||
| 3a63a14c4d | |||
| 5ed75c350a | |||
| 669349e020 | |||
| 8dba0f79aa | |||
| 08b3942d35 | |||
| 0a38267cb0 | |||
| ed044590a0 | |||
| e15ee90a62 | |||
| fbe73e2a68 | |||
| 2809e1bd37 | |||
| f08cb639a5 | |||
| d14f90d628 | |||
| f38af13dc1 | |||
| 705b420b9e | |||
| df54829a67 | |||
| f65439fb50 | |||
| fc0c73aa5b | |||
| 6f7d8bf244 | |||
| 6588190e8b | |||
| 8f04566b5a | |||
| f2274f6c58 | |||
| feb0524d39 | |||
| f2753e1495 | |||
| d6e2d02bff | |||
| f9d7439def | |||
| 7298ab662f | |||
| ec56f64420 | |||
| 249bbe4a98 | |||
| b986d45d82 | |||
| 827928178e | |||
| 8b558eaf5f | |||
| e465da6854 | |||
| a582b75c82 | |||
| b3a405016e | |||
| ee6cff4104 | |||
| d8ea9f79c6 | |||
| 24cb8ecb6e | |||
| 95294686b7 | |||
| 65ea47dbb6 | |||
| a3bebd89be | |||
| a2dd8993a6 | |||
| 8fa57eddb1 | |||
| b91ff2c802 | |||
| 541847221b | |||
| c988a16783 | |||
| f59b647b27 | |||
| 15c140db31 | |||
| 63b63038d1 | |||
| b5fdcd5dca | |||
| 72a512bb4f | |||
| 8588a17928 | |||
| bc9f5c6d3c | |||
| 333817c9e1 | |||
| 5e32c3cbd3 | |||
| 8319afc7ea | |||
| 0a51727af8 | |||
| d08db300fc | |||
| 96e41efdec | |||
| 284d959bc3 | |||
| e2d83aa779 | |||
| 69d55ce060 | |||
| cbcfb8a286 | |||
| c2059dcd6e | |||
| be9aee7145 | |||
| cfe60cfb8e | |||
| e5d245519a | |||
| 0a00a5ee2b | |||
| 1b55498b00 | |||
| ea3743cb04 | |||
| 613deae8e2 | |||
| e1df58b434 | |||
| ad118cc832 | |||
| a9b28c860f | |||
| b2a96c3b84 | |||
| 0c4d618fa4 | |||
| 03c072e67b | |||
| 8577314875 | |||
| 1dc24df037 | |||
| 0621bad6f8 | |||
| c57b37e88f | |||
| b72645852b | |||
| 8007b11731 | |||
| 7ad1336c88 | |||
| 9b7335a72e | |||
| aab55a143f | |||
| 654c1eb7b5 |
@@ -1,25 +1,55 @@
|
|||||||
// @title Easywish client API
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
// @version 1.0
|
//
|
||||||
// @description Easy and feature-rich wishlist.
|
// This file is part of Easywish
|
||||||
// @license.name GPL 3.0
|
//
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
// @BasePath /api/
|
// @title Easywish client API
|
||||||
// @Schemes http
|
// @version 1.0
|
||||||
|
// @description Easy and feature-rich wishlist.
|
||||||
|
// @license.name GPL-3.0
|
||||||
|
|
||||||
// @securityDefinitions.apikey JWT
|
// @BasePath /api/
|
||||||
// @in header
|
// @Schemes http
|
||||||
// @name Authorization
|
|
||||||
|
// @securityDefinitions.apikey JWT
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"easywish/config"
|
"easywish/config"
|
||||||
"easywish/internal/logger"
|
|
||||||
"easywish/internal/routes"
|
|
||||||
|
|
||||||
docs "easywish/docs"
|
docs "easywish/docs"
|
||||||
|
"easywish/internal/controllers"
|
||||||
|
"easywish/internal/database"
|
||||||
|
"easywish/internal/logger"
|
||||||
|
minioclient "easywish/internal/minioClient"
|
||||||
|
redisclient "easywish/internal/redisClient"
|
||||||
|
"easywish/internal/services"
|
||||||
|
"easywish/internal/validation"
|
||||||
|
|
||||||
swaggerfiles "github.com/swaggo/files"
|
swaggerfiles "github.com/swaggo/files"
|
||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
)
|
)
|
||||||
@@ -30,14 +60,58 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer logger.Sync()
|
cfg := config.GetConfig()
|
||||||
|
|
||||||
r := gin.Default()
|
fx.New(
|
||||||
r = routes.SetupRoutes(r)
|
fx.Provide(
|
||||||
|
logger.NewLogger,
|
||||||
|
logger.NewSyncLogger,
|
||||||
|
redisclient.NewRedisClient,
|
||||||
|
minioclient.NewMinioClient,
|
||||||
|
gin.Default,
|
||||||
|
),
|
||||||
|
database.Module,
|
||||||
|
services.Module,
|
||||||
|
validation.Module,
|
||||||
|
|
||||||
docs.SwaggerInfo.Schemes = []string{"http"}
|
controllers.Module,
|
||||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
|
||||||
|
|
||||||
r.Run(":8080")
|
fx.Invoke(func(lc fx.Lifecycle, router *gin.Engine, syncLogger *logger.SyncLogger) {
|
||||||
|
|
||||||
|
// Swagger
|
||||||
|
docs.SwaggerInfo.Schemes = []string{"http"}
|
||||||
|
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||||
|
|
||||||
|
// Gin
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%s", strconv.Itoa(int(cfg.Port))),
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
lc.Append(fx.Hook{
|
||||||
|
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
|
||||||
|
},
|
||||||
|
OnStop: func(ctx context.Context) error {
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,149 @@
|
|||||||
|
// 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 (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Hostname string `mapstructure:"HOSTNAME"`
|
Hostname string `mapstructure:"HOSTNAME"`
|
||||||
Port string `mapstructure:"PORT"`
|
Port uint16 `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"`
|
MinioHost string `mapstructure:"MINIO_HOST"`
|
||||||
JwtIssuer string `mapstructure:"JWT_ISSUER"`
|
MinioPort uint16 `mapstructure:"MINIO_PORT"`
|
||||||
JwtAudience string `mapstructure:"JWT_AUDIENCE"`
|
MinioTimeout int64 `mapstructure:"MINIO_TIMEOUT"`
|
||||||
JwtExpAccess int `mapstructure:"JWT_EXP_ACCESS"`
|
|
||||||
JwtExpRefresh int `mapstructure:"JWT_EXP_REFRESH"`
|
JwtAlgorithm string `mapstructure:"JWT_ALGORITHM"`
|
||||||
Environment string `mapstructure:"ENVIRONMENT"`
|
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"`
|
||||||
|
|
||||||
|
SmtpEnabled bool `mapstructure:"SMTP_ENABLED"`
|
||||||
|
SmtpServer string `mapstructure:"SMTP_SERVER"`
|
||||||
|
SmtpPort uint16 `mapstructure:"SMTP_PORT"`
|
||||||
|
SmtpUser string `mapstructure:"SMTP_USER"`
|
||||||
|
SmtpPassword string `mapstructure:"SMTP_PASSWORD"`
|
||||||
|
SmtpFrom string `mapstructure:"SMTP_FROM"`
|
||||||
|
SmtpUseTLS bool `mapstructure:"SMTP_USE_TLS"`
|
||||||
|
SmtpUseSSL bool `mapstructure:"SMTP_USE_SSL"`
|
||||||
|
SmtpTimeout uint `mapstructure:"SMTP_TIMEOUT"`
|
||||||
|
|
||||||
|
PasswordMinLength int `mapstructure:"PASSWORD_MIN_LENGTH"`
|
||||||
|
PasswordMaxLength int `mapstructure:"PASSWORD_MAX_LENGTH"`
|
||||||
|
PasswordCheckNumbers bool `mapstructure:"PASSWORD_CHECK_NUMBERS"`
|
||||||
|
PasswordCheckCharacters bool `mapstructure:"PASSWORD_CHECK_CHARACTERS"`
|
||||||
|
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("MINIO_TIMEOUT", 90 * time.Second)
|
||||||
|
|
||||||
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("SMTP_ENABLED", false)
|
||||||
|
viper.SetDefault("SMTP_USE_TLS", false)
|
||||||
|
viper.SetDefault("SMTP_USE_SSL", false)
|
||||||
|
viper.SetDefault("SMTP_FROM", "An Easywish instance")
|
||||||
|
|
||||||
|
viper.SetDefault("PASSWORD_MIN_LENGTH", 6)
|
||||||
|
viper.SetDefault("PASSWORD_MAX_LENGTH", 100)
|
||||||
|
viper.SetDefault("PASSWORD_CHECK_NUMBERS", false)
|
||||||
|
viper.SetDefault("PASSWORD_CHECK_CHARACTERS", 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()
|
||||||
|
|
||||||
|
// 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("MINIO_TIMEOUT")
|
||||||
|
|
||||||
|
viper.BindEnv("POSTGRES_URL")
|
||||||
|
viper.BindEnv("REDIS_URL")
|
||||||
|
viper.BindEnv("MINIO_URL")
|
||||||
|
viper.BindEnv("MINIO_HOST")
|
||||||
|
viper.BindEnv("MINIO_PORT")
|
||||||
|
|
||||||
|
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("SMTP_ENABLED")
|
||||||
|
viper.BindEnv("SMTP_SERVER")
|
||||||
|
viper.BindEnv("SMTP_PORT")
|
||||||
|
viper.BindEnv("SMTP_USER")
|
||||||
|
viper.BindEnv("SMTP_PASSWORD")
|
||||||
|
viper.BindEnv("SMTP_FROM")
|
||||||
|
viper.BindEnv("SMTP_USE_TLS")
|
||||||
|
viper.BindEnv("SMTP_USE_SSL")
|
||||||
|
viper.BindEnv("SMTP_TIMEOUT")
|
||||||
|
|
||||||
|
viper.BindEnv("PASSWORD_MIN_LENGTH")
|
||||||
|
viper.BindEnv("PASSWORD_MAX_LENGTH")
|
||||||
|
viper.BindEnv("PASSWORD_CHECK_NUMBERS")
|
||||||
|
viper.BindEnv("PASSWORD_CHECK_CHARACTERS")
|
||||||
|
viper.BindEnv("PASSWORD_CHECK_CASES")
|
||||||
|
viper.BindEnv("PASSWORD_CHECK_SYMBOLS")
|
||||||
|
viper.BindEnv("PASSWORD_CHECK_LEAKED")
|
||||||
|
|
||||||
|
viper.BindEnv("ENVIRONMENT")
|
||||||
|
|
||||||
required := []string{
|
required := []string{
|
||||||
"POSTGRES_URL",
|
"POSTGRES_URL",
|
||||||
"REDIS_URL",
|
"REDIS_URL",
|
||||||
"MINIO_URL",
|
"MINIO_URL",
|
||||||
|
"MINIO_HOST",
|
||||||
|
"MINIO_PORT",
|
||||||
}
|
}
|
||||||
var missing []string
|
var missing []string
|
||||||
for _, key := range required {
|
for _, key := range required {
|
||||||
@@ -57,19 +161,20 @@ func Load() (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config = &cfg
|
config = &cfg
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConfig() *Config {
|
func GetConfig() Config {
|
||||||
|
|
||||||
if config == nil {
|
if config == nil {
|
||||||
|
|
||||||
if _, err := Load(); err != nil {
|
if _, err := Load(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return *config
|
||||||
}
|
}
|
||||||
|
|
||||||
var config *Config
|
var config *Config
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ const docTemplate = `{
|
|||||||
"title": "{{.Title}}",
|
"title": "{{.Title}}",
|
||||||
"contact": {},
|
"contact": {},
|
||||||
"license": {
|
"license": {
|
||||||
"name": "GPL 3.0"
|
"name": "GPL-3.0"
|
||||||
},
|
},
|
||||||
"version": "{{.Version}}"
|
"version": "{{.Version}}"
|
||||||
},
|
},
|
||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/account/changePassword": {
|
"/auth/changePassword": {
|
||||||
"put": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"JWT": []
|
"JWT": []
|
||||||
@@ -32,10 +32,28 @@ const docTemplate = `{
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Account"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Change account password",
|
"summary": "Set new password using the old password",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.ChangePasswordRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Password successfully changed"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Invalid old password"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/login": {
|
"/auth/login": {
|
||||||
@@ -52,7 +70,7 @@ const docTemplate = `{
|
|||||||
"summary": "Acquire tokens via login credentials (and 2FA code if needed)",
|
"summary": "Acquire tokens via login credentials (and 2FA code if needed)",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "desc",
|
"description": " ",
|
||||||
"name": "request",
|
"name": "request",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -63,10 +81,13 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "desc",
|
"description": " ",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/models.LoginResponse"
|
"$ref": "#/definitions/models.LoginResponse"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Invalid login credentials"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +104,25 @@ const docTemplate = `{
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Request password reset email",
|
"summary": "Request password reset email",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PasswordResetBeginRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Reset code sent to the email if it is attached to an account"
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"description": "Too many recent requests for this email"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/passwordResetComplete": {
|
"/auth/passwordResetComplete": {
|
||||||
@@ -97,8 +136,29 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Complete password reset with email code and provide 2FA code or backup code if needed",
|
"summary": "Complete password reset via email code",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PasswordResetCompleteRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PasswordResetCompleteResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Wrong verification code or username"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/refresh": {
|
"/auth/refresh": {
|
||||||
@@ -113,7 +173,28 @@ const docTemplate = `{
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Receive new tokens via refresh token",
|
"summary": "Receive new tokens via refresh token",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RefreshRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RefreshResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Invalid refresh token"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/registrationBegin": {
|
"/auth/registrationBegin": {
|
||||||
@@ -128,7 +209,28 @@ const docTemplate = `{
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Register an account",
|
"summary": "Register an account",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationBeginRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Account is created and awaiting verification"
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "Username or email is already taken"
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"description": "Too many recent registration attempts for this email"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/registrationComplete": {
|
"/auth/registrationComplete": {
|
||||||
@@ -143,10 +245,31 @@ const docTemplate = `{
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Confirm with code, finish creating the account",
|
"summary": "Confirm with code, finish creating the account",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationCompleteRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationCompleteResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Invalid email or verification code"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/profile/me": {
|
"/profile": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
@@ -162,30 +285,17 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Profile"
|
"Profile"
|
||||||
],
|
],
|
||||||
"summary": "Get own profile when authorized",
|
"summary": "Get your profile",
|
||||||
"responses": {}
|
"responses": {
|
||||||
}
|
"200": {
|
||||||
},
|
"description": " ",
|
||||||
"/profile/privacy": {
|
"schema": {
|
||||||
"get": {
|
"$ref": "#/definitions/dto.ProfileDto"
|
||||||
"security": [
|
}
|
||||||
{
|
|
||||||
"JWT": []
|
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Profile"
|
|
||||||
],
|
|
||||||
"summary": "Get profile privacy settings",
|
|
||||||
"responses": {}
|
|
||||||
},
|
},
|
||||||
"patch": {
|
"put": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"JWT": []
|
"JWT": []
|
||||||
@@ -200,8 +310,89 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Profile"
|
"Profile"
|
||||||
],
|
],
|
||||||
"summary": "Update profile privacy settings",
|
"summary": "Update your profile",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.NewProfileDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/profile/settings": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"JWT": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Profile"
|
||||||
|
],
|
||||||
|
"summary": "Get your profile settings",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ProfileSettingsDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"JWT": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Profile"
|
||||||
|
],
|
||||||
|
"summary": "Update your profile's settings",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ProfileSettingsDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/profile/{username}": {
|
"/profile/{username}": {
|
||||||
@@ -220,17 +411,30 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Profile"
|
"Profile"
|
||||||
],
|
],
|
||||||
"summary": "Get someone's profile details",
|
"summary": "Get profile by username",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Username",
|
"description": " ",
|
||||||
"name": "username",
|
"name": "username",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {}
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ProfileDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Restricted profile"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Profile not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/service/health": {
|
"/service/health": {
|
||||||
@@ -250,7 +454,51 @@ const docTemplate = `{
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "Says whether it's healthy or not",
|
"description": "Says whether it's healthy or not",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/controllers.HealthStatus"
|
"$ref": "#/definitions/models.HealthStatusResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/upload/avatar": {
|
||||||
|
"get": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Upload"
|
||||||
|
],
|
||||||
|
"summary": "Get presigned URL for avatar upload",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Presigned URL and form data",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PresignedUploadResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/upload/image": {
|
||||||
|
"get": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Upload"
|
||||||
|
],
|
||||||
|
"summary": "Get presigned URL for image upload",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Presigned URL and form data",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PresignedUploadResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +506,100 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"controllers.HealthStatus": {
|
"dto.NewProfileDto": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"avatar_upload_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bio": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"birthday": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color_grad": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ProfileDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"avatar_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bio": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"birthday": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color_grad": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ProfileSettingsDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"captcha": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"followers_only_interaction": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_birthday": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_dates": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_for_unauthenticated": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_fulfilled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_profile_details": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.ChangePasswordRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"old_password",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"old_password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"totp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.HealthStatusResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"healthy": {
|
"healthy": {
|
||||||
@@ -268,11 +609,129 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"models.LoginRequest": {
|
"models.LoginRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"password",
|
||||||
|
"username"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100
|
||||||
|
},
|
||||||
|
"totp": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 20,
|
||||||
|
"minLength": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.LoginResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.PasswordResetBeginRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.PasswordResetCompleteRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"password",
|
||||||
|
"verification_code"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"log_out_sessions": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"totp": {
|
"verification_code": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.PasswordResetCompleteResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.PresignedUploadResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fields": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RefreshRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"refresh_token"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RefreshResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RegistrationBeginRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"password",
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
@@ -280,7 +739,29 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models.LoginResponse": {
|
"models.RegistrationCompleteRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"username",
|
||||||
|
"verification_code"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"birthday": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"verification_code": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RegistrationCompleteResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"access_token": {
|
"access_token": {
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
"title": "Easywish client API",
|
"title": "Easywish client API",
|
||||||
"contact": {},
|
"contact": {},
|
||||||
"license": {
|
"license": {
|
||||||
"name": "GPL 3.0"
|
"name": "GPL-3.0"
|
||||||
},
|
},
|
||||||
"version": "1.0"
|
"version": "1.0"
|
||||||
},
|
},
|
||||||
"basePath": "/api/",
|
"basePath": "/api/",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/account/changePassword": {
|
"/auth/changePassword": {
|
||||||
"put": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"JWT": []
|
"JWT": []
|
||||||
@@ -28,10 +28,28 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Account"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Change account password",
|
"summary": "Set new password using the old password",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.ChangePasswordRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Password successfully changed"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Invalid old password"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/login": {
|
"/auth/login": {
|
||||||
@@ -48,7 +66,7 @@
|
|||||||
"summary": "Acquire tokens via login credentials (and 2FA code if needed)",
|
"summary": "Acquire tokens via login credentials (and 2FA code if needed)",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "desc",
|
"description": " ",
|
||||||
"name": "request",
|
"name": "request",
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -59,10 +77,13 @@
|
|||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "desc",
|
"description": " ",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/models.LoginResponse"
|
"$ref": "#/definitions/models.LoginResponse"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Invalid login credentials"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +100,25 @@
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Request password reset email",
|
"summary": "Request password reset email",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PasswordResetBeginRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Reset code sent to the email if it is attached to an account"
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"description": "Too many recent requests for this email"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/passwordResetComplete": {
|
"/auth/passwordResetComplete": {
|
||||||
@@ -93,8 +132,29 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Complete password reset with email code and provide 2FA code or backup code if needed",
|
"summary": "Complete password reset via email code",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PasswordResetCompleteRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PasswordResetCompleteResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Wrong verification code or username"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/refresh": {
|
"/auth/refresh": {
|
||||||
@@ -109,7 +169,28 @@
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Receive new tokens via refresh token",
|
"summary": "Receive new tokens via refresh token",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RefreshRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RefreshResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Invalid refresh token"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/registrationBegin": {
|
"/auth/registrationBegin": {
|
||||||
@@ -124,7 +205,28 @@
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Register an account",
|
"summary": "Register an account",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationBeginRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Account is created and awaiting verification"
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "Username or email is already taken"
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"description": "Too many recent registration attempts for this email"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/registrationComplete": {
|
"/auth/registrationComplete": {
|
||||||
@@ -139,10 +241,31 @@
|
|||||||
"Auth"
|
"Auth"
|
||||||
],
|
],
|
||||||
"summary": "Confirm with code, finish creating the account",
|
"summary": "Confirm with code, finish creating the account",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationCompleteRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.RegistrationCompleteResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Invalid email or verification code"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/profile/me": {
|
"/profile": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
@@ -158,30 +281,17 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Profile"
|
"Profile"
|
||||||
],
|
],
|
||||||
"summary": "Get own profile when authorized",
|
"summary": "Get your profile",
|
||||||
"responses": {}
|
"responses": {
|
||||||
}
|
"200": {
|
||||||
},
|
"description": " ",
|
||||||
"/profile/privacy": {
|
"schema": {
|
||||||
"get": {
|
"$ref": "#/definitions/dto.ProfileDto"
|
||||||
"security": [
|
}
|
||||||
{
|
|
||||||
"JWT": []
|
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Profile"
|
|
||||||
],
|
|
||||||
"summary": "Get profile privacy settings",
|
|
||||||
"responses": {}
|
|
||||||
},
|
},
|
||||||
"patch": {
|
"put": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"JWT": []
|
"JWT": []
|
||||||
@@ -196,8 +306,89 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Profile"
|
"Profile"
|
||||||
],
|
],
|
||||||
"summary": "Update profile privacy settings",
|
"summary": "Update your profile",
|
||||||
"responses": {}
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.NewProfileDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/profile/settings": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"JWT": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Profile"
|
||||||
|
],
|
||||||
|
"summary": "Get your profile settings",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ProfileSettingsDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"JWT": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Profile"
|
||||||
|
],
|
||||||
|
"summary": "Update your profile's settings",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ProfileSettingsDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/profile/{username}": {
|
"/profile/{username}": {
|
||||||
@@ -216,17 +407,30 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Profile"
|
"Profile"
|
||||||
],
|
],
|
||||||
"summary": "Get someone's profile details",
|
"summary": "Get profile by username",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Username",
|
"description": " ",
|
||||||
"name": "username",
|
"name": "username",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {}
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": " ",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ProfileDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Restricted profile"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Profile not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/service/health": {
|
"/service/health": {
|
||||||
@@ -246,7 +450,51 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "Says whether it's healthy or not",
|
"description": "Says whether it's healthy or not",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/controllers.HealthStatus"
|
"$ref": "#/definitions/models.HealthStatusResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/upload/avatar": {
|
||||||
|
"get": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Upload"
|
||||||
|
],
|
||||||
|
"summary": "Get presigned URL for avatar upload",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Presigned URL and form data",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PresignedUploadResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/upload/image": {
|
||||||
|
"get": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Upload"
|
||||||
|
],
|
||||||
|
"summary": "Get presigned URL for image upload",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Presigned URL and form data",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.PresignedUploadResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,7 +502,100 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"controllers.HealthStatus": {
|
"dto.NewProfileDto": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"avatar_upload_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bio": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"birthday": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color_grad": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ProfileDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"avatar_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bio": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"birthday": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color_grad": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dto.ProfileSettingsDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"captcha": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"followers_only_interaction": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_birthday": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_dates": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_for_unauthenticated": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_fulfilled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"hide_profile_details": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.ChangePasswordRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"old_password",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"old_password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"totp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.HealthStatusResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"healthy": {
|
"healthy": {
|
||||||
@@ -264,11 +605,129 @@
|
|||||||
},
|
},
|
||||||
"models.LoginRequest": {
|
"models.LoginRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"password",
|
||||||
|
"username"
|
||||||
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100
|
||||||
|
},
|
||||||
|
"totp": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 20,
|
||||||
|
"minLength": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.LoginResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.PasswordResetBeginRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.PasswordResetCompleteRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"password",
|
||||||
|
"verification_code"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"log_out_sessions": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"totp": {
|
"verification_code": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.PasswordResetCompleteResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.PresignedUploadResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fields": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RefreshRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"refresh_token"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RefreshResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RegistrationBeginRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"password",
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
@@ -276,7 +735,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models.LoginResponse": {
|
"models.RegistrationCompleteRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"username",
|
||||||
|
"verification_code"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"birthday": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"verification_code": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models.RegistrationCompleteResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"access_token": {
|
"access_token": {
|
||||||
|
|||||||
@@ -1,6 +1,67 @@
|
|||||||
basePath: /api/
|
basePath: /api/
|
||||||
definitions:
|
definitions:
|
||||||
controllers.HealthStatus:
|
dto.NewProfileDto:
|
||||||
|
properties:
|
||||||
|
avatar_upload_id:
|
||||||
|
type: string
|
||||||
|
bio:
|
||||||
|
type: string
|
||||||
|
birthday:
|
||||||
|
type: integer
|
||||||
|
color:
|
||||||
|
type: string
|
||||||
|
color_grad:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
dto.ProfileDto:
|
||||||
|
properties:
|
||||||
|
avatar_url:
|
||||||
|
type: string
|
||||||
|
bio:
|
||||||
|
type: string
|
||||||
|
birthday:
|
||||||
|
type: integer
|
||||||
|
color:
|
||||||
|
type: string
|
||||||
|
color_grad:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
dto.ProfileSettingsDto:
|
||||||
|
properties:
|
||||||
|
captcha:
|
||||||
|
type: boolean
|
||||||
|
followers_only_interaction:
|
||||||
|
type: boolean
|
||||||
|
hide_birthday:
|
||||||
|
type: boolean
|
||||||
|
hide_dates:
|
||||||
|
type: boolean
|
||||||
|
hide_for_unauthenticated:
|
||||||
|
type: boolean
|
||||||
|
hide_fulfilled:
|
||||||
|
type: boolean
|
||||||
|
hide_profile_details:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
models.ChangePasswordRequest:
|
||||||
|
properties:
|
||||||
|
old_password:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
totp:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- old_password
|
||||||
|
- password
|
||||||
|
type: object
|
||||||
|
models.HealthStatusResponse:
|
||||||
properties:
|
properties:
|
||||||
healthy:
|
healthy:
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -8,11 +69,17 @@ definitions:
|
|||||||
models.LoginRequest:
|
models.LoginRequest:
|
||||||
properties:
|
properties:
|
||||||
password:
|
password:
|
||||||
|
maxLength: 100
|
||||||
type: string
|
type: string
|
||||||
totp:
|
totp:
|
||||||
type: string
|
type: string
|
||||||
username:
|
username:
|
||||||
|
maxLength: 20
|
||||||
|
minLength: 3
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- password
|
||||||
|
- username
|
||||||
type: object
|
type: object
|
||||||
models.LoginResponse:
|
models.LoginResponse:
|
||||||
properties:
|
properties:
|
||||||
@@ -21,32 +88,131 @@ definitions:
|
|||||||
refresh_token:
|
refresh_token:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
models.PasswordResetBeginRequest:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
type: object
|
||||||
|
models.PasswordResetCompleteRequest:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
log_out_sessions:
|
||||||
|
type: boolean
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
verification_code:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
- password
|
||||||
|
- verification_code
|
||||||
|
type: object
|
||||||
|
models.PasswordResetCompleteResponse:
|
||||||
|
properties:
|
||||||
|
access_token:
|
||||||
|
type: string
|
||||||
|
refresh_token:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
models.PresignedUploadResponse:
|
||||||
|
properties:
|
||||||
|
fields:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
models.RefreshRequest:
|
||||||
|
properties:
|
||||||
|
refresh_token:
|
||||||
|
maxLength: 2000
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- refresh_token
|
||||||
|
type: object
|
||||||
|
models.RefreshResponse:
|
||||||
|
properties:
|
||||||
|
access_token:
|
||||||
|
type: string
|
||||||
|
refresh_token:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
models.RegistrationBeginRequest:
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- email
|
||||||
|
- password
|
||||||
|
- username
|
||||||
|
type: object
|
||||||
|
models.RegistrationCompleteRequest:
|
||||||
|
properties:
|
||||||
|
birthday:
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
verification_code:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- username
|
||||||
|
- verification_code
|
||||||
|
type: object
|
||||||
|
models.RegistrationCompleteResponse:
|
||||||
|
properties:
|
||||||
|
access_token:
|
||||||
|
type: string
|
||||||
|
refresh_token:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
info:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
description: Easy and feature-rich wishlist.
|
description: Easy and feature-rich wishlist.
|
||||||
license:
|
license:
|
||||||
name: GPL 3.0
|
name: GPL-3.0
|
||||||
title: Easywish client API
|
title: Easywish client API
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
paths:
|
paths:
|
||||||
/account/changePassword:
|
/auth/changePassword:
|
||||||
put:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: ' '
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.ChangePasswordRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Password successfully changed
|
||||||
|
"403":
|
||||||
|
description: Invalid old password
|
||||||
security:
|
security:
|
||||||
- JWT: []
|
- JWT: []
|
||||||
summary: Change account password
|
summary: Set new password using the old password
|
||||||
tags:
|
tags:
|
||||||
- Account
|
- Auth
|
||||||
/auth/login:
|
/auth/login:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
parameters:
|
parameters:
|
||||||
- description: desc
|
- description: ' '
|
||||||
in: body
|
in: body
|
||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
@@ -56,9 +222,11 @@ paths:
|
|||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: desc
|
description: ' '
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/models.LoginResponse'
|
$ref: '#/definitions/models.LoginResponse'
|
||||||
|
"403":
|
||||||
|
description: Invalid login credentials
|
||||||
summary: Acquire tokens via login credentials (and 2FA code if needed)
|
summary: Acquire tokens via login credentials (and 2FA code if needed)
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
@@ -66,9 +234,20 @@ paths:
|
|||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: ' '
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.PasswordResetBeginRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Reset code sent to the email if it is attached to an account
|
||||||
|
"429":
|
||||||
|
description: Too many recent requests for this email
|
||||||
summary: Request password reset email
|
summary: Request password reset email
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
@@ -76,20 +255,45 @@ paths:
|
|||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: ' '
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.PasswordResetCompleteRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
summary: Complete password reset with email code and provide 2FA code or backup
|
"200":
|
||||||
code if needed
|
description: ' '
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.PasswordResetCompleteResponse'
|
||||||
|
"403":
|
||||||
|
description: Wrong verification code or username
|
||||||
|
summary: Complete password reset via email code
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
/auth/refresh:
|
/auth/refresh:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: ' '
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.RefreshRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ' '
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.RefreshResponse'
|
||||||
|
"401":
|
||||||
|
description: Invalid refresh token
|
||||||
summary: Receive new tokens via refresh token
|
summary: Receive new tokens via refresh token
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
@@ -97,9 +301,22 @@ paths:
|
|||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: ' '
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.RegistrationBeginRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Account is created and awaiting verification
|
||||||
|
"409":
|
||||||
|
description: Username or email is already taken
|
||||||
|
"429":
|
||||||
|
description: Too many recent registration attempts for this email
|
||||||
summary: Register an account
|
summary: Register an account
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
@@ -107,63 +324,125 @@ paths:
|
|||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: ' '
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.RegistrationCompleteRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ' '
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.RegistrationCompleteResponse'
|
||||||
|
"403":
|
||||||
|
description: Invalid email or verification code
|
||||||
summary: Confirm with code, finish creating the account
|
summary: Confirm with code, finish creating the account
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
|
/profile:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ' '
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ProfileDto'
|
||||||
|
security:
|
||||||
|
- JWT: []
|
||||||
|
summary: Get your profile
|
||||||
|
tags:
|
||||||
|
- Profile
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: ' '
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.NewProfileDto'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ' '
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
security:
|
||||||
|
- JWT: []
|
||||||
|
summary: Update your profile
|
||||||
|
tags:
|
||||||
|
- Profile
|
||||||
/profile/{username}:
|
/profile/{username}:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
parameters:
|
parameters:
|
||||||
- description: Username
|
- description: ' '
|
||||||
in: path
|
in: path
|
||||||
name: username
|
name: username
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ' '
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ProfileDto'
|
||||||
|
"403":
|
||||||
|
description: Restricted profile
|
||||||
|
"404":
|
||||||
|
description: Profile not found
|
||||||
security:
|
security:
|
||||||
- JWT: []
|
- JWT: []
|
||||||
summary: Get someone's profile details
|
summary: Get profile by username
|
||||||
tags:
|
tags:
|
||||||
- Profile
|
- Profile
|
||||||
/profile/me:
|
/profile/settings:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ' '
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ProfileSettingsDto'
|
||||||
security:
|
security:
|
||||||
- JWT: []
|
- JWT: []
|
||||||
summary: Get own profile when authorized
|
summary: Get your profile settings
|
||||||
tags:
|
tags:
|
||||||
- Profile
|
- Profile
|
||||||
/profile/privacy:
|
put:
|
||||||
get:
|
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: ' '
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ProfileSettingsDto'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses: {}
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ' '
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
security:
|
security:
|
||||||
- JWT: []
|
- JWT: []
|
||||||
summary: Get profile privacy settings
|
summary: Update your profile's settings
|
||||||
tags:
|
|
||||||
- Profile
|
|
||||||
patch:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses: {}
|
|
||||||
security:
|
|
||||||
- JWT: []
|
|
||||||
summary: Update profile privacy settings
|
|
||||||
tags:
|
tags:
|
||||||
- Profile
|
- Profile
|
||||||
/service/health:
|
/service/health:
|
||||||
@@ -177,10 +456,38 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: Says whether it's healthy or not
|
description: Says whether it's healthy or not
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/controllers.HealthStatus'
|
$ref: '#/definitions/models.HealthStatusResponse'
|
||||||
summary: Get health status
|
summary: Get health status
|
||||||
tags:
|
tags:
|
||||||
- Service
|
- Service
|
||||||
|
/upload/avatar:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Presigned URL and form data
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.PresignedUploadResponse'
|
||||||
|
summary: Get presigned URL for avatar upload
|
||||||
|
tags:
|
||||||
|
- Upload
|
||||||
|
/upload/image:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Presigned URL and form data
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.PresignedUploadResponse'
|
||||||
|
summary: Get presigned URL for image upload
|
||||||
|
tags:
|
||||||
|
- Upload
|
||||||
schemes:
|
schemes:
|
||||||
- http
|
- http
|
||||||
securityDefinitions:
|
securityDefinitions:
|
||||||
|
|||||||
@@ -4,57 +4,78 @@ go 1.24.3
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
|
||||||
github.com/jackc/pgx/v5 v5.7.5
|
github.com/jackc/pgx/v5 v5.7.5
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/swaggo/files v1.0.1
|
github.com/swaggo/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
|
go.uber.org/fx v1.24.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/crypto v0.40.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/bytedance/sonic v1.13.3 // indirect
|
github.com/bytedance/sonic v1.13.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-openapi/spec v0.21.0 // indirect
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.1 // indirect
|
github.com/go-openapi/swag v0.23.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/minio/crc64nvme v1.0.2 // indirect
|
||||||
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
|
github.com/minio/minio-go/v7 v7.0.95 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
|
github.com/rafiulgits/go-automapper v0.1.4 // indirect
|
||||||
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.14.0 // indirect
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
github.com/spf13/cast v1.9.2 // indirect
|
github.com/spf13/cast v1.9.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tinylib/msgp v1.3.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
go.uber.org/dig v1.19.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.18.0 // indirect
|
golang.org/x/arch v0.18.0 // indirect
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
golang.org/x/net v0.42.0 // indirect
|
||||||
golang.org/x/net v0.41.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
golang.org/x/text v0.26.0 // indirect
|
golang.org/x/text v0.27.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
golang.org/x/tools v0.34.0 // indirect
|
golang.org/x/tools v0.34.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -5,12 +5,18 @@ github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1
|
|||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
@@ -23,6 +29,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
|||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
@@ -37,8 +45,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
@@ -48,6 +58,10 @@ github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
|
|||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@@ -60,9 +74,14 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -74,17 +93,37 @@ github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4
|
|||||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
|
||||||
|
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||||
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
|
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||||
|
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rafiulgits/go-automapper v0.1.4 h1:JiuPl3kjixpngxoDHLXKUfWYIPNLYO7EIGN+m6X0zFk=
|
||||||
|
github.com/rafiulgits/go-automapper v0.1.4/go.mod h1:5R1UXVz04qYUVBQMSOJfC6472yAZIT2wWIl/zx4aNvo=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||||
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
@@ -115,11 +154,17 @@ github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+z
|
|||||||
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||||
|
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||||
|
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||||
|
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||||
|
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
|
||||||
|
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
@@ -132,6 +177,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
@@ -141,10 +188,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -154,6 +205,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
@@ -163,6 +216,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
|
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
@@ -174,6 +231,10 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// @Summary Change account password
|
|
||||||
// @Tags Account
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Security JWT
|
|
||||||
// @Router /account/changePassword [put]
|
|
||||||
func ChangePassword(c *gin.Context) {
|
|
||||||
c.Status(http.StatusNotImplemented)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,82 +1,299 @@
|
|||||||
|
// 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 (
|
||||||
|
errs "easywish/internal/errors"
|
||||||
"easywish/internal/models"
|
"easywish/internal/models"
|
||||||
"easywish/internal/services"
|
"easywish/internal/services"
|
||||||
|
"easywish/internal/utils"
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Summary Register an account
|
type AuthController struct {
|
||||||
// @Tags Auth
|
auth services.AuthService
|
||||||
// @Accept json
|
log *zap.Logger
|
||||||
// @Produce json
|
|
||||||
// @Router /auth/registrationBegin [post]
|
|
||||||
func RegistrationBegin(c *gin.Context) {
|
|
||||||
c.Status(http.StatusNotImplemented)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Confirm with code, finish creating the account
|
func NewAuthController(log *zap.Logger, auth services.AuthService) Controller {
|
||||||
// @Tags Auth
|
ctrl := &AuthController{auth: auth, log: log}
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Router /auth/registrationComplete [post]
|
|
||||||
func RegistrationComplete(c *gin.Context) {
|
|
||||||
c.Status(http.StatusNotImplemented)
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Acquire tokens via login credentials (and 2FA code if needed)
|
|
||||||
// @Tags Auth
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body models.LoginRequest true "desc"
|
|
||||||
// @Success 200 {object} models.LoginResponse "desc"
|
|
||||||
// @Router /auth/login [post]
|
|
||||||
func Login(c *gin.Context) {
|
|
||||||
|
|
||||||
var request models.LoginRequest
|
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
return &controllerImpl{
|
||||||
c.Status(http.StatusBadRequest)
|
Path: "/auth",
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Methods: []ControllerMethod{
|
||||||
|
{
|
||||||
|
HttpMethod: POST,
|
||||||
|
Path: "/registrationBegin",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.registrationBeginHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: POST,
|
||||||
|
Path: "/registrationComplete",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.registrationCompleteHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: POST,
|
||||||
|
Path: "/login",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.loginHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: POST,
|
||||||
|
Path: "/refresh",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.refreshHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: POST,
|
||||||
|
Path: "/passwordResetBegin",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.passwordResetBeginHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: POST,
|
||||||
|
Path: "/passwordResetComplete",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.passwordResetCompleteHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: POST,
|
||||||
|
Path: "/changePassword",
|
||||||
|
Authorization: enums.UserRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.changePasswordHandler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Register an account
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body models.RegistrationBeginRequest true " "
|
||||||
|
// @Success 200 "Account is created and awaiting verification"
|
||||||
|
// @Failure 409 "Username or email is already taken"
|
||||||
|
// @Failure 429 "Too many recent registration attempts for this email"
|
||||||
|
// @Router /auth/registrationBegin [post]
|
||||||
|
func (ctrl *AuthController) registrationBeginHandler(c *gin.Context) {
|
||||||
|
request, err := GetRequest[models.RegistrationBeginRequest](c)
|
||||||
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
auths := services.NewAuthService()
|
_, err = ctrl.auth.RegistrationBegin(request.Body)
|
||||||
|
|
||||||
result, err := auths.Login(request)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Status(http.StatusTeapot)
|
if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) {
|
||||||
return
|
c.Status(http.StatusConflict)
|
||||||
}
|
} else if errors.Is(err, errs.ErrTooManyRequests) {
|
||||||
|
c.Status(http.StatusTooManyRequests)
|
||||||
|
} else {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusFound, result)
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Receive new tokens via refresh token
|
// @Summary Confirm with code, finish creating the account
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Router /auth/refresh [post]
|
// @Param request body models.RegistrationCompleteRequest true " "
|
||||||
func Refresh(c *gin.Context) {
|
// @Success 200 {object} models.RegistrationCompleteResponse " "
|
||||||
c.Status(http.StatusNotImplemented)
|
// @Failure 403 "Invalid email or verification code"
|
||||||
|
// @Router /auth/registrationComplete [post]
|
||||||
|
func (ctrl *AuthController) registrationCompleteHandler(c *gin.Context) {
|
||||||
|
request, err := GetRequest[models.RegistrationCompleteRequest](c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := ctrl.auth.RegistrationComplete(request.User, request.Body)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errs.ErrForbidden) {
|
||||||
|
c.Status(http.StatusForbidden)
|
||||||
|
} else if errors.Is(err, errs.ErrUnauthorized) {
|
||||||
|
c.Status(http.StatusUnauthorized)
|
||||||
|
} else {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Request password reset email
|
// @Summary Acquire tokens via login credentials (and 2FA code if needed)
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Router /auth/passwordResetBegin [post]
|
// @Param request body models.LoginRequest true " "
|
||||||
func PasswordResetBegin(c *gin.Context) {
|
// @Success 200 {object} models.LoginResponse " "
|
||||||
c.Status(http.StatusNotImplemented)
|
// @Failure 403 "Invalid login credentials"
|
||||||
|
// @Router /auth/login [post]
|
||||||
|
func (ctrl *AuthController) loginHandler(c *gin.Context) {
|
||||||
|
request, err := GetRequest[models.LoginRequest](c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := ctrl.auth.Login(request.User, request.Body)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errs.ErrForbidden) {
|
||||||
|
c.Status(http.StatusForbidden)
|
||||||
|
} else {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Complete password reset with email code and provide 2FA code or backup code if needed
|
// @Summary Receive new tokens via refresh token
|
||||||
// @Tags Auth
|
// @Tags Auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Router /auth/passwordResetComplete [post]
|
// @Param request body models.RefreshRequest true " "
|
||||||
func PasswordResetComplete(c *gin.Context) {
|
// @Router /auth/refresh [post]
|
||||||
c.Status(http.StatusNotImplemented)
|
// @Success 200 {object} models.RefreshResponse " "
|
||||||
|
// @Failure 401 "Invalid refresh token"
|
||||||
|
func (ctrl *AuthController) refreshHandler(c *gin.Context) {
|
||||||
|
request, err := GetRequest[models.RefreshRequest](c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := ctrl.auth.Refresh(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
if utils.ErrorIsOneOf(
|
||||||
|
err,
|
||||||
|
errs.ErrTokenExpired,
|
||||||
|
errs.ErrTokenInvalid,
|
||||||
|
errs.ErrInvalidToken,
|
||||||
|
errs.ErrWrongTokenType,
|
||||||
|
errs.ErrSessionNotFound,
|
||||||
|
errs.ErrSessionTerminated,
|
||||||
|
) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Request password reset email
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body models.PasswordResetBeginRequest true " "
|
||||||
|
// @Router /auth/passwordResetBegin [post]
|
||||||
|
// @Success 200 "Reset code sent to the email if it is attached to an account"
|
||||||
|
// @Failure 429 "Too many recent requests for this email"
|
||||||
|
func (ctrl *AuthController) passwordResetBeginHandler(c *gin.Context) {
|
||||||
|
request, err := GetRequest[models.PasswordResetBeginRequest](c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ctrl.auth.PasswordResetBegin(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errs.ErrTooManyRequests) {
|
||||||
|
c.Status(http.StatusTooManyRequests)
|
||||||
|
} else {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Complete password reset via email code
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body models.PasswordResetCompleteRequest true " "
|
||||||
|
// @Router /auth/passwordResetComplete [post]
|
||||||
|
// @Success 200 {object} models.PasswordResetCompleteResponse " "
|
||||||
|
// @Success 403 "Wrong verification code or username"
|
||||||
|
func (ctrl *AuthController) passwordResetCompleteHandler(c *gin.Context) {
|
||||||
|
request, err := GetRequest[models.PasswordResetCompleteRequest](c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := ctrl.auth.PasswordResetComplete(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errs.ErrForbidden) {
|
||||||
|
c.Status(http.StatusForbidden)
|
||||||
|
} else {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Set new password using the old password
|
||||||
|
// @Tags Auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security JWT
|
||||||
|
// @Param request body models.ChangePasswordRequest true " "
|
||||||
|
// @Success 200 "Password successfully changed"
|
||||||
|
// @Failure 403 "Invalid old password"
|
||||||
|
// @Router /auth/changePassword [post]
|
||||||
|
func (ctrl *AuthController) changePasswordHandler(c *gin.Context) {
|
||||||
|
request, err := GetRequest[models.ChangePasswordRequest](c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ctrl.auth.ChangePassword(request.Body, request.User)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errs.ErrForbidden) {
|
||||||
|
c.Status(http.StatusForbidden)
|
||||||
|
} else {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|||||||
154
backend/internal/controllers/controller.go
Normal file
154
backend/internal/controllers/controller.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
// 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 (
|
||||||
|
"easywish/internal/dto"
|
||||||
|
"easywish/internal/services"
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
|
"easywish/internal/validation"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
GET = "GET"
|
||||||
|
POST = "POST"
|
||||||
|
PUT = "PUT"
|
||||||
|
PATCH = "PATCH"
|
||||||
|
DELETE = "DELETE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ControllerMethod struct {
|
||||||
|
HttpMethod string
|
||||||
|
Path string
|
||||||
|
Authorization enums.Role
|
||||||
|
Middleware []gin.HandlerFunc
|
||||||
|
Function func (c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type controllerImpl struct {
|
||||||
|
Path string
|
||||||
|
Middleware []gin.HandlerFunc
|
||||||
|
Methods []ControllerMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
type Controller interface {
|
||||||
|
Setup(group *gin.RouterGroup, log *zap.Logger, auth services.AuthService)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *controllerImpl) Setup(group *gin.RouterGroup, log *zap.Logger, auth services.AuthService) {
|
||||||
|
ctrlGroup := group.Group(ctrl.Path)
|
||||||
|
ctrlGroup.Use(ctrl.Middleware...)
|
||||||
|
|
||||||
|
for _, method := range ctrl.Methods {
|
||||||
|
ctrlGroup.Handle(
|
||||||
|
method.HttpMethod,
|
||||||
|
method.Path,
|
||||||
|
append(
|
||||||
|
method.Middleware,
|
||||||
|
gin.HandlerFunc(func(c *gin.Context) {
|
||||||
|
clientInfo, _ := c.Get("client_info")
|
||||||
|
if clientInfo.(dto.ClientInfo).Role < method.Authorization {
|
||||||
|
c.AbortWithStatusJSON(
|
||||||
|
http.StatusForbidden,
|
||||||
|
gin.H{"error": "Insufficient authorization for this method"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
method.Function)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFile(c *gin.Context, name string, maxSize int64, allowedTypes map[string]bool) (*string, error) {
|
||||||
|
file, err := c.FormFile(name); if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("File '%s': not provided", name)})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.Size > int64(maxSize) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("File '%s': file too large", name)})
|
||||||
|
return nil, fmt.Errorf("File too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileType := file.Header.Get("Content-Type")
|
||||||
|
if len(allowedTypes) > 0 && !allowedTypes[fileType] {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("File '%s': forbidden file type: %s", name, fileType)})
|
||||||
|
return nil, fmt.Errorf("Wrong file type")
|
||||||
|
}
|
||||||
|
|
||||||
|
folderPath := "/tmp/uploads"
|
||||||
|
if _, err := os.Stat(folderPath); os.IsNotExist(err) {
|
||||||
|
os.MkdirAll(folderPath, 0700)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := fmt.Sprintf("%s/%s-%s", folderPath, uuid.New().String(), filepath.Base(file.Filename))
|
||||||
|
if err := c.SaveUploadedFile(file, filePath); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Error saving file"})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRequest[ModelT any](c *gin.Context) (*dto.Request[ModelT], error) {
|
||||||
|
|
||||||
|
var body ModelT
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Think hard on a singleton for better performance
|
||||||
|
validate := validation.NewValidator()
|
||||||
|
|
||||||
|
if err := validate.Struct(body); err != nil {
|
||||||
|
c.AbortWithStatusJSON(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
gin.H{"error": err.Error()})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cinfo := GetClientInfo(c)
|
||||||
|
|
||||||
|
return &dto.Request[ModelT]{
|
||||||
|
Body: body,
|
||||||
|
User: cinfo,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClientInfo(c * gin.Context) (dto.ClientInfo) {
|
||||||
|
|
||||||
|
cinfoFromCtx, ok := c.Get("client_info"); if !ok {
|
||||||
|
c.AbortWithStatusJSON(
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
gin.H{"error": "Client info was not found"})
|
||||||
|
panic("No client_info found in gin context. Does the handler use AuthMiddleware?")
|
||||||
|
}
|
||||||
|
cinfo := cinfoFromCtx.(dto.ClientInfo)
|
||||||
|
|
||||||
|
return cinfo
|
||||||
|
}
|
||||||
@@ -1,65 +1,201 @@
|
|||||||
|
// 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 (
|
||||||
|
"easywish/internal/dto"
|
||||||
|
errs "easywish/internal/errors"
|
||||||
|
"easywish/internal/services"
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Summary Get someone's profile details
|
type ProfileController struct {
|
||||||
// @Tags Profile
|
log *zap.Logger
|
||||||
// @Accept json
|
ps services.ProfileService
|
||||||
// @Produce json
|
}
|
||||||
// @Param username path string true "Username"
|
|
||||||
// @Security JWT
|
|
||||||
// @Router /profile/{username} [get]
|
|
||||||
func GetProfile(c *gin.Context) {
|
|
||||||
|
|
||||||
username := c.Param("username")
|
func NewProfileController(_log *zap.Logger, _ps services.ProfileService) Controller {
|
||||||
|
|
||||||
if username == "" {
|
ctrl := ProfileController{log: _log, ps: _ps}
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
|
||||||
"error": "Username cannot be empty",
|
return &controllerImpl{
|
||||||
})
|
Path: "/profile",
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Methods: []ControllerMethod{
|
||||||
|
{
|
||||||
|
HttpMethod: GET,
|
||||||
|
Path: "",
|
||||||
|
Authorization: enums.UserRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.getMyProfile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: GET,
|
||||||
|
Path: "/:username",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.getProfileByUsername,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: GET,
|
||||||
|
Path: "/settings",
|
||||||
|
Authorization: enums.UserRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.getProfileSettings,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: PUT,
|
||||||
|
Path: "",
|
||||||
|
Authorization: enums.UserRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.updateProfile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: PUT,
|
||||||
|
Path: "/settings",
|
||||||
|
Authorization: enums.UserRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.updateProfileSettings,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get your profile
|
||||||
|
// @Tags Profile
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security JWT
|
||||||
|
// @Success 200 {object} dto.ProfileDto " "
|
||||||
|
// @Router /profile [get]
|
||||||
|
func (ctrl *ProfileController) getMyProfile(c *gin.Context) {
|
||||||
|
cinfo := GetClientInfo(c)
|
||||||
|
|
||||||
|
response, err := ctrl.ps.GetMyProfile(cinfo); if err != nil {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{
|
c.JSON(http.StatusOK, response)
|
||||||
"username": username,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Get own profile when authorized
|
// @Summary Get profile by username
|
||||||
// @Tags Profile
|
// @Tags Profile
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security JWT
|
// @Security JWT
|
||||||
// @Router /profile/me [get]
|
// @Param username path string true " "
|
||||||
func GetOwnProfile(c *gin.Context) {
|
// @Success 200 {object} dto.ProfileDto " "
|
||||||
|
// @Failure 404 "Profile not found"
|
||||||
|
// @Failure 403 "Restricted profile"
|
||||||
|
// @Router /profile/{username} [get]
|
||||||
|
func (ctrl *ProfileController) getProfileByUsername(c *gin.Context) {
|
||||||
|
cinfo := GetClientInfo(c)
|
||||||
|
|
||||||
username := "Gregory House"
|
username := c.Param("username"); if username == "" {
|
||||||
|
c.Status(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{
|
response, err := ctrl.ps.GetProfileByUsername(cinfo, username); if err != nil {
|
||||||
"username": username,
|
if errors.Is(err, errs.ErrNotFound) {
|
||||||
})
|
c.JSON(http.StatusNotFound, gin.H{"error": "Profile not found"})
|
||||||
|
} else if errors.Is(err, errs.ErrUnauthorized) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Profile avaiable to authrorized users only"})
|
||||||
|
} else if errors.Is(err, errs.ErrForbidden) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Access restricted by profile's privacy settings"})
|
||||||
|
} else if errors.Is(err, errs.ErrGone) {
|
||||||
|
c.JSON(http.StatusGone, gin.H{"error": "Profile no longer available"})
|
||||||
|
} else {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Get profile privacy settings
|
// @Summary Get your profile settings
|
||||||
// @Tags Profile
|
// @Tags Profile
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security JWT
|
// @Security JWT
|
||||||
// @Router /profile/privacy [get]
|
// @Success 200 {object} dto.ProfileSettingsDto " "
|
||||||
func GetPrivacySettings(c *gin.Context) {
|
// @Router /profile/settings [get]
|
||||||
c.Status(http.StatusNotImplemented)
|
func (ctrl *ProfileController) getProfileSettings(c *gin.Context) {
|
||||||
|
cinfo := GetClientInfo(c)
|
||||||
|
|
||||||
|
response, err := ctrl.ps.GetProfileSettings(cinfo); if err != nil {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Update profile privacy settings
|
// @Summary Update your profile
|
||||||
// @Tags Profile
|
// @Tags Profile
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security JWT
|
// @Security JWT
|
||||||
// @Router /profile/privacy [patch]
|
// @Param request body dto.NewProfileDto true " "
|
||||||
func UpdatePrivacySettings(c *gin.Context) {
|
// @Success 200 {object} bool " "
|
||||||
c.Status(http.StatusNotImplemented)
|
// @Router /profile [put]
|
||||||
|
func (ctrl *ProfileController) updateProfile(c *gin.Context) {
|
||||||
|
request, err := GetRequest[dto.NewProfileDto](c); if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := ctrl.ps.UpdateProfile(request.User, request.Body); if err != nil {
|
||||||
|
|
||||||
|
if errors.Is(err, errs.ErrFileNotFound) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid upload ID. Make sure file was uploaded and is not expired."})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Update your profile's settings
|
||||||
|
// @Tags Profile
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security JWT
|
||||||
|
// @Param request body dto.ProfileSettingsDto true " "
|
||||||
|
// @Success 200 {object} bool " "
|
||||||
|
// @Router /profile/settings [put]
|
||||||
|
func (ctrl *ProfileController) updateProfileSettings(c *gin.Context) {
|
||||||
|
request, err := GetRequest[dto.ProfileSettingsDto](c); if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := ctrl.ps.UpdateProfileSettings(request.User, request.Body); if err != nil || !response {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|||||||
102
backend/internal/controllers/s3.go
Normal file
102
backend/internal/controllers/s3.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// 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 (
|
||||||
|
"easywish/internal/middleware"
|
||||||
|
"easywish/internal/models"
|
||||||
|
"easywish/internal/services"
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type S3Controller struct {
|
||||||
|
log *zap.Logger
|
||||||
|
s3 services.S3Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewS3Controller(_log *zap.Logger, _us services.S3Service) Controller {
|
||||||
|
ctrl := S3Controller{log: _log, s3: _us}
|
||||||
|
|
||||||
|
return &controllerImpl{
|
||||||
|
Path: "/upload",
|
||||||
|
Middleware: []gin.HandlerFunc{
|
||||||
|
middleware.RateLimitMiddleware(rate.Every(2*time.Second), 1),
|
||||||
|
},
|
||||||
|
Methods: []ControllerMethod{
|
||||||
|
{
|
||||||
|
HttpMethod: GET,
|
||||||
|
Path: "/avatar",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.getAvatarUploadUrl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HttpMethod: GET,
|
||||||
|
Path: "/image",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.getImageUploadUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get presigned URL for avatar upload
|
||||||
|
// @Tags Upload
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.PresignedUploadResponse "Presigned URL and form data"
|
||||||
|
// @Router /upload/avatar [get]
|
||||||
|
func (ctrl *S3Controller) getAvatarUploadUrl(c *gin.Context) {
|
||||||
|
url, formData, err := ctrl.s3.CreateAvatarUrl()
|
||||||
|
if err != nil {
|
||||||
|
ctrl.log.Error("Failed to generate avatar upload URL", zap.Error(err))
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.PresignedUploadResponse{
|
||||||
|
Url: *url,
|
||||||
|
Fields: *formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get presigned URL for image upload
|
||||||
|
// @Tags Upload
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.PresignedUploadResponse "Presigned URL and form data"
|
||||||
|
// @Router /upload/image [get]
|
||||||
|
func (ctrl *S3Controller) getImageUploadUrl(c *gin.Context) {
|
||||||
|
url, formData, err := ctrl.s3.CreateImageUrl()
|
||||||
|
if err != nil {
|
||||||
|
c.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.PresignedUploadResponse{
|
||||||
|
Url: *url,
|
||||||
|
Fields: *formData,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,22 +1,60 @@
|
|||||||
|
// 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 (
|
||||||
|
"easywish/internal/models"
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthStatus struct {
|
type ServiceController struct {}
|
||||||
Healthy bool `json:"healthy"`
|
|
||||||
|
func NewServiceController() Controller {
|
||||||
|
|
||||||
|
ctrl := &ServiceController{}
|
||||||
|
|
||||||
|
return &controllerImpl{
|
||||||
|
Path: "/service",
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Methods: []ControllerMethod{
|
||||||
|
|
||||||
|
{
|
||||||
|
HttpMethod: GET,
|
||||||
|
Path: "/health",
|
||||||
|
Authorization: enums.GuestRole,
|
||||||
|
Middleware: []gin.HandlerFunc{},
|
||||||
|
Function: ctrl.healthHandler,
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Get health status
|
// @Summary Get health status
|
||||||
// @Description Used internally for checking service health
|
// @Description Used internally for checking service health
|
||||||
// @Tags Service
|
// @Tags Service
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} HealthStatus "Says whether it's healthy or not"
|
// @Success 200 {object} models.HealthStatusResponse "Says whether it's healthy or not"
|
||||||
// @Router /service/health [get]
|
// @Router /service/health [get]
|
||||||
func HealthCheck(c *gin.Context) {
|
func (ctrl *ServiceController) healthHandler(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"healthy": true})
|
c.JSON(http.StatusOK, models.HealthStatusResponse{Healthy: true,})
|
||||||
}
|
}
|
||||||
|
|||||||
74
backend/internal/controllers/setup.go
Normal file
74
backend/internal/controllers/setup.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// 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 (
|
||||||
|
"easywish/internal/dto"
|
||||||
|
"easywish/internal/middleware"
|
||||||
|
"easywish/internal/services"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SetupControllersParams struct {
|
||||||
|
fx.In
|
||||||
|
Controllers []Controller `group:"controllers"`
|
||||||
|
Log *zap.Logger
|
||||||
|
Auth services.AuthService
|
||||||
|
Group *gin.Engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupControllers(p SetupControllersParams) {
|
||||||
|
|
||||||
|
apiGroup := p.Group.Group("/api")
|
||||||
|
apiGroup.Use(middleware.AuthMiddleware(p.Log, p.Auth))
|
||||||
|
apiGroup.Use(gin.HandlerFunc(func(c *gin.Context) {
|
||||||
|
ip := c.ClientIP()
|
||||||
|
userAgent := c.Request.UserAgent()
|
||||||
|
sessionInfoFromCtx, ok := c.Get("session_info"); if !ok {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid or missing session data"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionInfo := sessionInfoFromCtx.(dto.SessionInfo)
|
||||||
|
|
||||||
|
c.Set("client_info", dto.ClientInfo{
|
||||||
|
SessionInfo: sessionInfo,
|
||||||
|
IP: ip,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}))
|
||||||
|
for _, ctrl := range p.Controllers {
|
||||||
|
ctrl.Setup(apiGroup, p.Log, p.Auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Module = fx.Module("controllers",
|
||||||
|
fx.Provide(
|
||||||
|
fx.Annotate(NewServiceController, fx.ResultTags(`group:"controllers"`)),
|
||||||
|
fx.Annotate(NewAuthController, fx.ResultTags(`group:"controllers"`)),
|
||||||
|
fx.Annotate(NewProfileController, fx.ResultTags(`group:"controllers"`)),
|
||||||
|
fx.Annotate(NewS3Controller, fx.ResultTags(`group:"controllers"`)),
|
||||||
|
),
|
||||||
|
fx.Invoke(setupControllers),
|
||||||
|
)
|
||||||
66
backend/internal/database/dbContext.go
Normal file
66
backend/internal/database/dbContext.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
"easywish/config"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DbContext interface {
|
||||||
|
DBTX
|
||||||
|
Close()
|
||||||
|
BeginTx(ctx context.Context) (pgx.Tx, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbContextImpl struct {
|
||||||
|
Pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDbContext() DbContext {
|
||||||
|
pool, err := pgxpool.New(context.Background(), config.GetConfig().DatabaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
panic("db connection failed: " + err.Error())
|
||||||
|
}
|
||||||
|
return &dbContextImpl{Pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbContextImpl) Close() {
|
||||||
|
d.Pool.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbContextImpl) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
|
||||||
|
return d.Pool.Exec(ctx, sql, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbContextImpl) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) {
|
||||||
|
return d.Pool.Query(ctx, sql, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbContextImpl) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row {
|
||||||
|
return d.Pool.QueryRow(ctx, sql, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dbContextImpl) BeginTx(ctx context.Context) (pgx.Tx, error) {
|
||||||
|
return d.Pool.Begin(ctx)
|
||||||
|
}
|
||||||
104
backend/internal/database/helper.go
Normal file
104
backend/internal/database/helper.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// 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
|
||||||
|
RollbackOnError(err error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DbHelper struct {
|
||||||
|
CTX context.Context
|
||||||
|
Queries Queries
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbHelperTransactionImpl struct {
|
||||||
|
CTX context.Context
|
||||||
|
TXlessQueries Queries
|
||||||
|
TX pgx.Tx
|
||||||
|
TXQueries Queries
|
||||||
|
isCommited bool
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
d.isCommited = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return errCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollback implements DbHelperTransaction.
|
||||||
|
func (d *dbHelperTransactionImpl) Rollback() error {
|
||||||
|
if d.isCommited {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.TX.Rollback(d.CTX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RollbackOnError implements DbHelperTransaction.
|
||||||
|
func (d *dbHelperTransactionImpl) RollbackOnError(err error) error {
|
||||||
|
if d.isCommited || err == nil {
|
||||||
|
return d.Rollback()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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,70 @@ 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
|
LastRefreshExpTime pgtype.Timestamp
|
||||||
Terminated pgtype.Bool
|
LastSeenDate pgtype.Timestamp
|
||||||
|
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
|
Role int32
|
||||||
|
Deleted *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Wish struct {
|
||||||
|
ID int64
|
||||||
|
Guid pgtype.UUID
|
||||||
|
WishListID int64
|
||||||
|
WishListGuid pgtype.UUID
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
PictureUrl string
|
||||||
|
Stars int16
|
||||||
|
CreationDate pgtype.Timestamp
|
||||||
|
Fulfilled bool
|
||||||
|
FulfilledDate pgtype.Timestamp
|
||||||
|
Deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type WishList struct {
|
||||||
|
ID int64
|
||||||
|
Guid pgtype.UUID
|
||||||
|
ProfileID int64
|
||||||
|
Hidden bool
|
||||||
|
Name string
|
||||||
|
IconName string
|
||||||
|
Color string
|
||||||
|
ColorGrad string
|
||||||
|
Deleted bool
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
28
backend/internal/database/setup.go
Normal file
28
backend/internal/database/setup.go
Normal file
@@ -0,0 +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 database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Module = fx.Module("database",
|
||||||
|
fx.Provide(
|
||||||
|
NewDbContext,
|
||||||
|
),
|
||||||
|
)
|
||||||
24
backend/internal/dto/clientInfo.go
Normal file
24
backend/internal/dto/clientInfo.go
Normal file
@@ -0,0 +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/>.
|
||||||
|
|
||||||
|
package dto
|
||||||
|
|
||||||
|
type ClientInfo struct {
|
||||||
|
SessionInfo
|
||||||
|
IP string
|
||||||
|
UserAgent string
|
||||||
|
}
|
||||||
22
backend/internal/dto/general.go
Normal file
22
backend/internal/dto/general.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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 dto
|
||||||
|
|
||||||
|
type UrlDto struct {
|
||||||
|
Url string `json:"url" binding:"required"`
|
||||||
|
}
|
||||||
46
backend/internal/dto/profile.go
Normal file
46
backend/internal/dto/profile.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// 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 dto
|
||||||
|
|
||||||
|
type ProfileDto struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Bio string `json:"bio"`
|
||||||
|
AvatarUrl *string `json:"avatar_url"`
|
||||||
|
Birthday int64 `json:"birthday"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
ColorGrad string `json:"color_grad"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewProfileDto struct {
|
||||||
|
Name string `json:"name" binding:"required" validate:"name"`
|
||||||
|
Bio string `json:"bio" validate:"bio"`
|
||||||
|
AvatarUploadID string `json:"avatar_upload_id" validate:"omitempty,upload_id=avatar"`
|
||||||
|
Birthday int64 `json:"birthday" validate:"birthday_unix_milli"`
|
||||||
|
Color string `json:"color" validate:"color_hex"`
|
||||||
|
ColorGrad string `json:"color_grad" validate:"color_hex"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileSettingsDto struct {
|
||||||
|
HideFulfilled bool `json:"hide_fulfilled"`
|
||||||
|
HideProfileDetails bool `json:"hide_profile_details"`
|
||||||
|
HideForUnauthenticated bool `json:"hide_for_unauthenticated"`
|
||||||
|
HideBirthday bool `json:"hide_birthday"`
|
||||||
|
HideDates bool `json:"hide_dates"`
|
||||||
|
Captcha bool `json:"captcha"`
|
||||||
|
FollowersOnlyInteraction bool `json:"followers_only_interaction"`
|
||||||
|
}
|
||||||
23
backend/internal/dto/request.go
Normal file
23
backend/internal/dto/request.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// 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 dto
|
||||||
|
|
||||||
|
type Request[T any] struct {
|
||||||
|
User ClientInfo
|
||||||
|
Body T
|
||||||
|
}
|
||||||
27
backend/internal/dto/sessionInfo.go
Normal file
27
backend/internal/dto/sessionInfo.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
// 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 dto
|
||||||
|
|
||||||
|
import "easywish/internal/utils/enums"
|
||||||
|
|
||||||
|
type SessionInfo struct {
|
||||||
|
Username string
|
||||||
|
Session string
|
||||||
|
Role enums.Role
|
||||||
|
}
|
||||||
32
backend/internal/dto/userClaims.go
Normal file
32
backend/internal/dto/userClaims.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 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 dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserClaims struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Role enums.Role `json:"role"`
|
||||||
|
Type enums.JwtTokenType `json:"type"`
|
||||||
|
Session string `json:"session"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
55
backend/internal/dto/wishList.go
Normal file
55
backend/internal/dto/wishList.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// 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 dto
|
||||||
|
|
||||||
|
type WishListDto struct {
|
||||||
|
Guid string `json:"guid" mapstructure:"guid"`
|
||||||
|
Name string `json:"name" mapstructure:"name"`
|
||||||
|
Hidden bool `json:"hidden" mapstructure:"hidden"`
|
||||||
|
IconName string `json:"icon_name" mapstructure:"icon_name"`
|
||||||
|
Color string `json:"color" mapstructure:"color"`
|
||||||
|
ColorGrad string `json:"color_grad" mapstructure:"color_grad"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewWishListDto struct {
|
||||||
|
Name string `json:"name" mapstructure:"name" binding:"required" validate:"max=32"`
|
||||||
|
Hidden bool `json:"hidden" mapstructure:"hidden"`
|
||||||
|
IconName string `json:"icon_name" mapstructure:"icon_name" validate:"omitempty,max=64"`
|
||||||
|
Color string `json:"color" mapstructure:"color" validate:"omitempty,color_hex"`
|
||||||
|
ColorGrad string `json:"color_grad" mapstructure:"color_grad" validate:"omitempty,color_hex"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WishDto struct {
|
||||||
|
Guid string `json:"guid" mapstructure:"guid"`
|
||||||
|
WishListGuid string `json:"wish_list_guid" mapstructure:"wish_list_guid"`
|
||||||
|
Name string `json:"name" mapstructure:"name"`
|
||||||
|
Description string `json:"description" mapstructure:"description"`
|
||||||
|
PictureUrl string `json:"picture_url" mapstructure:"picture_url"`
|
||||||
|
Stars int `json:"stars" mapstructure:"stars"`
|
||||||
|
CreationDate int64 `json:"creation_date" mapstructure:"creation_date"`
|
||||||
|
Fulfilled bool `json:"fulfilled" mapstructure:"fulfilled"`
|
||||||
|
FulfilledDate int64 `json:"fulfilled_date" mapstructure:"fulfilled_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewWishDto struct {
|
||||||
|
WishListGuid string `json:"wish_list_guid" mapstructure:"wish_list_guid" binding:"required" validate:"guid"`
|
||||||
|
Name string `json:"name" mapstructure:"name" binding:"required" validate:"max=64"`
|
||||||
|
Description string `json:"description" mapstructure:"description" validate:"omitempty,max=1000"`
|
||||||
|
PictureUploadID string `json:"picture_upload_id" mapstructure:"picture_upload_id" validate:"omitempty,upload_id=image"`
|
||||||
|
Stars int `json:"stars" mapstructure:"stars" validate:"min=1,max=5"`
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
@@ -6,4 +23,16 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUnauthorized = errors.New("User is not authorized")
|
ErrUnauthorized = errors.New("User is not authorized")
|
||||||
|
ErrUsernameTaken = errors.New("Provided username is already in use")
|
||||||
|
ErrEmailTaken = errors.New("Provided email is already in use")
|
||||||
|
ErrUserNotFound = errors.New("User was not found")
|
||||||
|
ErrInvalidCredentials = errors.New("Invalid username, password or TOTP code")
|
||||||
|
ErrInvalidToken = errors.New("Token is invalid or expired")
|
||||||
|
ErrServerError = errors.New("Internal server error")
|
||||||
|
|
||||||
|
ErrTokenExpired = errors.New("Token is expired")
|
||||||
|
ErrTokenInvalid = ErrInvalidToken
|
||||||
|
ErrWrongTokenType = errors.New("Invalid token type")
|
||||||
|
ErrSessionNotFound = errors.New("Could not find session in database")
|
||||||
|
ErrSessionTerminated = errors.New("Session is terminated")
|
||||||
)
|
)
|
||||||
|
|||||||
24
backend/internal/errors/controller.go
Normal file
24
backend/internal/errors/controller.go
Normal file
@@ -0,0 +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/>.
|
||||||
|
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrClientInfoNotProvided = errors.New("No client info provded")
|
||||||
|
)
|
||||||
@@ -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 (
|
||||||
@@ -7,5 +24,8 @@ import (
|
|||||||
var (
|
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")
|
||||||
|
ErrForbidden = errors.New("Access is denied")
|
||||||
|
ErrTooManyRequests = errors.New("Too many requests")
|
||||||
|
ErrNotFound = errors.New("Resource not found")
|
||||||
|
ErrGone = errors.New("Resource no longer available")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
34
backend/internal/errors/postgres.go
Normal file
34
backend/internal/errors/postgres.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
|
//
|
||||||
|
// This file is part of Easywish
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||||
|
// the GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPgError(err error) string {
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
errors.As(err, &pgErr)
|
||||||
|
return pgErr.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchPgError(err error, code string) bool {
|
||||||
|
return GetPgError(err) == code
|
||||||
|
}
|
||||||
27
backend/internal/errors/s3.go
Normal file
27
backend/internal/errors/s3.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
|
//
|
||||||
|
// This file is part of Easywish
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||||
|
// the GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFileNotFound = errors.New("File with this key does not exist")
|
||||||
|
)
|
||||||
27
backend/internal/errors/smtp.go
Normal file
27
backend/internal/errors/smtp.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
|
//
|
||||||
|
// This file is part of Easywish
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||||
|
// the GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSmtpDisabled = errors.New("Smtp is not enabled in the config")
|
||||||
|
ErrSmtpMissingConfiguration = errors.New("Some necessary SMTP configuration is missing")
|
||||||
|
)
|
||||||
@@ -1,33 +1,60 @@
|
|||||||
|
// 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 (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"easywish/config"
|
"easywish/config"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger *zap.Logger
|
instance *zap.Logger
|
||||||
once sync.Once
|
once sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetLogger() *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 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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,70 @@
|
|||||||
|
// 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 (
|
||||||
"easywish/config"
|
"easywish/internal/dto"
|
||||||
|
"easywish/internal/services"
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"go.uber.org/zap"
|
||||||
|
errs "easywish/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Claims struct {
|
func AuthMiddleware(log *zap.Logger, auth services.AuthService) gin.HandlerFunc {
|
||||||
Username string `json:"username"`
|
|
||||||
jwt.RegisteredClaims
|
|
||||||
}
|
|
||||||
|
|
||||||
func JWTAuthMiddleware() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
cfg := config.GetConfig()
|
|
||||||
authHeader := c.GetHeader("Authorization")
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
|
||||||
if authHeader == "" {
|
if authHeader == "" {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
|
||||||
|
c.Set("session_info", dto.SessionInfo{
|
||||||
|
Username: "",
|
||||||
|
Session: "",
|
||||||
|
Role: enums.GuestRole},
|
||||||
|
)
|
||||||
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenString := authHeader
|
tokenString := authHeader
|
||||||
|
if sessionInfo, err := auth.ValidateToken(tokenString, enums.JwtAccessTokenType); err != nil {
|
||||||
token, err := jwt.ParseWithClaims(
|
if errors.Is(err, errs.ErrTokenExpired) {
|
||||||
tokenString,
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token is expired"})
|
||||||
&Claims{},
|
} else if errors.Is(err, errs.ErrTokenInvalid) {
|
||||||
func(token *jwt.Token) (any, error) {
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token is invalid"})
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
} else if errors.Is(err, errs.ErrWrongTokenType) {
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token type"})
|
||||||
}
|
} else if errors.Is(err, errs.ErrSessionNotFound) {
|
||||||
return []byte(cfg.JwtSecret), nil
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Could not find session in database"})
|
||||||
},
|
} else if errors.Is(err, errs.ErrSessionTerminated) {
|
||||||
)
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Session is terminated"})
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token expired"})
|
|
||||||
} else {
|
} else {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
c.Set("session_info", *sessionInfo)
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
return
|
||||||
c.Set("userID", claims.Username)
|
|
||||||
c.Next()
|
|
||||||
} else {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid claims"})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
backend/internal/middleware/ratelimit.go
Normal file
37
backend/internal/middleware/ratelimit.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 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 (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RateLimitMiddleware(r rate.Limit, b int) gin.HandlerFunc {
|
||||||
|
limiter := rate.NewLimiter(r, b)
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if !limiter.Allow() {
|
||||||
|
c.AbortWithStatus(http.StatusTooManyRequests)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
87
backend/internal/minioClient/buckets.go
Normal file
87
backend/internal/minioClient/buckets.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// 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 minioclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/lifecycle"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Buckets map[string]string
|
||||||
|
|
||||||
|
func setupBuckets(client *minio.Client) {
|
||||||
|
|
||||||
|
Buckets = map[string]string{
|
||||||
|
"avatars": "ew-avatars",
|
||||||
|
"images": "ew-images",
|
||||||
|
"uploads": "ew-uploads",
|
||||||
|
}
|
||||||
|
|
||||||
|
hiddenBuckets := []string{
|
||||||
|
"uploads",
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTICE: it has a formatting value in there for the bucket name!!
|
||||||
|
// I'm kind of ashamed for doing this, but the library did not have
|
||||||
|
// an API for configuring a policy, so we're left with JSON I guess
|
||||||
|
readOnlyPolicyTemplate := `{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject"],"Effect": "Allow","Principal": {"AWS": ["*"]},"Resource": ["arn:aws:s3:::%s/*"],"Sid": ""}]}`
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
var newBuckets []string
|
||||||
|
for key, value := range Buckets {
|
||||||
|
bucketExists, err := client.BucketExists(ctx, value); if err != nil {
|
||||||
|
panic("Failure to check if bucket '" + value + "' exists: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bucketExists {
|
||||||
|
err := client.MakeBucket(ctx, value, minio.MakeBucketOptions{}); if err != nil {
|
||||||
|
panic("Failure to create bucket '" + value + "': " + err.Error())
|
||||||
|
}
|
||||||
|
newBuckets = append(newBuckets, key)
|
||||||
|
|
||||||
|
if !slices.Contains(hiddenBuckets, key) {
|
||||||
|
client.SetBucketPolicy(ctx, value, fmt.Sprintf(readOnlyPolicyTemplate, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(newBuckets, "uploads") {
|
||||||
|
uploadsCfg := lifecycle.NewConfiguration()
|
||||||
|
uploadsCfg.Rules = []lifecycle.Rule{
|
||||||
|
{
|
||||||
|
ID: "expire-uploads",
|
||||||
|
Status: "Enabled",
|
||||||
|
Expiration: lifecycle.Expiration{Days: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.SetBucketLifecycle(ctx, Buckets["uploads"], uploadsCfg); if err != nil {
|
||||||
|
errRm := client.RemoveBucket(ctx, Buckets["uploads"])
|
||||||
|
|
||||||
|
if errRm != nil {
|
||||||
|
panic("Failed to set lifecycle configuration for the uploads bucket: '" + err.Error() + "' and then failed to delete it: " + errRm.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("Failed to set lifecycle configuration for the uploads bucket: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
backend/internal/minioClient/ginEndpoint.go
Normal file
69
backend/internal/minioClient/ginEndpoint.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// 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 minioclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"easywish/config"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupGinEndpoint(router *gin.Engine) {
|
||||||
|
cfg := config.GetConfig()
|
||||||
|
minioHost := fmt.Sprintf("%s:%d", cfg.MinioHost, cfg.MinioPort)
|
||||||
|
|
||||||
|
proxy := &httputil.ReverseProxy{
|
||||||
|
Director: func(req *http.Request) {
|
||||||
|
path := strings.TrimPrefix(req.URL.Path, "/s3")
|
||||||
|
targetURL := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: minioHost,
|
||||||
|
Path: path,
|
||||||
|
RawQuery: req.URL.RawQuery,
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL = targetURL
|
||||||
|
req.Host = minioHost
|
||||||
|
req.Header.Set("Host", minioHost)
|
||||||
|
|
||||||
|
req.Header.Del("X-Forwarded-For")
|
||||||
|
req.Header.Del("X-Forwarded-Host")
|
||||||
|
req.Header.Del("X-Forwarded-Proto")
|
||||||
|
},
|
||||||
|
Transport: &http.Transport{
|
||||||
|
ResponseHeaderTimeout: time.Duration(cfg.MinioTimeout),
|
||||||
|
},
|
||||||
|
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte(`{"error": "Failed to forward request"}`))
|
||||||
|
fmt.Printf("Proxy error: %v\n", err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s3group := router.Group("/s3")
|
||||||
|
s3group.Any("/*path", func(c *gin.Context) {
|
||||||
|
proxy.ServeHTTP(c.Writer, c.Request)
|
||||||
|
})
|
||||||
|
}
|
||||||
61
backend/internal/minioClient/minioClient.go
Normal file
61
backend/internal/minioClient/minioClient.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// 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 minioclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"easywish/config"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMinioClient(router *gin.Engine) *minio.Client {
|
||||||
|
cfg := config.GetConfig()
|
||||||
|
|
||||||
|
if cfg.MinioUrl == "" {
|
||||||
|
panic("Failed Minio URL not set in config")
|
||||||
|
}
|
||||||
|
|
||||||
|
minioURL, err := url.Parse(cfg.MinioUrl); if err != nil {
|
||||||
|
panic("Failed to parse Minio URL: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
user := minioURL.User.Username()
|
||||||
|
password, ok := minioURL.User.Password(); if !ok {
|
||||||
|
panic("Failed to parse Minio secret key from the URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := minio.New(minioURL.Host, &minio.Options{
|
||||||
|
Creds: credentials.NewStaticV4(user, password, ""),
|
||||||
|
Secure: minioURL.Scheme == "https",
|
||||||
|
}); if err != nil {
|
||||||
|
panic("Failed to initiate minio client: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.HealthCheck(time.Second*5); if err != nil {
|
||||||
|
panic("Failed to communicate with minio: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
setupBuckets(client)
|
||||||
|
setupGinEndpoint(router)
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
@@ -1,13 +1,47 @@
|
|||||||
|
// 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 {
|
||||||
|
Username string `json:"username" binding:"required" validate:"username"`
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required" validate:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegistrationCompleteRequest struct {
|
||||||
|
Username string `json:"username" binding:"required" validate:"username"`
|
||||||
|
VerificationCode string `json:"verification_code" binding:"required" validate:"verification_code=reg"`
|
||||||
|
Name string `json:"name" binding:"required" validate:"name"`
|
||||||
|
Birthday int64 `json:"birthday" validate:"birthday_unix_milli"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegistrationCompleteResponse struct {
|
||||||
|
Tokens
|
||||||
|
}
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username" binding:"required,min=3,max=20" validate:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password" binding:"required,max=100"`
|
||||||
TOTP *string `json:"totp"`
|
TOTP *string `json:"totp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,9 +50,34 @@ type LoginResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RefreshRequest struct {
|
type RefreshRequest struct {
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token" binding:"required,max=2000"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RefreshResponse struct {
|
type RefreshResponse struct {
|
||||||
Tokens
|
Tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PasswordResetBeginRequest struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PasswordResetCompleteRequest struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
VerificationCode string `json:"verification_code" binding:"required" validate:"verification_code=reset"`
|
||||||
|
NewPassword string `json:"password" binding:"required" validate:"password"`
|
||||||
|
LogOutSessions bool `json:"log_out_sessions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PasswordResetCompleteResponse struct {
|
||||||
|
Tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangePasswordRequest struct {
|
||||||
|
OldPassword string `json:"old_password" binding:"required"`
|
||||||
|
NewPassword string `json:"password" binding:"required" validate:"password"`
|
||||||
|
TOTP string `json:"totp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangePasswordResponse struct {
|
||||||
|
Tokens
|
||||||
|
}
|
||||||
|
|||||||
18
backend/internal/models/profile.go
Normal file
18
backend/internal/models/profile.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// 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
|
||||||
24
backend/internal/models/s3.go
Normal file
24
backend/internal/models/s3.go
Normal file
@@ -0,0 +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/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
type PresignedUploadResponse struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
Fields map[string]string `json:"fields"`
|
||||||
|
}
|
||||||
|
|
||||||
22
backend/internal/models/service.go
Normal file
22
backend/internal/models/service.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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 HealthStatusResponse struct {
|
||||||
|
Healthy bool `json:"healthy"`
|
||||||
|
}
|
||||||
45
backend/internal/redisClient/redisClient.go
Normal file
45
backend/internal/redisClient/redisClient.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// 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 redisclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"easywish/config"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRedisClient() *redis.Client {
|
||||||
|
|
||||||
|
cfg := config.GetConfig()
|
||||||
|
|
||||||
|
options, err := redis.ParseURL(cfg.RedisUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to parse redis URL: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
client := redis.NewClient(options)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := client.Ping(ctx).Err(); err != nil {
|
||||||
|
panic("Redis connection failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"easywish/internal/controllers"
|
|
||||||
"easywish/internal/middleware"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SetupRoutes(r *gin.Engine) *gin.Engine {
|
|
||||||
apiGroup := r.Group("/api")
|
|
||||||
{
|
|
||||||
serviceGroup := apiGroup.Group("/service")
|
|
||||||
{
|
|
||||||
serviceGroup.GET("/health", controllers.HealthCheck)
|
|
||||||
}
|
|
||||||
|
|
||||||
authGroup := apiGroup.Group("/auth")
|
|
||||||
{
|
|
||||||
authGroup.POST("/registrationBegin", controllers.RegistrationBegin)
|
|
||||||
authGroup.POST("/registrationComplete", controllers.RegistrationComplete)
|
|
||||||
authGroup.POST("/login", controllers.Login)
|
|
||||||
authGroup.POST("/refresh", controllers.Refresh)
|
|
||||||
authGroup.POST("/passwordResetBegin", controllers.PasswordResetBegin)
|
|
||||||
authGroup.POST("/passwordResetComplete", controllers.PasswordResetComplete)
|
|
||||||
}
|
|
||||||
|
|
||||||
profileGroup := apiGroup.Group("/profile")
|
|
||||||
{
|
|
||||||
profileGroup.GET("/:username", controllers.GetProfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected := apiGroup.Group("")
|
|
||||||
protected.Use(middleware.JWTAuthMiddleware())
|
|
||||||
{
|
|
||||||
accountGroup := protected.Group("/account")
|
|
||||||
{
|
|
||||||
accountGroup.PUT("/changePassword", controllers.ChangePassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
profileGroup := protected.Group("/profile")
|
|
||||||
{
|
|
||||||
profileGroup.GET("/me", controllers.GetOwnProfile)
|
|
||||||
profileGroup.GET("/privacy", controllers.GetPrivacySettings)
|
|
||||||
profileGroup.PATCH("/privacy", controllers.UpdatePrivacySettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
245
backend/internal/services/profile.go
Normal file
245
backend/internal/services/profile.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
// 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 (
|
||||||
|
"easywish/internal/database"
|
||||||
|
"easywish/internal/dto"
|
||||||
|
errs "easywish/internal/errors"
|
||||||
|
"easywish/internal/utils"
|
||||||
|
mapspecial "easywish/internal/utils/mapSpecial"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/rafiulgits/go-automapper"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfileService interface {
|
||||||
|
GetProfileByUsername(cinfo dto.ClientInfo, username string) (*dto.ProfileDto, error)
|
||||||
|
GetMyProfile(cinfo dto.ClientInfo) (*dto.ProfileDto, error)
|
||||||
|
UpdateProfile(cinfo dto.ClientInfo, newProfile dto.NewProfileDto) (bool, error)
|
||||||
|
GetProfileSettings(cinfo dto.ClientInfo) (*dto.ProfileSettingsDto, error)
|
||||||
|
UpdateProfileSettings(cinfo dto.ClientInfo, newProfileSettings dto.ProfileSettingsDto) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type profileServiceImpl struct {
|
||||||
|
log *zap.Logger
|
||||||
|
dbctx database.DbContext
|
||||||
|
redis *redis.Client
|
||||||
|
minio *minio.Client
|
||||||
|
s3 S3Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProfileService(_log *zap.Logger, _dbctx database.DbContext, _redis *redis.Client, _minio *minio.Client, _s3 S3Service) ProfileService {
|
||||||
|
return &profileServiceImpl{log: _log, dbctx: _dbctx, redis: _redis, minio: _minio, s3: _s3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *profileServiceImpl) GetMyProfile(cinfo dto.ClientInfo) (*dto.ProfileDto, error) {
|
||||||
|
db := database.NewDbHelper(p.dbctx);
|
||||||
|
|
||||||
|
profile, err := db.Queries.GetProfileByUsername(db.CTX, cinfo.Username); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to find user profile by username",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
profileDto := &dto.ProfileDto{}
|
||||||
|
mapspecial.MapProfileDto(profile, profileDto)
|
||||||
|
|
||||||
|
return profileDto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *profileServiceImpl) GetProfileByUsername(cinfo dto.ClientInfo, username string) (*dto.ProfileDto, error) {
|
||||||
|
helper, db, err := database.NewDbHelperTransaction(p.dbctx); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to start transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
defer helper.Rollback()
|
||||||
|
|
||||||
|
profileRow, err := db.TXQueries.GetProfileByUsername(db.CTX, username); if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, errs.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to get user profile by username",
|
||||||
|
zap.String("username", username),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
accessChecks, err := db.TXlessQueries.CheckProfileAccess(db.CTX, database.CheckProfileAccessParams{
|
||||||
|
Requester: cinfo.Username,
|
||||||
|
ID: profileRow.ID,
|
||||||
|
}); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to check access for given profile",
|
||||||
|
zap.String("profile_owner_username", username),
|
||||||
|
zap.String("requester", cinfo.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessChecks.AuthRequired {
|
||||||
|
return nil, errs.ErrUnauthorized
|
||||||
|
}
|
||||||
|
if accessChecks.Hidden {
|
||||||
|
return nil, errs.ErrForbidden
|
||||||
|
}
|
||||||
|
if accessChecks.UserBanned {
|
||||||
|
return nil, errs.ErrGone
|
||||||
|
}
|
||||||
|
if accessChecks.UserUnavailable {
|
||||||
|
return nil, errs.ErrGone
|
||||||
|
}
|
||||||
|
if accessChecks.CaptchaRequired {
|
||||||
|
p.log.Warn("Captcha check is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
profileDto := &dto.ProfileDto{
|
||||||
|
Name: profileRow.Name,
|
||||||
|
Bio: profileRow.Bio,
|
||||||
|
AvatarUrl: &profileRow.AvatarUrl,
|
||||||
|
Birthday: profileRow.Birthday.Time.UnixMilli(),
|
||||||
|
Color: profileRow.Color,
|
||||||
|
ColorGrad: profileRow.ColorGrad,
|
||||||
|
}
|
||||||
|
|
||||||
|
return profileDto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *profileServiceImpl) GetProfileSettings(cinfo dto.ClientInfo) (*dto.ProfileSettingsDto, error) {
|
||||||
|
db := database.NewDbHelper(p.dbctx);
|
||||||
|
|
||||||
|
profileSettings, err := db.Queries.GetProfileSettingsByUsername(db.CTX, cinfo.Username); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to find user profile settings by username",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
profileSettingsDto := &dto.ProfileSettingsDto{}
|
||||||
|
automapper.Map(profileSettings, profileSettingsDto)
|
||||||
|
|
||||||
|
return profileSettingsDto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *profileServiceImpl) UpdateProfile(cinfo dto.ClientInfo, newProfile dto.NewProfileDto) (bool, error) {
|
||||||
|
helper, db, err := database.NewDbHelperTransaction(p.dbctx); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to open transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return false, errs.ErrServerError
|
||||||
|
}
|
||||||
|
defer helper.Rollback()
|
||||||
|
|
||||||
|
birthdayTimestamp := pgtype.Timestamp {
|
||||||
|
Time: time.UnixMilli(newProfile.Birthday),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarUrl *string
|
||||||
|
if newProfile.AvatarUploadID != "" {
|
||||||
|
key, err := p.s3.SaveUpload(newProfile.AvatarUploadID, "avatars"); if err != nil {
|
||||||
|
|
||||||
|
if errors.Is(err, errs.ErrFileNotFound) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.log.Error("Failed to save avatar",
|
||||||
|
zap.String("upload_id", newProfile.AvatarUploadID),
|
||||||
|
zap.Error(err))
|
||||||
|
return false, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
urlObj := p.s3.GetLocalizedFileUrl(*key, "avatars")
|
||||||
|
avatarUrl = utils.NewPointer(urlObj.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.TXlessQueries.UpdateProfileByUsername(db.CTX, database.UpdateProfileByUsernameParams{
|
||||||
|
Username: cinfo.Username,
|
||||||
|
Name: newProfile.Name,
|
||||||
|
Bio: newProfile.Bio,
|
||||||
|
Birthday: birthdayTimestamp,
|
||||||
|
AvatarUrl: avatarUrl,
|
||||||
|
Color: newProfile.Color,
|
||||||
|
ColorGrad: newProfile.ColorGrad,
|
||||||
|
}); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to update user profile",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
return false, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
err = helper.Commit(); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to commit transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return false, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *profileServiceImpl) UpdateProfileSettings(cinfo dto.ClientInfo, newProfileSettings dto.ProfileSettingsDto) (bool, error) {
|
||||||
|
helper, db, err := database.NewDbHelperTransaction(p.dbctx); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to open transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer helper.Rollback()
|
||||||
|
|
||||||
|
// I wanted an automapper here but I'm feeling lazy, it's not used anywhere else regardless.
|
||||||
|
// Also this was initially meant to be a PATCH request but I realized that the fields in the
|
||||||
|
// DTO model are not pointers. Too late, guess this is a PUT request now. Who the hell cares
|
||||||
|
// about a couple extra bytes you have to send every now and then anyways.
|
||||||
|
err = db.TXlessQueries.UpdateProfileSettingsByUsername(db.CTX, database.UpdateProfileSettingsByUsernameParams{
|
||||||
|
Username: cinfo.Username,
|
||||||
|
HideFulfilled: newProfileSettings.HideFulfilled,
|
||||||
|
HideProfileDetails: newProfileSettings.HideProfileDetails,
|
||||||
|
HideForUnauthenticated: newProfileSettings.HideForUnauthenticated,
|
||||||
|
HideBirthday: newProfileSettings.HideBirthday,
|
||||||
|
Captcha: newProfileSettings.Captcha,
|
||||||
|
FollowersOnlyInteraction: newProfileSettings.FollowersOnlyInteraction,
|
||||||
|
}); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to update user profile settings",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
return false, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
err = helper.Commit(); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to commit transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return false, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
176
backend/internal/services/s3.go
Normal file
176
backend/internal/services/s3.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
// 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/config"
|
||||||
|
minioclient "easywish/internal/minioClient"
|
||||||
|
errs "easywish/internal/errors"
|
||||||
|
"easywish/internal/utils"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type S3Service interface {
|
||||||
|
CreateAvatarUrl() (*string, *map[string]string, error)
|
||||||
|
CreateImageUrl() (*string, *map[string]string, error)
|
||||||
|
SaveUpload(uploadID string, bucket string) (*string, error)
|
||||||
|
|
||||||
|
GetLocalizedFileUrl(key string, bucket string) url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
type s3ServiceImpl struct {
|
||||||
|
minio *minio.Client
|
||||||
|
log *zap.Logger
|
||||||
|
|
||||||
|
avatarPolicy minio.PostPolicy
|
||||||
|
imagePolicy minio.PostPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewS3Service(_minio *minio.Client, _log *zap.Logger) S3Service {
|
||||||
|
service := s3ServiceImpl{
|
||||||
|
minio: _minio,
|
||||||
|
log: _log,
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarPolicy := minio.NewPostPolicy()
|
||||||
|
imagePolicy := minio.NewPostPolicy()
|
||||||
|
|
||||||
|
// At the moment the parameters match for both policies but this may
|
||||||
|
// change with introduction of new policies
|
||||||
|
for _, policy := range [...]*minio.PostPolicy{avatarPolicy, imagePolicy} {
|
||||||
|
if err := policy.SetBucket(minioclient.Buckets["uploads"]); err != nil {
|
||||||
|
panic("Failed to set bucket for policy: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := policy.SetExpires(time.Now().UTC().Add(10 * time.Minute)); err != nil {
|
||||||
|
panic("Failed to set expiration time for policy: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := policy.SetContentTypeStartsWith("image/"); err != nil {
|
||||||
|
panic("Failed to set allowed content types for the policy: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := policy.SetContentLengthRange(1, 512*1024); err != nil {
|
||||||
|
panic("Failed to set allowed content length range for the policy: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.imagePolicy = *imagePolicy
|
||||||
|
service.avatarPolicy = *avatarPolicy
|
||||||
|
|
||||||
|
return &service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3ServiceImpl) genUrl(policy minio.PostPolicy, prefix string) (*string, *map[string]string, error) {
|
||||||
|
|
||||||
|
object := prefix + uuid.New().String()
|
||||||
|
|
||||||
|
if err := policy.SetKey(object); err != nil {
|
||||||
|
s.log.Error(
|
||||||
|
"Failed to set random key for presigned url",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
url, formData, err := s.minio.PresignedPostPolicy(context.Background(), &policy)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error(
|
||||||
|
"Failed to generate presigned url",
|
||||||
|
zap.String("object", object),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedUrl, err := utils.LocalizeS3Url(url.String())
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error(
|
||||||
|
"Failed to localize object URL to user-accessible format",
|
||||||
|
zap.String("url", url.String()),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.NewPointer(convertedUrl.String()), &formData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3ServiceImpl) CreateAvatarUrl() (*string, *map[string]string, error) {
|
||||||
|
return s.genUrl(s.avatarPolicy, "avatar-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3ServiceImpl) CreateImageUrl() (*string, *map[string]string, error) {
|
||||||
|
return s.genUrl(s.imagePolicy, "image-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3ServiceImpl) SaveUpload(uploadID string, bucketAlias string) (*string, error) {
|
||||||
|
sourceBucket := minioclient.Buckets["uploads"]
|
||||||
|
bucket := minioclient.Buckets[bucketAlias]
|
||||||
|
newObjectKey := uuid.New().String()
|
||||||
|
|
||||||
|
_, err := s.minio.StatObject(context.Background(), sourceBucket, uploadID, minio.StatObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if minio.ToErrorResponse(err).Code == minio.NoSuchKey {
|
||||||
|
return nil, errs.ErrFileNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = s.minio.CopyObject(context.Background(), minio.CopyDestOptions{
|
||||||
|
Bucket: bucket,
|
||||||
|
Object: newObjectKey,
|
||||||
|
}, minio.CopySrcOptions{
|
||||||
|
Bucket: sourceBucket,
|
||||||
|
Object: uploadID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error(
|
||||||
|
"Failed to copy object to new bucket",
|
||||||
|
zap.String("sourceBucket", sourceBucket),
|
||||||
|
zap.String("uploadID", uploadID),
|
||||||
|
zap.String("destinationBucket", bucket),
|
||||||
|
zap.String("newObjectKey", newObjectKey),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.minio.RemoveObject(context.Background(), sourceBucket, uploadID, minio.RemoveObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error(
|
||||||
|
"Failed to remove original object from uploads bucket",
|
||||||
|
zap.String("sourceBucket", sourceBucket),
|
||||||
|
zap.String("uploadID", uploadID),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &newObjectKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3ServiceImpl) GetLocalizedFileUrl(key string, bucketAlias string) url.URL {
|
||||||
|
cfg := config.GetConfig()
|
||||||
|
|
||||||
|
return url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: fmt.Sprintf("%s:%d", cfg.Hostname, cfg.Port),
|
||||||
|
Path: fmt.Sprintf("/s3/%s/%s", minioclient.Buckets[bucketAlias], key),
|
||||||
|
}
|
||||||
|
}
|
||||||
32
backend/internal/services/setup.go
Normal file
32
backend/internal/services/setup.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 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 (
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Module = fx.Module("services",
|
||||||
|
fx.Provide(
|
||||||
|
NewS3Service,
|
||||||
|
NewSmtpService,
|
||||||
|
NewAuthService,
|
||||||
|
NewProfileService,
|
||||||
|
NewWishListService,
|
||||||
|
),
|
||||||
|
)
|
||||||
131
backend/internal/services/smtp.go
Normal file
131
backend/internal/services/smtp.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// 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 (
|
||||||
|
"crypto/tls"
|
||||||
|
"easywish/config"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/smtp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
errs "easywish/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SmtpService interface {
|
||||||
|
SendEmail(to string, subject, body string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type smtpServiceImpl struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSmtpService() SmtpService {
|
||||||
|
return &smtpServiceImpl{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *smtpServiceImpl) SendEmail(to string, subject, body string) error {
|
||||||
|
cfg := config.GetConfig()
|
||||||
|
|
||||||
|
if !cfg.SmtpEnabled {
|
||||||
|
return errs.ErrSmtpDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.SmtpServer == "" || cfg.SmtpPort == 0 || cfg.SmtpFrom == "" {
|
||||||
|
return errs.ErrSmtpMissingConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
toSlice := []string{to}
|
||||||
|
|
||||||
|
headers := map[string]string{
|
||||||
|
"From": cfg.SmtpFrom,
|
||||||
|
"To": strings.Join(toSlice, ", "),
|
||||||
|
"Subject": subject,
|
||||||
|
"MIME-Version": "1.0",
|
||||||
|
"Content-Type": "text/html; charset=UTF-8",
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
for k, v := range headers {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
|
||||||
|
}
|
||||||
|
sb.WriteString("\r\n" + body)
|
||||||
|
message := []byte(sb.String())
|
||||||
|
|
||||||
|
hostPort := net.JoinHostPort(cfg.SmtpServer, fmt.Sprintf("%d", cfg.SmtpPort))
|
||||||
|
var conn net.Conn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if cfg.SmtpUseSSL {
|
||||||
|
tlsConfig := &tls.Config{ServerName: cfg.SmtpServer}
|
||||||
|
conn, err = tls.Dial("tcp", hostPort, tlsConfig)
|
||||||
|
} else {
|
||||||
|
timeout := time.Duration(cfg.SmtpTimeout) * time.Second
|
||||||
|
conn, err = net.DialTimeout("tcp", hostPort, timeout)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client, err := smtp.NewClient(conn, cfg.SmtpServer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if !cfg.SmtpUseSSL && cfg.SmtpUseTLS {
|
||||||
|
tlsConfig := &tls.Config{ServerName: cfg.SmtpServer}
|
||||||
|
if err = client.StartTLS(tlsConfig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate if credentials exist
|
||||||
|
if cfg.SmtpUser != "" && cfg.SmtpPassword != "" {
|
||||||
|
auth := smtp.PlainAuth("", cfg.SmtpUser, cfg.SmtpPassword, cfg.SmtpServer)
|
||||||
|
if err = client.Auth(auth); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = client.Mail(cfg.SmtpFrom); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, recipient := range toSlice {
|
||||||
|
if err = client.Rcpt(recipient); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send email body
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = w.Write(message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Quit()
|
||||||
|
}
|
||||||
228
backend/internal/services/wishlist.go
Normal file
228
backend/internal/services/wishlist.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
// 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 (
|
||||||
|
"easywish/internal/database"
|
||||||
|
"easywish/internal/dto"
|
||||||
|
errs "easywish/internal/errors"
|
||||||
|
"easywish/internal/utils"
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
|
mapspecial "easywish/internal/utils/mapSpecial"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wishListServiceImpl struct {
|
||||||
|
log *zap.Logger
|
||||||
|
dbctx database.DbContext
|
||||||
|
redis *redis.Client
|
||||||
|
s3 S3Service
|
||||||
|
}
|
||||||
|
|
||||||
|
type WishListService interface {
|
||||||
|
CreateWishList(cinfo dto.ClientInfo, object dto.NewWishListDto) (*dto.WishListDto, error)
|
||||||
|
UpdateWishListByGuid(cinfo dto.ClientInfo, guid string, object dto.NewWishDto) (bool, error)
|
||||||
|
DeleteWishListByGuid(cinfo dto.ClientInfo, guid string) (bool, error)
|
||||||
|
GetWishListByGuid(cinfo dto.ClientInfo, guid string) (*dto.WishListDto, error)
|
||||||
|
GetUserWishListsPaginated(cinfo dto.ClientInfo, amount int, page int, sorting enums.Sorting, sortOrder enums.SortOrder) (*[]dto.WishListDto, error)
|
||||||
|
|
||||||
|
CreateWish(cinfo dto.ClientInfo, object dto.NewWishDto) (*dto.WishDto, error)
|
||||||
|
UpdateWish(cinfo dto.ClientInfo, guid string, object dto.NewWishDto) (bool, error)
|
||||||
|
MoveWishToWishList(cinfo dto.ClientInfo, wishGuid string, wishListGuid string) (bool, error)
|
||||||
|
GetWishByGuid(cinfo dto.ClientInfo, guid string) (*dto.WishDto, error)
|
||||||
|
GetWishesByWishListGuidPaginated(cinfo dto.ClientInfo, guid string, amount int, page int, sorting enums.Sorting, sortOrder enums.SortOrder) (*[]dto.WishDto, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWishListService(_log *zap.Logger, _dbctx database.DbContext, _redis *redis.Client, _s3 S3Service) WishListService {
|
||||||
|
return wishListServiceImpl{
|
||||||
|
log: _log,
|
||||||
|
dbctx: _dbctx,
|
||||||
|
redis: _redis,
|
||||||
|
s3: _s3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) CreateWish(cinfo dto.ClientInfo, object dto.NewWishDto) (*dto.WishDto, error) {
|
||||||
|
helper, db, err := database.NewDbHelperTransaction(w.dbctx); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to open transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
defer helper.Rollback()
|
||||||
|
|
||||||
|
// Check if wish list exists
|
||||||
|
wishList, err := db.TXQueries.GetWishlistByGuid(db.CTX, object.WishListGuid); if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
w.log.Warn(
|
||||||
|
"Attempted to create a wish for a wish list that does not exist",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", object.WishListGuid),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to get wishlist for the new wish",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", object.WishListGuid),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
wishListOwnerUser, err := db.TXQueries.GetWishListOwnerByGuid(db.CTX, wishList.Guid.String()); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to get wish list owner",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", wishList.Guid.String()),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
if wishListOwnerUser.Username != cinfo.Username {
|
||||||
|
w.log.Warn(
|
||||||
|
"Attempt to create wish in a wish list the user does not own",
|
||||||
|
zap.String("owner_username", wishListOwnerUser.Username),
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", object.WishListGuid))
|
||||||
|
|
||||||
|
// As usual, we will pretend that it does not exist
|
||||||
|
return nil, errs.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarUrl *string
|
||||||
|
if object.PictureUploadID != "" {
|
||||||
|
key, err := w.s3.SaveUpload(object.PictureUploadID, "images"); if err != nil {
|
||||||
|
if errors.Is(err, errs.ErrFileNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to save image",
|
||||||
|
zap.String("upload_id", object.PictureUploadID),
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
urlObj := w.s3.GetLocalizedFileUrl(*key, "images")
|
||||||
|
avatarUrl = utils.NewPointer(urlObj.String())
|
||||||
|
} else {
|
||||||
|
avatarUrl = utils.NewPointer("")
|
||||||
|
}
|
||||||
|
|
||||||
|
newWish, err := db.TXQueries.CreateWish(db.CTX, database.CreateWishParams{
|
||||||
|
WishListGuid: object.WishListGuid,
|
||||||
|
Name: object.Name,
|
||||||
|
Description: object.Description,
|
||||||
|
PictureUrl: *avatarUrl,
|
||||||
|
Stars: int16(object.Stars),
|
||||||
|
}); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to create a new wish",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", object.WishListGuid),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
err = helper.Commit(); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to commit transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
wishDto := &dto.WishDto{}
|
||||||
|
mapspecial.MapWishDto(newWish, wishDto)
|
||||||
|
return wishDto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) CreateWishList(cinfo dto.ClientInfo, object dto.NewWishListDto) (*dto.WishListDto, error) {
|
||||||
|
helper, db, err := database.NewDbHelperTransaction(w.dbctx); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to open transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
defer helper.Rollback()
|
||||||
|
|
||||||
|
createdWishList, err := db.TXQueries.CreateWishList(db.CTX, database.CreateWishListParams{
|
||||||
|
Username: cinfo.Username,
|
||||||
|
Hidden: object.Hidden,
|
||||||
|
Name: object.Name,
|
||||||
|
IconName: object.IconName,
|
||||||
|
Color: object.Color,
|
||||||
|
ColorGrad: object.ColorGrad,
|
||||||
|
}); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to create wish list",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
err = helper.Commit(); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to commit transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
wishListDto := &dto.WishListDto{}
|
||||||
|
mapspecial.MapWishListDto(createdWishList, wishListDto)
|
||||||
|
|
||||||
|
return wishListDto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) DeleteWishListByGuid(cinfo dto.ClientInfo, guid string) (bool, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) GetUserWishListsPaginated(cinfo dto.ClientInfo, amount int, page int, sorting enums.Sorting, sortOrder enums.SortOrder) (*[]dto.WishListDto, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) GetWishByGuid(cinfo dto.ClientInfo, guid string) (*dto.WishDto, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) GetWishListByGuid(cinfo dto.ClientInfo, guid string) (*dto.WishListDto, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) GetWishesByWishListGuidPaginated(cinfo dto.ClientInfo, guid string, amount int, page int, sorting enums.Sorting, sortOrder enums.SortOrder) (*[]dto.WishDto, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) MoveWishToWishList(cinfo dto.ClientInfo, wishGuid string, wishListGuid string) (bool, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) UpdateWish(cinfo dto.ClientInfo, guid string, object dto.NewWishDto) (bool, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) UpdateWishListByGuid(cinfo dto.ClientInfo, guid string, object dto.NewWishDto) (bool, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
37
backend/internal/utils/codes.go
Normal file
37
backend/internal/utils/codes.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 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"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateSecure6DigitNumber() (string, error) {
|
||||||
|
maxNumber := 1000000
|
||||||
|
b := make([]byte, 4)
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read random bytes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
num := int(uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])) % maxNumber
|
||||||
|
|
||||||
|
return fmt.Sprintf("%06d", num), nil
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"easywish/config"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetDbConn() (*pgx.Conn, context.Context, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
conn, err := pgx.Connect(ctx, config.GetConfig().DatabaseUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, ctx, nil
|
|
||||||
}
|
|
||||||
49
backend/internal/utils/enums/enums.go
Normal file
49
backend/internal/utils/enums/enums.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// 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
|
||||||
|
)
|
||||||
|
|
||||||
|
type Role int32
|
||||||
|
const (
|
||||||
|
GuestRole Role = iota
|
||||||
|
UserRole
|
||||||
|
AdminRole
|
||||||
|
)
|
||||||
|
|
||||||
|
type JwtTokenType int32
|
||||||
|
const (
|
||||||
|
JwtAccessTokenType JwtTokenType = iota
|
||||||
|
JwtRefreshTokenType
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sorting int32
|
||||||
|
const (
|
||||||
|
ByDate Sorting = iota
|
||||||
|
ByScore
|
||||||
|
)
|
||||||
|
|
||||||
|
type SortOrder int32
|
||||||
|
const (
|
||||||
|
Descending = iota
|
||||||
|
Ascending
|
||||||
|
)
|
||||||
29
backend/internal/utils/errors.go
Normal file
29
backend/internal/utils/errors.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// 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 "errors"
|
||||||
|
|
||||||
|
func ErrorIsOneOf(err error, ignoreErrors ...error) bool {
|
||||||
|
for _, ignore := range ignoreErrors {
|
||||||
|
if errors.Is(err, ignore) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
31
backend/internal/utils/hash.go
Normal file
31
backend/internal/utils/hash.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// 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 "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPasswordHash(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@@ -1,24 +1,48 @@
|
|||||||
|
// 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 (
|
||||||
"easywish/config"
|
"easywish/config"
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateTokens(username string) (accessToken, refreshToken string, err error) {
|
func GenerateTokens(username string, sessionGuid string, role enums.Role) (accessToken, refreshToken string, err error) {
|
||||||
cfg := config.GetConfig()
|
cfg := config.GetConfig()
|
||||||
|
|
||||||
accessClaims := jwt.MapClaims{
|
accessClaims := jwt.MapClaims{
|
||||||
"username": username,
|
"username": username,
|
||||||
"exp": time.Now().Add(time.Minute * time.Duration(cfg.JwtExpAccess)).Unix(),
|
"role": role,
|
||||||
|
"session": sessionGuid,
|
||||||
|
"type": enums.JwtAccessTokenType,
|
||||||
|
"exp": time.Now().Add(time.Minute * time.Duration(cfg.JwtExpAccess)).Unix(),
|
||||||
}
|
}
|
||||||
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString([]byte(cfg.JwtSecret))
|
accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString([]byte(cfg.JwtSecret))
|
||||||
|
|
||||||
refreshClaims := jwt.MapClaims{
|
refreshClaims := jwt.MapClaims{
|
||||||
"username": username,
|
"username": username,
|
||||||
"exp": time.Now().Add(time.Hour * time.Duration(cfg.JwtExpRefresh)).Unix(),
|
"role": role,
|
||||||
|
"session": sessionGuid,
|
||||||
|
"type": enums.JwtRefreshTokenType,
|
||||||
|
"exp": time.Now().Add(time.Hour * time.Duration(cfg.JwtExpRefresh)).Unix(),
|
||||||
}
|
}
|
||||||
refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString([]byte(cfg.JwtSecret))
|
refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString([]byte(cfg.JwtSecret))
|
||||||
|
|
||||||
|
|||||||
34
backend/internal/utils/mapSpecial/profileDto.go
Normal file
34
backend/internal/utils/mapSpecial/profileDto.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
|
//
|
||||||
|
// This file is part of Easywish
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||||
|
// the GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package mapspecial
|
||||||
|
|
||||||
|
import (
|
||||||
|
"easywish/internal/database"
|
||||||
|
"easywish/internal/dto"
|
||||||
|
|
||||||
|
"github.com/rafiulgits/go-automapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MapProfileDto(dbModel database.Profile, dtoModel *dto.ProfileDto) {
|
||||||
|
if dtoModel == nil {
|
||||||
|
dtoModel = &dto.ProfileDto{}
|
||||||
|
}
|
||||||
|
automapper.Map(&dbModel, dtoModel, func(src *database.Profile, dst *dto.ProfileDto) {
|
||||||
|
dst.Birthday = src.Birthday.Time.UnixMilli()
|
||||||
|
})
|
||||||
|
}
|
||||||
37
backend/internal/utils/mapSpecial/wishDto.go
Normal file
37
backend/internal/utils/mapSpecial/wishDto.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 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 mapspecial
|
||||||
|
|
||||||
|
import (
|
||||||
|
"easywish/internal/database"
|
||||||
|
"easywish/internal/dto"
|
||||||
|
|
||||||
|
"github.com/rafiulgits/go-automapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MapWishDto(dbModel database.Wish, dtoModel *dto.WishDto) {
|
||||||
|
if dtoModel == nil {
|
||||||
|
dtoModel = &dto.WishDto{}
|
||||||
|
}
|
||||||
|
automapper.Map(&dbModel, dtoModel, func(src *database.Wish, dst *dto.WishDto) {
|
||||||
|
dst.Guid = src.Guid.String()
|
||||||
|
dst.WishListGuid = src.WishListGuid.String()
|
||||||
|
dst.CreationDate = src.CreationDate.Time.UnixMilli()
|
||||||
|
dst.FulfilledDate = src.FulfilledDate.Time.UnixMilli()
|
||||||
|
})
|
||||||
|
}
|
||||||
35
backend/internal/utils/mapSpecial/wishListDto.go
Normal file
35
backend/internal/utils/mapSpecial/wishListDto.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// 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 mapspecial
|
||||||
|
|
||||||
|
import (
|
||||||
|
"easywish/internal/database"
|
||||||
|
"easywish/internal/dto"
|
||||||
|
|
||||||
|
"github.com/rafiulgits/go-automapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MapWishListDto(dbModel database.WishList, dtoModel *dto.WishListDto) {
|
||||||
|
if dtoModel == nil {
|
||||||
|
dtoModel = &dto.WishListDto{}
|
||||||
|
}
|
||||||
|
automapper.Map(&dbModel, dtoModel, func(src *database.WishList, dst *dto.WishListDto) {
|
||||||
|
dst.Guid = src.Guid.String()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
45
backend/internal/utils/minio.go
Normal file
45
backend/internal/utils/minio.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// 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 (
|
||||||
|
"easywish/config"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Move this method to s3 service
|
||||||
|
func LocalizeS3Url(originalURL string) (*url.URL, error) {
|
||||||
|
|
||||||
|
cfg := config.GetConfig()
|
||||||
|
newDomain := fmt.Sprintf("%s:%d", cfg.Hostname, cfg.Port)
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(originalURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newURL := url.URL{
|
||||||
|
Scheme: parsedURL.Scheme,
|
||||||
|
Host: newDomain,
|
||||||
|
Path: "/s3" + parsedURL.Path,
|
||||||
|
RawQuery: parsedURL.RawQuery,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &newURL, nil
|
||||||
|
}
|
||||||
22
backend/internal/utils/pointer.go
Normal file
22
backend/internal/utils/pointer.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
func NewPointer[T any](d T) *T {
|
||||||
|
return &d
|
||||||
|
}
|
||||||
155
backend/internal/validation/custom.go
Normal file
155
backend/internal/validation/custom.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
// 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"easywish/config"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomValidatorHandler struct {
|
||||||
|
Function func(fl validator.FieldLevel) bool
|
||||||
|
FieldName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCustomHandlers() []CustomValidatorHandler {
|
||||||
|
|
||||||
|
handlers := []CustomValidatorHandler{
|
||||||
|
|
||||||
|
{
|
||||||
|
FieldName: "username",
|
||||||
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
username := fl.Field().String()
|
||||||
|
return regexp.MustCompile(`^[a-zA-Z0-9_]{3,20}$`).MatchString(username)
|
||||||
|
}},
|
||||||
|
|
||||||
|
{
|
||||||
|
FieldName: "guid",
|
||||||
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
guid := fl.Field().String()
|
||||||
|
return regexp.MustCompile(`^([{(]?([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})[})]?)$`).MatchString(guid)
|
||||||
|
}},
|
||||||
|
|
||||||
|
{
|
||||||
|
FieldName: "name",
|
||||||
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
username := fl.Field().String()
|
||||||
|
return regexp.MustCompile(`^.{1,75}$`).MatchString(username)
|
||||||
|
}},
|
||||||
|
|
||||||
|
{
|
||||||
|
FieldName: "bio",
|
||||||
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
username := fl.Field().String()
|
||||||
|
return regexp.MustCompile(`^.{1,512}$`).MatchString(username)
|
||||||
|
}},
|
||||||
|
|
||||||
|
{
|
||||||
|
FieldName: "birthday_unix_milli",
|
||||||
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
|
||||||
|
timestamp := fl.Field().Int()
|
||||||
|
date := time.UnixMilli(timestamp)
|
||||||
|
currentDate := time.Now()
|
||||||
|
|
||||||
|
age := currentDate.Year() - date.Year()
|
||||||
|
if currentDate.YearDay() < date.YearDay() {
|
||||||
|
age--
|
||||||
|
}
|
||||||
|
|
||||||
|
return age >= 0 && age <= 122
|
||||||
|
}},
|
||||||
|
|
||||||
|
{
|
||||||
|
FieldName: "color_hex",
|
||||||
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
username := fl.Field().String()
|
||||||
|
return regexp.MustCompile(`^#[0-9a-f]{6,6}$`).MatchString(username)
|
||||||
|
}},
|
||||||
|
|
||||||
|
{
|
||||||
|
FieldName: "password",
|
||||||
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
password := fl.Field().String()
|
||||||
|
cfg := config.GetConfig()
|
||||||
|
|
||||||
|
if cfg.PasswordMaxLength < len(password) || len(password) < cfg.PasswordMinLength {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.PasswordCheckNumbers && !regexp.MustCompile(`\d+`).MatchString(password) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.PasswordCheckCases && !regexp.MustCompile(`^(?=.*[a-z])(?=.*[A-Z]).*$`).MatchString(password) ||
|
||||||
|
cfg.PasswordCheckCharacters && !regexp.MustCompile(`[a-zA-Z]`).MatchString(password) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.PasswordCheckSymbols && !regexp.MustCompile(`[.,/;'[\]\-=_+{}:"<>?\/|!@#$%^&*()~]`).MatchString(password) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.PasswordCheckLeaked {
|
||||||
|
// TODO: implement rockme check
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}},
|
||||||
|
|
||||||
|
{
|
||||||
|
FieldName: "verification_code",
|
||||||
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
codeType := fl.Param()
|
||||||
|
code := fl.Field().String()
|
||||||
|
|
||||||
|
if codeType == "reg" {
|
||||||
|
return regexp.MustCompile(`[\d]{6,6}`).MatchString(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if codeType == "reset" {
|
||||||
|
return regexp.MustCompile(
|
||||||
|
`^[{(]?([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})[})]?$`,
|
||||||
|
).MatchString(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(fmt.Sprintf("'%s' is not a valid verification code type", codeType))
|
||||||
|
}},
|
||||||
|
|
||||||
|
{
|
||||||
|
FieldName: "upload_id",
|
||||||
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
uploadType := fl.Param()
|
||||||
|
uploadID := fl.Field().String()
|
||||||
|
|
||||||
|
pattern := fmt.Sprintf(
|
||||||
|
"^%s-([{(]?([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})[})]?)$",
|
||||||
|
uploadType,
|
||||||
|
)
|
||||||
|
|
||||||
|
return regexp.MustCompile(pattern).MatchString(uploadID)
|
||||||
|
}},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlers
|
||||||
|
}
|
||||||
28
backend/internal/validation/setup.go
Normal file
28
backend/internal/validation/setup.go
Normal file
@@ -0,0 +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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/fx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Module = fx.Module("validation",
|
||||||
|
fx.Provide(
|
||||||
|
NewValidator,
|
||||||
|
),
|
||||||
|
)
|
||||||
32
backend/internal/validation/validator.go
Normal file
32
backend/internal/validation/validator.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 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 validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewValidator() *validator.Validate {
|
||||||
|
v := validator.New()
|
||||||
|
|
||||||
|
for _, handler := range GetCustomHandlers() {
|
||||||
|
v.RegisterValidation(handler.FieldName, handler.Function)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
131
dev-compose.yml
Normal file
131
dev-compose.yml
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build: ./backend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/usr/bin/curl", "-f", "http://localhost:8080/api/service/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
environment:
|
||||||
|
ENVIRONMENT: "development"
|
||||||
|
HOSTNAME: ${HOSTNAME}
|
||||||
|
PORT: ${PORT}
|
||||||
|
POSTGRES_URL: ${POSTGRES_URL}
|
||||||
|
REDIS_URL: ${REDIS_URL}
|
||||||
|
MINIO_URL: ${MINIO_URL}
|
||||||
|
MINIO_HOST: ${MINIO_HOST}
|
||||||
|
MINIO_PORT: ${MINIO_PORT}
|
||||||
|
JWT_ALGORITHM: ${JWT_ALGORITHM}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
JWT_ISSUER: ${JWT_ISSUER}
|
||||||
|
JWT_AUDIENCE: ${JWT_AUDIENCE}
|
||||||
|
JWT_EXP_ACCESS: ${JWT_EXP_ACCESS}
|
||||||
|
JWT_EXP_REFRESH: ${JWT_EXP_REFRESH}
|
||||||
|
SMTP_ENABLED: ${SMTP_ENABLED}
|
||||||
|
SMTP_SERVER: ${SMTP_SERVER}
|
||||||
|
SMTP_PORT: ${SMTP_PORT}
|
||||||
|
SMTP_USER: ${SMTP_USER}
|
||||||
|
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||||
|
SMTP_FROM: ${SMTP_FROM}
|
||||||
|
SMTP_USE_TLS: ${SMTP_USE_TLS}
|
||||||
|
SMTP_USE_SSL: ${SMTP_USE_SSL}
|
||||||
|
SMTP_TIMEOUT: ${SMTP_TIMEOUT}
|
||||||
|
PASSWORD_MIN_LENGTH: ${PASSWORD_MIN_LENGTH}
|
||||||
|
PASSWORD_MAX_LENGTH: ${PASSWORD_MAX_LENGTH}
|
||||||
|
PASSWORD_CHECK_NUMBER: ${PASSWORD_CHECK_NUMBER}
|
||||||
|
PASSWORD_CHECK_CHARAC: ${PASSWORD_CHECK_CHARAC}
|
||||||
|
PASSWORD_CHECK_CASES: ${PASSWORD_CHECK_CASES}
|
||||||
|
PASSWORD_CHECK_SYMBOL: ${PASSWORD_CHECK_SYMBOL}
|
||||||
|
PASSWORD_CHECK_LEAKED: ${PASSWORD_CHECK_LEAKED}
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
networks:
|
||||||
|
- easywish-network
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build: ./frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- easywish-network
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio
|
||||||
|
command: server /data --console-address ":9001" --ftp="address=:8021" --ftp="passive-port-range=30000-40000"
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "curl", "-I", "localhost:9000/minio/health/live" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
- "8021:8021"
|
||||||
|
networks:
|
||||||
|
- easywish-network
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:latest
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "pg_isready", "-q", "-d", "${POSTGRES_DB}", "-U", "${POSTGRES_USER}" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- easywish-network
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./sqlc/schema.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: eqalpha/keydb
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
command: ["keydb-server", "--requirepass", "${REDIS_PASSWORD}"]
|
||||||
|
environment:
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
networks:
|
||||||
|
- easywish-network
|
||||||
|
|
||||||
|
test-mailserver:
|
||||||
|
image: rnwood/smtp4dev
|
||||||
|
hostname: easywish.weirdcatland
|
||||||
|
ports:
|
||||||
|
- "25:25"
|
||||||
|
- "5000:80"
|
||||||
|
networks:
|
||||||
|
- easywish-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
minio_data:
|
||||||
|
networks:
|
||||||
|
easywish-network:
|
||||||
|
driver: bridge
|
||||||
@@ -16,9 +16,36 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
environment:
|
environment:
|
||||||
|
ENVIRONMENT: ${ENVIRONMENT}
|
||||||
|
HOSTNAME: ${HOSTNAME}
|
||||||
|
PORT: ${PORT}
|
||||||
POSTGRES_URL: ${POSTGRES_URL}
|
POSTGRES_URL: ${POSTGRES_URL}
|
||||||
REDIS_URL: ${REDIS_URL}
|
REDIS_URL: ${REDIS_URL}
|
||||||
MINIO_URL: ${MINIO_URL}
|
MINIO_URL: ${MINIO_URL}
|
||||||
|
MINIO_HOST: ${MINIO_HOST}
|
||||||
|
MINIO_PORT: ${MINIO_PORT}
|
||||||
|
JWT_ALGORITHM: ${JWT_ALGORITHM}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
JWT_ISSUER: ${JWT_ISSUER}
|
||||||
|
JWT_AUDIENCE: ${JWT_AUDIENCE}
|
||||||
|
JWT_EXP_ACCESS: ${JWT_EXP_ACCESS}
|
||||||
|
JWT_EXP_REFRESH: ${JWT_EXP_REFRESH}
|
||||||
|
SMTP_ENABLED: ${SMTP_ENABLED}
|
||||||
|
SMTP_SERVER: ${SMTP_SERVER}
|
||||||
|
SMTP_PORT: ${SMTP_PORT}
|
||||||
|
SMTP_USER: ${SMTP_USER}
|
||||||
|
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||||
|
SMTP_FROM: ${SMTP_FROM}
|
||||||
|
SMTP_USE_TLS: ${SMTP_USE_TLS}
|
||||||
|
SMTP_USE_SSL: ${SMTP_USE_SSL}
|
||||||
|
SMTP_TIMEOUT: ${SMTP_TIMEOUT}
|
||||||
|
PASSWORD_MIN_LENGTH: ${PASSWORD_MIN_LENGTH}
|
||||||
|
PASSWORD_MAX_LENGTH: ${PASSWORD_MAX_LENGTH}
|
||||||
|
PASSWORD_CHECK_NUMBER: ${PASSWORD_CHECK_NUMBER}
|
||||||
|
PASSWORD_CHECK_CHARAC: ${PASSWORD_CHECK_CHARAC}
|
||||||
|
PASSWORD_CHECK_CASES: ${PASSWORD_CHECK_CASES}
|
||||||
|
PASSWORD_CHECK_SYMBOL: ${PASSWORD_CHECK_SYMBOL}
|
||||||
|
PASSWORD_CHECK_LEAKED: ${PASSWORD_CHECK_LEAKED}
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
411
sqlc/query.sql
411
sqlc/query.sql
@@ -1,5 +1,22 @@
|
|||||||
-- vim:fileencoding=utf-8:foldmethod=marker
|
-- vim:fileencoding=utf-8:foldmethod=marker
|
||||||
|
|
||||||
|
-- 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/>.
|
||||||
|
|
||||||
--: User Object {{{
|
--: User Object {{{
|
||||||
|
|
||||||
;-- name: CreateUser :one
|
;-- name: CreateUser :one
|
||||||
@@ -8,12 +25,16 @@ VALUES ($1, false) RETURNING *;
|
|||||||
|
|
||||||
;-- name: UpdateUser :exec
|
;-- name: UpdateUser :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET verified = $2, deleted = $3
|
SET
|
||||||
|
verified = COALESCE($2, verified),
|
||||||
|
deleted = COALESCE($3, deleted)
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
;-- name: UpdateUserByUsername :exec
|
;-- name: UpdateUserByUsername :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET verified = $2, deleted = $3
|
SET
|
||||||
|
verified = COALESCE($2, verified),
|
||||||
|
deleted = COALESCE($3, deleted)
|
||||||
WHERE username = $1;
|
WHERE username = $1;
|
||||||
|
|
||||||
;-- name: DeleteUser :exec
|
;-- name: DeleteUser :exec
|
||||||
@@ -32,22 +53,71 @@ WHERE id = $1;
|
|||||||
SELECT * FROM users
|
SELECT * FROM users
|
||||||
WHERE username = $1;
|
WHERE username = $1;
|
||||||
|
|
||||||
;-- name: GetUserByLoginCredentials :one
|
;-- name: GetUserByEmail :one
|
||||||
|
SELECT users.* FROM users
|
||||||
|
JOIN login_informations linfo ON linfo.user_id = users.id
|
||||||
|
WHERE linfo.email = @email::text;
|
||||||
|
|
||||||
|
;-- name: GetValidUserByLoginCredentials :one
|
||||||
SELECT
|
SELECT
|
||||||
users.id,
|
users.*,
|
||||||
users.username,
|
|
||||||
linfo.password_hash,
|
linfo.password_hash,
|
||||||
linfo.totp_encrypted
|
linfo.totp_encrypted
|
||||||
FROM users
|
FROM users
|
||||||
JOIN login_informations AS linfo ON users.id = linfo.user_id
|
JOIN login_informations AS linfo ON users.id = linfo.user_id
|
||||||
LEFT JOIN banned_users AS banned ON users.id = banned.user_id
|
|
||||||
WHERE
|
WHERE
|
||||||
users.username = $1 AND
|
users.username = $1 AND
|
||||||
users.verified IS TRUE AND -- Verified
|
users.verified IS TRUE AND -- Verified
|
||||||
users.deleted IS FALSE AND -- Not deleted
|
users.deleted IS FALSE AND -- Not deleted
|
||||||
banned.user_id IS NULL AND -- Not banned
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM banned_users
|
||||||
|
WHERE user_id = users.id AND
|
||||||
|
pardoned IS FALSE AND
|
||||||
|
(expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
||||||
|
) AND -- Not banned
|
||||||
linfo.password_hash = crypt(@password::text, linfo.password_hash); -- Password hash matches
|
linfo.password_hash = crypt(@password::text, linfo.password_hash); -- Password hash matches
|
||||||
|
|
||||||
|
;-- name: CheckUserRegistrationAvailability :one
|
||||||
|
SELECT
|
||||||
|
COUNT(CASE WHEN users.username = @username::text THEN 1 END) > 0 AS username_busy,
|
||||||
|
COUNT(CASE WHEN linfo.email = @email::text THEN 1 END) > 0 AS email_busy
|
||||||
|
FROM users
|
||||||
|
JOIN login_informations AS linfo ON linfo.user_id = users.id
|
||||||
|
WHERE
|
||||||
|
(
|
||||||
|
users.username = @username::text OR
|
||||||
|
linfo.email = @email::text
|
||||||
|
)
|
||||||
|
AND
|
||||||
|
(
|
||||||
|
users.verified IS TRUE OR
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM confirmation_codes AS codes
|
||||||
|
WHERE codes.user_id = users.id
|
||||||
|
AND codes.code_type = 0
|
||||||
|
AND codes.deleted IS FALSE
|
||||||
|
AND codes.expires_at > CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
;-- name: DeleteUnverifiedAccountsHavingUsernameOrEmail :one
|
||||||
|
WITH deleted_rows AS (
|
||||||
|
DELETE FROM users
|
||||||
|
WHERE
|
||||||
|
(username = @username::text OR
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM login_informations AS linfo
|
||||||
|
WHERE linfo.user_id = users.id
|
||||||
|
AND linfo.email = @email::text
|
||||||
|
))
|
||||||
|
AND verified IS FALSE
|
||||||
|
RETURNING *
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) AS deleted_count FROM deleted_rows;
|
||||||
|
|
||||||
--: }}}
|
--: }}}
|
||||||
|
|
||||||
--: Banned User Object {{{
|
--: Banned User Object {{{
|
||||||
@@ -58,7 +128,12 @@ VALUES ( $1, $2, $3, $4) RETURNING *;
|
|||||||
|
|
||||||
;-- name: UpdateBannedUser :exec
|
;-- name: UpdateBannedUser :exec
|
||||||
UPDATE banned_users
|
UPDATE banned_users
|
||||||
SET reason = $2, expires_at = $3, banned_by = $4, pardoned = $5, pardoned_by = $6
|
SET
|
||||||
|
reason = COALESCE($2, reason),
|
||||||
|
expires_at = COALESCE($3, expires_at),
|
||||||
|
banned_by = COALESCE($4, banned_by),
|
||||||
|
pardoned = COALESCE($5, pardoned),
|
||||||
|
pardoned_by = COALESCE($6, pardoned_by)
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
;-- name: GetUserBans :many
|
;-- name: GetUserBans :many
|
||||||
@@ -76,11 +151,16 @@ WHERE users.username = $1;
|
|||||||
|
|
||||||
;-- name: CreateLoginInformation :one
|
;-- name: CreateLoginInformation :one
|
||||||
INSERT INTO login_informations(user_id, email, password_hash)
|
INSERT INTO login_informations(user_id, email, password_hash)
|
||||||
VALUES ( $1, $2, crypt(@password::text, gen_salt('bf')) ) RETURNING *;
|
VALUES ( $1, $2, @password_hash::text ) RETURNING *;
|
||||||
|
|
||||||
;-- name: UpdateLoginInformationByUsername :exec
|
;-- name: UpdateLoginInformationByUsername :exec
|
||||||
UPDATE login_informations
|
UPDATE login_informations
|
||||||
SET email = $2, password_hash = crypt(@password::text, gen_salt('bf')), totp_encrypted = $4, email_2fa_enabled = $5, password_change_date = $6
|
SET
|
||||||
|
email = COALESCE($2, email),
|
||||||
|
password_hash = COALESCE(@password_hash::text, password_hash),
|
||||||
|
totp_encrypted = COALESCE($4, totp_encrypted),
|
||||||
|
email_2fa_enabled = COALESCE($5, email_2fa_enabled),
|
||||||
|
password_change_date = COALESCE($6, password_change_date)
|
||||||
FROM users
|
FROM users
|
||||||
WHERE users.username = $1 AND login_informations.user_id = users.id;
|
WHERE users.username = $1 AND login_informations.user_id = users.id;
|
||||||
|
|
||||||
@@ -94,16 +174,32 @@ 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, @code_hash) RETURNING *;
|
||||||
|
|
||||||
;-- name: GetConfirmationCodeByCode :one
|
;-- name: GetValidConfirmationCodeByCode :one
|
||||||
SELECT * FROM confirmation_codes
|
SELECT * FROM confirmation_codes
|
||||||
WHERE user_id = $1 AND code_type = $2 AND expires_at > CURRENT_TIMESTAMP AND code_hash = crypt($3, code_hash);
|
WHERE
|
||||||
|
user_id = $1 AND
|
||||||
|
code_type = $2 AND
|
||||||
|
expires_at > CURRENT_TIMESTAMP AND
|
||||||
|
used IS FALSE AND
|
||||||
|
code_hash = crypt(@code::text, code_hash);
|
||||||
|
|
||||||
|
;-- name: GetValidConfirmationCodesByUsername :many
|
||||||
|
SELECT * FROM confirmation_codes
|
||||||
|
JOIN users on users.id = confirmation_codes.user_id
|
||||||
|
WHERE
|
||||||
|
users.username = @username::text AND
|
||||||
|
code_type = @code_type::integer AND
|
||||||
|
expires_at > CURRENT_TIMESTAMP AND
|
||||||
|
used IS FALSE;
|
||||||
|
|
||||||
;-- name: UpdateConfirmationCode :exec
|
;-- name: UpdateConfirmationCode :exec
|
||||||
UPDATE confirmation_codes
|
UPDATE confirmation_codes
|
||||||
SET used = $2, deleted = $3
|
SET
|
||||||
|
used = COALESCE($2, used),
|
||||||
|
deleted = COALESCE($3, deleted)
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
;-- name: PruneExpiredConfirmationCodes :exec
|
;-- name: PruneExpiredConfirmationCodes :exec
|
||||||
@@ -120,12 +216,40 @@ VALUES ($1, $2, $3, $4) RETURNING *;
|
|||||||
|
|
||||||
;-- name: UpdateSession :exec
|
;-- name: UpdateSession :exec
|
||||||
UPDATE sessions
|
UPDATE sessions
|
||||||
SET name = $2, platform = $3, latest_ip = $4, login_time = $5, last_seen_date = $6, terminated = $7
|
SET
|
||||||
|
name = COALESCE($2, name),
|
||||||
|
platform = COALESCE($3, platform),
|
||||||
|
latest_ip = COALESCE($4, latest_ip),
|
||||||
|
login_time = COALESCE($5, login_time),
|
||||||
|
last_refresh_exp_time = COALESCE($6, last_refresh_exp_time),
|
||||||
|
last_seen_date = COALESCE($7, last_seen_date),
|
||||||
|
terminated = COALESCE($8, terminated)
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
;-- name: GetUserSessions :many
|
;-- name: GetSessionByGuid :one
|
||||||
SELECT * FROM sessions
|
SELECT * FROM sessions
|
||||||
WHERE user_id = $1 AND terminated IS FALSE;
|
WHERE guid = (@guid::text)::uuid;
|
||||||
|
|
||||||
|
;-- name: GetValidUserSessions :many
|
||||||
|
SELECT * FROM sessions
|
||||||
|
WHERE
|
||||||
|
user_id = $1 AND terminated IS FALSE AND
|
||||||
|
last_refresh_exp_time > CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- name: GetUnexpiredTerminatedSessionsGuidsPaginated :many
|
||||||
|
SELECT guid FROM sessions
|
||||||
|
WHERE
|
||||||
|
terminated IS TRUE AND
|
||||||
|
last_refresh_exp_time > CURRENT_TIMESTAMP
|
||||||
|
LIMIT @batch_size::integer
|
||||||
|
OFFSET $2;
|
||||||
|
|
||||||
|
;-- name: TerminateAllSessionsForUserByUsername :many
|
||||||
|
UPDATE sessions
|
||||||
|
SET terminated = TRUE
|
||||||
|
FROM users
|
||||||
|
WHERE sessions.user_id = users.id AND users.username = @username::text
|
||||||
|
RETURNING sessions.guid;
|
||||||
|
|
||||||
;-- name: PruneTerminatedSessions :exec
|
;-- name: PruneTerminatedSessions :exec
|
||||||
DELETE FROM sessions
|
DELETE FROM sessions
|
||||||
@@ -141,7 +265,13 @@ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *;
|
|||||||
|
|
||||||
;-- name: UpdateProfileByUsername :exec
|
;-- name: UpdateProfileByUsername :exec
|
||||||
UPDATE profiles
|
UPDATE profiles
|
||||||
SET name = $2, bio = $3, birthday = $4, avatar_url = $5, color = $6, color_grad = $7
|
SET
|
||||||
|
name = COALESCE($2, name),
|
||||||
|
bio = COALESCE($3, bio),
|
||||||
|
birthday = COALESCE($4, birthday),
|
||||||
|
avatar_url = COALESCE(sqlc.narg('avatar_url'), avatar_url),
|
||||||
|
color = COALESCE($5, color),
|
||||||
|
color_grad = COALESCE($6, color_grad)
|
||||||
FROM users
|
FROM users
|
||||||
WHERE username = $1;
|
WHERE username = $1;
|
||||||
|
|
||||||
@@ -150,29 +280,39 @@ SELECT profiles.* FROM profiles
|
|||||||
JOIN users ON users.id = profiles.user_id
|
JOIN users ON users.id = profiles.user_id
|
||||||
WHERE users.username = $1;
|
WHERE users.username = $1;
|
||||||
|
|
||||||
;-- name: GetProfileByUsernameRestricted :one
|
;-- name: CheckProfileAccess :one
|
||||||
SELECT
|
SELECT
|
||||||
users.username,
|
CASE WHEN u.deleted OR NOT u.verified THEN TRUE ELSE FALSE END AS user_unavailable,
|
||||||
profiles.name,
|
CASE WHEN EXISTS (
|
||||||
CASE
|
SELECT 1
|
||||||
WHEN profile_settings.hide_birthday OR profile_settings.hide_profile_details THEN NULL
|
FROM banned_users
|
||||||
ELSE profiles.birthday
|
WHERE user_id = u.id AND
|
||||||
END AS birthday,
|
pardoned IS FALSE AND
|
||||||
CASE
|
(expires_at IS NULL OR expires_at < CURRENT_TIMESTAMP)
|
||||||
WHEN profile_settings.hide_profile_details THEN NULL
|
) THEN TRUE ELSE FALSE END AS user_banned,
|
||||||
ELSE profiles.bio
|
CASE WHEN ps.hide_profile_details THEN TRUE ELSE FALSE END AS hidden,
|
||||||
END AS bio,
|
CASE WHEN ps.hide_for_unauthenticated AND @requester::text = '' THEN TRUE ELSE FALSE END AS auth_required,
|
||||||
CASE
|
CASE WHEN ps.captcha THEN TRUE ELSE FALSE END AS captcha_required
|
||||||
WHEN profile_settings.hide_profile_details THEN NULL
|
FROM profiles p
|
||||||
ELSE profiles.avatar_url
|
JOIN profile_settings ps ON ps.profile_id = p.id
|
||||||
END AS avatar_url,
|
JOIN users u ON p.user_id = u.id
|
||||||
profiles.color,
|
WHERE p.id = $1;
|
||||||
profiles.color_grad,
|
|
||||||
profile_settings.hide_profile_details
|
;-- name: GetProfileByUsernameWithPrivacy :one
|
||||||
FROM profiles
|
SELECT
|
||||||
JOIN users ON users.id = profiles.user_id
|
u.username,
|
||||||
JOIN profile_settings ON profiles.id = profile_settings.profile_id
|
p.name,
|
||||||
WHERE users.username = $1 AND ($2 IS FALSE OR profile_settings.hide_for_unauthenticated IS FALSE);
|
p.bio,
|
||||||
|
p.avatar_url,
|
||||||
|
CASE WHEN ps.hide_birthday THEN NULL ELSE p.birthday END AS birthday,
|
||||||
|
p.color,
|
||||||
|
p.color_grad
|
||||||
|
FROM
|
||||||
|
users AS u
|
||||||
|
JOIN profiles AS p ON u.id = p.user_id
|
||||||
|
JOIN profile_settings AS ps ON p.id = ps.profile_id
|
||||||
|
WHERE
|
||||||
|
u.username = @searched_username::text;
|
||||||
|
|
||||||
;-- name: GetProfilesRestricted :many
|
;-- name: GetProfilesRestricted :many
|
||||||
SELECT
|
SELECT
|
||||||
@@ -200,17 +340,19 @@ LIMIT 20 OFFSET 20 * $1;
|
|||||||
INSERT INTO profile_settings(profile_id)
|
INSERT INTO profile_settings(profile_id)
|
||||||
VALUES ($1) RETURNING *;
|
VALUES ($1) RETURNING *;
|
||||||
|
|
||||||
;-- name: UpdateProfileSettings :exec
|
;-- name: UpdateProfileSettingsByUsername :exec
|
||||||
UPDATE profile_settings
|
UPDATE profile_settings ps
|
||||||
SET
|
SET
|
||||||
hide_fulfilled = $2,
|
hide_fulfilled = COALESCE($2, ps.hide_fulfilled),
|
||||||
hide_profile_details = $3,
|
hide_profile_details = COALESCE($3, ps.hide_profile_details),
|
||||||
hide_for_unauthenticated = $4,
|
hide_for_unauthenticated = COALESCE($4, ps.hide_for_unauthenticated),
|
||||||
hide_birthday = $5,
|
hide_birthday = COALESCE($5, ps.hide_birthday),
|
||||||
hide_dates = $6,
|
hide_dates = COALESCE($6, ps.hide_dates),
|
||||||
captcha = $7,
|
captcha = COALESCE($7, ps.captcha),
|
||||||
followers_only_interaction = $8
|
followers_only_interaction = COALESCE($8, ps.followers_only_interaction)
|
||||||
WHERE id = $1;
|
FROM profiles p
|
||||||
|
JOIN users u ON p.user_id = u.id
|
||||||
|
WHERE ps.profile_id = p.id AND u.username = $1;
|
||||||
|
|
||||||
;-- name: GetProfileSettingsByUsername :one
|
;-- name: GetProfileSettingsByUsername :one
|
||||||
SELECT profile_settings.* FROM profile_settings
|
SELECT profile_settings.* FROM profile_settings
|
||||||
@@ -219,3 +361,170 @@ JOIN users ON users.id = profiles.user_id
|
|||||||
WHERE users.username = $1;
|
WHERE users.username = $1;
|
||||||
|
|
||||||
--: }}}
|
--: }}}
|
||||||
|
|
||||||
|
--: Wish List Object {{{
|
||||||
|
|
||||||
|
;-- name: CreateWishList :one
|
||||||
|
INSERT INTO wish_lists(profile_id, hidden, name, icon_name, color, color_grad)
|
||||||
|
VALUES (
|
||||||
|
(SELECT p.id FROM profiles AS p
|
||||||
|
JOIN users AS u ON u.id = p.user_id
|
||||||
|
WHERE u.username = @username::text),
|
||||||
|
@hidden::boolean,
|
||||||
|
@name::text,
|
||||||
|
@icon_name::text,
|
||||||
|
@color::text,
|
||||||
|
@color_grad::text
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
;-- name: UpdateWishListByGuid :exec
|
||||||
|
UPDATE wish_lists wl
|
||||||
|
SET
|
||||||
|
hidden = COALESCE(@hidden::boolean, wl.hidden),
|
||||||
|
name = COALESCE(@name::text, wl.name),
|
||||||
|
icon_name = COALESCE(@icon_name::text, wl.icon_name),
|
||||||
|
color = COALESCE(@color::text, wl.color),
|
||||||
|
color_grad = COALESCE(@color_grad::text, wl.color_grad),
|
||||||
|
deleted = COALESCE(@deleted::boolean, wl.deleted)
|
||||||
|
WHERE wl.guid = (@guid::text)::uuid;
|
||||||
|
|
||||||
|
;-- name: GetWishlistByGuid :one
|
||||||
|
SELECT * FROM wish_lists wl
|
||||||
|
WHERE wl.guid = (@guid::text)::uuid;
|
||||||
|
|
||||||
|
;-- name: GetWishListOwnerByGuid :one
|
||||||
|
SELECT u.*
|
||||||
|
FROM wish_lists wl
|
||||||
|
JOIN profiles p ON wl.profile_id = p.id
|
||||||
|
JOIN users u ON p.user_id = u.id
|
||||||
|
WHERE wl.guid = (@guid::text)::uuid;
|
||||||
|
|
||||||
|
;-- name: GetWishlistsByUsername :many
|
||||||
|
SELECT * FROM wish_lists wl
|
||||||
|
JOIN profiles p ON p.id = wl.profile_id
|
||||||
|
JOIN users u ON u.id = p.user_id
|
||||||
|
WHERE u.username = @username::text;
|
||||||
|
|
||||||
|
-- name: GetWishlistsByUsernameWithPrivacy :many
|
||||||
|
SELECT
|
||||||
|
wl.*,
|
||||||
|
CASE
|
||||||
|
WHEN (
|
||||||
|
ps.hide_profile_details OR (
|
||||||
|
ps.hide_for_unauthenticated AND
|
||||||
|
@requester::text = ''
|
||||||
|
)
|
||||||
|
) THEN FALSE
|
||||||
|
ELSE TRUE
|
||||||
|
END AS access_allowed
|
||||||
|
FROM
|
||||||
|
wish_lists wl
|
||||||
|
JOIN
|
||||||
|
profiles AS p ON wl.profile_id = p.id
|
||||||
|
JOIN
|
||||||
|
profile_settings AS ps ON ps.profile_id = p.id
|
||||||
|
JOIN
|
||||||
|
users AS u ON p.user_id = u.id
|
||||||
|
WHERE
|
||||||
|
wl.deleted IS FALSE AND
|
||||||
|
u.username = @username::text AND
|
||||||
|
(
|
||||||
|
u.username = @requester::text OR
|
||||||
|
(u.verified IS TRUE AND
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM banned_users
|
||||||
|
WHERE user_id = u.id AND
|
||||||
|
pardoned IS FALSE AND
|
||||||
|
(expires_at IS NULL OR expires_at < CURRENT_TIMESTAMP)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
--: }}}
|
||||||
|
|
||||||
|
--: Wish Object {{{
|
||||||
|
|
||||||
|
;-- name: CreateWish :one
|
||||||
|
INSERT INTO wishes(
|
||||||
|
wish_list_id,
|
||||||
|
wish_list_guid,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
picture_url,
|
||||||
|
stars)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
(SELECT id FROM wish_lists wl WHERE wl.guid = (@wish_list_guid::text)::uuid),
|
||||||
|
(@wish_list_guid::text)::uuid,
|
||||||
|
@name::text,
|
||||||
|
@description::text,
|
||||||
|
@picture_url::text,
|
||||||
|
@stars::smallint
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
;-- name: UpdateWishByGuid :exec
|
||||||
|
UPDATE wishes w
|
||||||
|
SET
|
||||||
|
name = COALESCE(@name::text, w.name),
|
||||||
|
description = COALESCE(@description::text, w.description),
|
||||||
|
picture_url = COALESCE(@picture_url::text, w.picture_url),
|
||||||
|
stars = COALESCE(@stars::smallint, w.stars),
|
||||||
|
fulfilled = COALESCE(@fulfilled::boolean, w.fulfilled),
|
||||||
|
fulfilled_date = COALESCE(@fulfilled_date::timestamp, w.fulfilled_date),
|
||||||
|
deleted = COALESCE(@deleted::boolean, w.deleted)
|
||||||
|
WHERE w.guid = (@guid::text)::uuid;
|
||||||
|
|
||||||
|
;-- name: MoveWishToWishListWithGuid :one
|
||||||
|
WITH updated AS (
|
||||||
|
UPDATE wishes w
|
||||||
|
SET
|
||||||
|
wish_list_id = wl.id,
|
||||||
|
wish_list_guid = (@wish_list_guid::text)::uuid
|
||||||
|
FROM wish_lists wl
|
||||||
|
WHERE
|
||||||
|
wl.guid = (@wish_list_guid::text)::uuid AND
|
||||||
|
wl.profile_id = ( -- Make sure the wish is not moved to another profile
|
||||||
|
SELECT profile_id
|
||||||
|
FROM wish_lists
|
||||||
|
WHERE wish_lists.id = w.wish_list_id
|
||||||
|
)
|
||||||
|
RETURNING w.id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(*) > 0 AS target_found
|
||||||
|
FROM updated;
|
||||||
|
|
||||||
|
;-- name: GetWishByGuid :one
|
||||||
|
SELECT * FROM wishes w
|
||||||
|
WHERE w.guid = (@guid::text)::uuid;
|
||||||
|
|
||||||
|
;-- name: CheckWishAccessByGuid :one
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM wishes w
|
||||||
|
JOIN wish_lists wl ON w.wish_list_id = wl.id
|
||||||
|
JOIN profiles p ON wl.profile_id = p.id
|
||||||
|
JOIN profile_settings ps ON ps.profile_id = p.id
|
||||||
|
JOIN users u ON p.user_id = u.id
|
||||||
|
LEFT JOIN banned_users bu ON u.id = bu.user_id
|
||||||
|
AND bu.pardoned = FALSE
|
||||||
|
AND (bu.expires_at IS NULL OR bu.expires_at > NOW())
|
||||||
|
WHERE w.guid = (@guid::text)::uuid
|
||||||
|
AND ps.hide_profile_details = FALSE
|
||||||
|
AND (
|
||||||
|
@requester::text != ''
|
||||||
|
OR ps.hide_for_unauthenticated IS FALSE
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
w.fulfilled = FALSE
|
||||||
|
OR ps.hide_fulfilled IS FALSE
|
||||||
|
)
|
||||||
|
AND w.deleted = FALSE
|
||||||
|
AND wl.deleted = FALSE
|
||||||
|
AND u.deleted = FALSE
|
||||||
|
AND bu.id IS NULL -- Ensures owner is not banned
|
||||||
|
);
|
||||||
|
|
||||||
|
--: }}}
|
||||||
|
|||||||
@@ -1,10 +1,30 @@
|
|||||||
|
-- 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/>.
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "users" (
|
CREATE TABLE IF NOT EXISTS "users" (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
username VARCHAR(20) UNIQUE NOT NULL,
|
username VARCHAR(20) UNIQUE NOT NULL,
|
||||||
verified BOOLEAN DEFAULT FALSE,
|
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
registration_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
registration_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
role INTEGER NOT NULL DEFAULT 1, -- enum user
|
||||||
deleted BOOLEAN DEFAULT FALSE
|
deleted BOOLEAN DEFAULT FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -15,7 +35,7 @@ CREATE TABLE IF NOT EXISTS "banned_users" (
|
|||||||
reason VARCHAR(512),
|
reason VARCHAR(512),
|
||||||
expires_at TIMESTAMP,
|
expires_at TIMESTAMP,
|
||||||
banned_by VARCHAR(20) DEFAULT 'system',
|
banned_by VARCHAR(20) DEFAULT 'system',
|
||||||
pardoned BOOLEAN DEFAULT FALSE,
|
pardoned BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
pardoned_by VARCHAR(20)
|
pardoned_by VARCHAR(20)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -35,18 +55,19 @@ CREATE TABLE IF NOT EXISTS "confirmation_codes" (
|
|||||||
code_type INTEGER NOT NULL CHECK (code_type IN (0, 1)),
|
code_type INTEGER NOT NULL CHECK (code_type IN (0, 1)),
|
||||||
code_hash VARCHAR(512) NOT NULL,
|
code_hash VARCHAR(512) NOT NULL,
|
||||||
expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + INTERVAL '10 minutes',
|
expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + INTERVAL '10 minutes',
|
||||||
used BOOLEAN DEFAULT FALSE,
|
used BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
deleted BOOLEAN DEFAULT FALSE
|
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "sessions" (
|
CREATE TABLE IF NOT EXISTS "sessions" (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
user_id BIGINT UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
guid UUID NOT NULL DEFAULT gen_random_uuid(),
|
guid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
|
||||||
name VARCHAR(100),
|
name VARCHAR(175),
|
||||||
platform VARCHAR(32),
|
platform VARCHAR(175),
|
||||||
latest_ip VARCHAR(16),
|
latest_ip VARCHAR(16),
|
||||||
login_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
login_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_refresh_exp_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + INTERVAL '10080 seconds',
|
||||||
last_seen_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
last_seen_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
terminated BOOLEAN DEFAULT FALSE
|
terminated BOOLEAN DEFAULT FALSE
|
||||||
);
|
);
|
||||||
@@ -55,21 +76,48 @@ CREATE TABLE IF NOT EXISTS "profiles" (
|
|||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
user_id BIGINT UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
name VARCHAR(75) NOT NULL,
|
name VARCHAR(75) NOT NULL,
|
||||||
bio VARCHAR(512),
|
bio VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
avatar_url VARCHAR(255),
|
avatar_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
birthday TIMESTAMP,
|
birthday TIMESTAMP,
|
||||||
color VARCHAR(7),
|
color VARCHAR(7) NOT NULL DEFAULT '#254333',
|
||||||
color_grad VARCHAR(7)
|
color_grad VARCHAR(7) NOT NULL DEFAULT '#691E4D'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "profile_settings" (
|
CREATE TABLE IF NOT EXISTS "profile_settings" (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
profile_id BIGINT UNIQUE NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
profile_id BIGINT UNIQUE NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||||
hide_fulfilled BOOLEAN DEFAULT TRUE,
|
hide_fulfilled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
hide_profile_details BOOLEAN DEFAULT FALSE,
|
hide_profile_details BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
hide_for_unauthenticated BOOLEAN DEFAULT FALSE,
|
hide_for_unauthenticated BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
hide_birthday BOOLEAN DEFAULT FALSE,
|
hide_birthday BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
hide_dates BOOLEAN DEFAULT FALSE,
|
hide_dates BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
captcha BOOLEAN DEFAULT FALSE,
|
captcha BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
followers_only_interaction BOOLEAN DEFAULT FALSE
|
followers_only_interaction BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
)
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "wish_lists" (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
guid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
profile_id BIGINT UNIQUE NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||||
|
hidden BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
name VARCHAR(32) NOT NULL DEFAULT 'Wishes',
|
||||||
|
icon_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
color VARCHAR(7) NOT NULL DEFAULT '',
|
||||||
|
color_grad VARCHAR(7) NOT NULL DEFAULT '',
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "wishes" (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
guid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
wish_list_id BIGINT UNIQUE NOT NULL REFERENCES wish_lists(id) ON DELETE CASCADE,
|
||||||
|
wish_list_guid UUID NOT NULL REFERENCES wish_lists(guid) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(64) NOT NULL DEFAULT 'New wish',
|
||||||
|
description VARCHAR(1000) NOT NULL DEFAULT '',
|
||||||
|
picture_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
stars SMALLINT NOT NULL DEFAULT 3 CHECK (stars BETWEEN 1 AND 5),
|
||||||
|
creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
fulfilled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
fulfilled_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ sql:
|
|||||||
go:
|
go:
|
||||||
out: "../backend/internal/database"
|
out: "../backend/internal/database"
|
||||||
sql_package: "pgx/v5"
|
sql_package: "pgx/v5"
|
||||||
|
emit_prepared_queries: true
|
||||||
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user