Compare commits
9 Commits
15c140db31
...
95294686b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 95294686b7 | |||
| 65ea47dbb6 | |||
| a3bebd89be | |||
| a2dd8993a6 | |||
| 8fa57eddb1 | |||
| b91ff2c802 | |||
| 541847221b | |||
| c988a16783 | |||
| f59b647b27 |
@@ -45,6 +45,7 @@ import (
|
||||
"easywish/internal/controllers"
|
||||
"easywish/internal/database"
|
||||
"easywish/internal/logger"
|
||||
redisclient "easywish/internal/redisClient"
|
||||
"easywish/internal/routes"
|
||||
"easywish/internal/services"
|
||||
"easywish/internal/validation"
|
||||
@@ -59,10 +60,13 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg := config.GetConfig()
|
||||
|
||||
fx.New(
|
||||
fx.Provide(
|
||||
logger.NewLogger,
|
||||
logger.NewSyncLogger,
|
||||
redisclient.NewRedisClient,
|
||||
gin.Default,
|
||||
),
|
||||
database.Module,
|
||||
@@ -80,7 +84,7 @@ func main() {
|
||||
|
||||
// Gin
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", strconv.Itoa(int(config.GetConfig().Port))),
|
||||
Addr: fmt.Sprintf(":%s", strconv.Itoa(int(cfg.Port))),
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const docTemplate = `{
|
||||
"summary": "Acquire tokens via login credentials (and 2FA code if needed)",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -63,10 +63,13 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LoginResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid login credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +86,25 @@ const docTemplate = `{
|
||||
"Auth"
|
||||
],
|
||||
"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": {
|
||||
@@ -97,8 +118,29 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Complete password reset with email code and provide 2FA code or backup code if needed",
|
||||
"responses": {}
|
||||
"summary": "Complete password reset via email code",
|
||||
"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": {
|
||||
@@ -113,7 +155,28 @@ const docTemplate = `{
|
||||
"Auth"
|
||||
],
|
||||
"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": {
|
||||
@@ -130,7 +193,7 @@ const docTemplate = `{
|
||||
"summary": "Register an account",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -145,6 +208,9 @@ const docTemplate = `{
|
||||
},
|
||||
"409": {
|
||||
"description": "Username or email is already taken"
|
||||
},
|
||||
"429": {
|
||||
"description": "Too many recent registration attempts for this email"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +229,7 @@ const docTemplate = `{
|
||||
"summary": "Confirm with code, finish creating the account",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -174,10 +240,13 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.RegistrationCompleteResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid email or verification code"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,9 +423,76 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"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_accounts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"verification_code": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.PasswordResetCompleteResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.RefreshRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"refresh_token"
|
||||
],
|
||||
"properties": {
|
||||
"refresh_token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.RefreshResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.RegistrationBeginRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password",
|
||||
"username"
|
||||
],
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"summary": "Acquire tokens via login credentials (and 2FA code if needed)",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -59,10 +59,13 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.LoginResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid login credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +82,25 @@
|
||||
"Auth"
|
||||
],
|
||||
"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": {
|
||||
@@ -93,8 +114,29 @@
|
||||
"tags": [
|
||||
"Auth"
|
||||
],
|
||||
"summary": "Complete password reset with email code and provide 2FA code or backup code if needed",
|
||||
"responses": {}
|
||||
"summary": "Complete password reset via email code",
|
||||
"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": {
|
||||
@@ -109,7 +151,28 @@
|
||||
"Auth"
|
||||
],
|
||||
"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": {
|
||||
@@ -126,7 +189,7 @@
|
||||
"summary": "Register an account",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -141,6 +204,9 @@
|
||||
},
|
||||
"409": {
|
||||
"description": "Username or email is already taken"
|
||||
},
|
||||
"429": {
|
||||
"description": "Too many recent registration attempts for this email"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +225,7 @@
|
||||
"summary": "Confirm with code, finish creating the account",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -170,10 +236,13 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "desc",
|
||||
"description": " ",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.RegistrationCompleteResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid email or verification code"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,9 +419,76 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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_accounts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"verification_code": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.PasswordResetCompleteResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.RefreshRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"refresh_token"
|
||||
],
|
||||
"properties": {
|
||||
"refresh_token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.RefreshResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.RegistrationBeginRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password",
|
||||
"username"
|
||||
],
|
||||
|
||||
@@ -27,6 +27,49 @@ definitions:
|
||||
refresh_token:
|
||||
type: string
|
||||
type: object
|
||||
models.PasswordResetBeginRequest:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
type: object
|
||||
models.PasswordResetCompleteRequest:
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
log_out_accounts:
|
||||
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.RefreshRequest:
|
||||
properties:
|
||||
refresh_token:
|
||||
type: string
|
||||
required:
|
||||
- refresh_token
|
||||
type: object
|
||||
models.RefreshResponse:
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
refresh_token:
|
||||
type: string
|
||||
type: object
|
||||
models.RegistrationBeginRequest:
|
||||
properties:
|
||||
email:
|
||||
@@ -36,6 +79,7 @@ definitions:
|
||||
username:
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
- username
|
||||
type: object
|
||||
@@ -86,7 +130,7 @@ paths:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: desc
|
||||
- description: ' '
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
@@ -96,9 +140,11 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: desc
|
||||
description: ' '
|
||||
schema:
|
||||
$ref: '#/definitions/models.LoginResponse'
|
||||
"403":
|
||||
description: Invalid login credentials
|
||||
summary: Acquire tokens via login credentials (and 2FA code if needed)
|
||||
tags:
|
||||
- Auth
|
||||
@@ -106,9 +152,20 @@ paths:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: ' '
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.PasswordResetBeginRequest'
|
||||
produces:
|
||||
- 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
|
||||
tags:
|
||||
- Auth
|
||||
@@ -116,20 +173,45 @@ paths:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: ' '
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.PasswordResetCompleteRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses: {}
|
||||
summary: Complete password reset with email code and provide 2FA code or backup
|
||||
code if needed
|
||||
responses:
|
||||
"200":
|
||||
description: ' '
|
||||
schema:
|
||||
$ref: '#/definitions/models.PasswordResetCompleteResponse'
|
||||
"403":
|
||||
description: Wrong verification code or username
|
||||
summary: Complete password reset via email code
|
||||
tags:
|
||||
- Auth
|
||||
/auth/refresh:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: ' '
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.RefreshRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses: {}
|
||||
responses:
|
||||
"200":
|
||||
description: ' '
|
||||
schema:
|
||||
$ref: '#/definitions/models.RefreshResponse'
|
||||
"401":
|
||||
description: Invalid refresh token
|
||||
summary: Receive new tokens via refresh token
|
||||
tags:
|
||||
- Auth
|
||||
@@ -138,7 +220,7 @@ paths:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: desc
|
||||
- description: ' '
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
@@ -151,6 +233,8 @@ paths:
|
||||
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
|
||||
tags:
|
||||
- Auth
|
||||
@@ -159,7 +243,7 @@ paths:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: desc
|
||||
- description: ' '
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
@@ -169,9 +253,11 @@ paths:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: desc
|
||||
description: ' '
|
||||
schema:
|
||||
$ref: '#/definitions/models.RegistrationCompleteResponse'
|
||||
"403":
|
||||
description: Invalid email or verification code
|
||||
summary: Confirm with code, finish creating the account
|
||||
tags:
|
||||
- Auth
|
||||
|
||||
@@ -19,7 +19,9 @@ require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
@@ -29,8 +31,10 @@ require (
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
|
||||
@@ -5,12 +5,16 @@ 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.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
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/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
@@ -39,6 +43,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
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/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
@@ -48,6 +54,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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=
|
||||
|
||||
@@ -42,20 +42,21 @@ type AuthController interface {
|
||||
}
|
||||
|
||||
type authControllerImpl struct {
|
||||
authService services.AuthService
|
||||
log *zap.Logger
|
||||
auth services.AuthService
|
||||
}
|
||||
|
||||
func NewAuthController(_log *zap.Logger, as services.AuthService) AuthController {
|
||||
return &authControllerImpl{log: _log, authService: as}
|
||||
func NewAuthController(_log *zap.Logger, _auth services.AuthService) AuthController {
|
||||
return &authControllerImpl{log: _log, auth: _auth}
|
||||
}
|
||||
|
||||
// @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"
|
||||
// @Param request body models.LoginRequest true " "
|
||||
// @Success 200 {object} models.LoginResponse " "
|
||||
// @Failure 403 "Invalid login credentials"
|
||||
// @Router /auth/login [post]
|
||||
func (a *authControllerImpl) Login(c *gin.Context) {
|
||||
request, ok := utils.GetRequest[models.LoginRequest](c)
|
||||
@@ -64,7 +65,7 @@ func (a *authControllerImpl) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := a.authService.Login(request.Body)
|
||||
response, err := a.auth.Login(request.Body)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrForbidden) {
|
||||
@@ -83,25 +84,52 @@ func (a *authControllerImpl) Login(c *gin.Context) {
|
||||
// @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 (a *authControllerImpl) PasswordResetBegin(c *gin.Context) {
|
||||
c.Status(http.StatusNotImplemented)
|
||||
request, ok := utils.GetRequest[models.PasswordResetBeginRequest](c)
|
||||
if !ok {
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := a.auth.PasswordResetBegin(request.Body)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrTooManyRequests) {
|
||||
c.Status(http.StatusTooManyRequests)
|
||||
} else {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
// @Summary Complete password reset with email code and provide 2FA code or backup code if needed
|
||||
// @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 (a *authControllerImpl) PasswordResetComplete(c *gin.Context) {
|
||||
c.Status(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
|
||||
// @Summary Receive new tokens via refresh token
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.RefreshRequest true " "
|
||||
// @Router /auth/refresh [post]
|
||||
// @Success 200 {object} models.RefreshResponse " "
|
||||
// @Failure 401 "Invalid refresh token"
|
||||
func (a *authControllerImpl) Refresh(c *gin.Context) {
|
||||
c.Status(http.StatusNotImplemented)
|
||||
}
|
||||
@@ -110,9 +138,10 @@ func (a *authControllerImpl) Refresh(c *gin.Context) {
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.RegistrationBeginRequest true "desc"
|
||||
// @Param request body models.RegistrationBeginRequest true " "
|
||||
// @Success 200 "Account is created and awaiting verification"
|
||||
// @Success 409 "Username or email is already taken"
|
||||
// @Failure 409 "Username or email is already taken"
|
||||
// @Failure 429 "Too many recent registration attempts for this email"
|
||||
// @Router /auth/registrationBegin [post]
|
||||
func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
|
||||
|
||||
@@ -122,7 +151,7 @@ func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := a.authService.RegistrationBegin(request.Body)
|
||||
_, err := a.auth.RegistrationBegin(request.Body)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) {
|
||||
@@ -141,8 +170,9 @@ func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.RegistrationCompleteRequest true "desc"
|
||||
// @Success 200 {object} models.RegistrationCompleteResponse "desc"
|
||||
// @Param request body models.RegistrationCompleteRequest true " "
|
||||
// @Success 200 {object} models.RegistrationCompleteResponse " "
|
||||
// @Failure 403 "Invalid email or verification code"
|
||||
// @Router /auth/registrationComplete [post]
|
||||
func (a *authControllerImpl) RegistrationComplete(c *gin.Context) {
|
||||
request, ok := utils.GetRequest[models.RegistrationCompleteRequest](c)
|
||||
@@ -151,7 +181,7 @@ func (a *authControllerImpl) RegistrationComplete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := a.authService.RegistrationComplete(request.Body)
|
||||
response, err := a.auth.RegistrationComplete(request.Body)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrForbidden) {
|
||||
@@ -172,6 +202,6 @@ func (a *authControllerImpl) RegisterRoutes(group *gin.RouterGroup) {
|
||||
group.POST("/registrationComplete", middleware.RequestMiddleware[models.RegistrationCompleteRequest](enums.GuestRole), a.RegistrationComplete)
|
||||
group.POST("/login", middleware.RequestMiddleware[models.LoginRequest](enums.GuestRole), a.Login)
|
||||
group.POST("/refresh", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.UserRole), a.Refresh)
|
||||
group.POST("/passwordResetBegin", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.PasswordResetBegin)
|
||||
group.POST("/passwordResetBegin", middleware.RequestMiddleware[models.PasswordResetBeginRequest](enums.GuestRole), a.PasswordResetBegin)
|
||||
group.POST("/passwordResetComplete", middleware.RequestMiddleware[models.RegistrationBeginRequest](enums.GuestRole), a.PasswordResetComplete)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
type DbHelperTransaction interface {
|
||||
Commit() error
|
||||
Rollback() error
|
||||
RollbackOnError(err error) error
|
||||
}
|
||||
|
||||
type DbHelper struct {
|
||||
@@ -98,3 +99,11 @@ func (d *dbHelperTransactionImpl) Rollback() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RollbackOnError implements DbHelperTransaction.
|
||||
func (d *dbHelperTransactionImpl) RollbackOnError(err error) error {
|
||||
if err != nil {
|
||||
return d.Rollback()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,48 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const checkUserRegistrationAvailability = `-- name: CheckUserRegistrationAvailability :one
|
||||
SELECT
|
||||
COUNT(CASE WHEN users.username = $1::text THEN 1 END) > 0 AS username_busy,
|
||||
COUNT(CASE WHEN linfo.email = $2::text THEN 1 END) > 0 AS email_busy
|
||||
FROM users
|
||||
JOIN login_informations AS linfo ON linfo.user_id = users.id
|
||||
WHERE
|
||||
(
|
||||
users.username = $1::text OR
|
||||
linfo.email = $2::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
|
||||
)
|
||||
)
|
||||
`
|
||||
|
||||
type CheckUserRegistrationAvailabilityParams struct {
|
||||
Username string
|
||||
Email string
|
||||
}
|
||||
|
||||
type CheckUserRegistrationAvailabilityRow struct {
|
||||
UsernameBusy bool
|
||||
EmailBusy bool
|
||||
}
|
||||
|
||||
func (q *Queries) CheckUserRegistrationAvailability(ctx context.Context, arg CheckUserRegistrationAvailabilityParams) (CheckUserRegistrationAvailabilityRow, error) {
|
||||
row := q.db.QueryRow(ctx, checkUserRegistrationAvailability, arg.Username, arg.Email)
|
||||
var i CheckUserRegistrationAvailabilityRow
|
||||
err := row.Scan(&i.UsernameBusy, &i.EmailBusy)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createBannedUser = `-- name: CreateBannedUser :one
|
||||
INSERT INTO banned_users(user_id, expires_at, reason, banned_by)
|
||||
VALUES ( $1, $2, $3, $4) RETURNING id, user_id, date, reason, expires_at, banned_by, pardoned, pardoned_by
|
||||
@@ -209,6 +251,35 @@ func (q *Queries) CreateUser(ctx context.Context, username string) (User, error)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteUnverifiedAccountsHavingUsernameOrEmail = `-- name: DeleteUnverifiedAccountsHavingUsernameOrEmail :one
|
||||
WITH deleted_rows AS (
|
||||
DELETE FROM users
|
||||
WHERE
|
||||
(username = $1::text OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM login_informations AS linfo
|
||||
WHERE linfo.user_id = users.id
|
||||
AND linfo.email = $2::text
|
||||
))
|
||||
AND verified IS FALSE
|
||||
RETURNING id, username, verified, registration_date, deleted
|
||||
)
|
||||
SELECT COUNT(*) AS deleted_count FROM deleted_rows
|
||||
`
|
||||
|
||||
type DeleteUnverifiedAccountsHavingUsernameOrEmailParams struct {
|
||||
Username string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteUnverifiedAccountsHavingUsernameOrEmail(ctx context.Context, arg DeleteUnverifiedAccountsHavingUsernameOrEmailParams) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, deleteUnverifiedAccountsHavingUsernameOrEmail, arg.Username, arg.Email)
|
||||
var deleted_count int64
|
||||
err := row.Scan(&deleted_count)
|
||||
return deleted_count, err
|
||||
}
|
||||
|
||||
const deleteUser = `-- name: DeleteUser :exec
|
||||
DELETE FROM users
|
||||
WHERE id = $1
|
||||
@@ -603,6 +674,69 @@ func (q *Queries) GetValidConfirmationCodeByCode(ctx context.Context, arg GetVal
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getValidConfirmationCodesByUsername = `-- name: GetValidConfirmationCodesByUsername :many
|
||||
SELECT confirmation_codes.id, user_id, code_type, code_hash, expires_at, used, confirmation_codes.deleted, users.id, username, verified, registration_date, users.deleted FROM confirmation_codes
|
||||
JOIN users on users.id = confirmation_codes.user_id
|
||||
WHERE
|
||||
users.username = $1::text AND
|
||||
code_type = $2::integer AND
|
||||
expires_at > CURRENT_TIMESTAMP AND
|
||||
used IS FALSE
|
||||
`
|
||||
|
||||
type GetValidConfirmationCodesByUsernameParams struct {
|
||||
Username string
|
||||
CodeType int32
|
||||
}
|
||||
|
||||
type GetValidConfirmationCodesByUsernameRow struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
CodeType int32
|
||||
CodeHash string
|
||||
ExpiresAt pgtype.Timestamp
|
||||
Used *bool
|
||||
Deleted *bool
|
||||
ID_2 int64
|
||||
Username string
|
||||
Verified *bool
|
||||
RegistrationDate pgtype.Timestamp
|
||||
Deleted_2 *bool
|
||||
}
|
||||
|
||||
func (q *Queries) GetValidConfirmationCodesByUsername(ctx context.Context, arg GetValidConfirmationCodesByUsernameParams) ([]GetValidConfirmationCodesByUsernameRow, error) {
|
||||
rows, err := q.db.Query(ctx, getValidConfirmationCodesByUsername, arg.Username, arg.CodeType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetValidConfirmationCodesByUsernameRow
|
||||
for rows.Next() {
|
||||
var i GetValidConfirmationCodesByUsernameRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.CodeType,
|
||||
&i.CodeHash,
|
||||
&i.ExpiresAt,
|
||||
&i.Used,
|
||||
&i.Deleted,
|
||||
&i.ID_2,
|
||||
&i.Username,
|
||||
&i.Verified,
|
||||
&i.RegistrationDate,
|
||||
&i.Deleted_2,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getValidUserByLoginCredentials = `-- name: GetValidUserByLoginCredentials :one
|
||||
SELECT
|
||||
users.id,
|
||||
@@ -731,13 +865,7 @@ const updateLoginInformationByUsername = `-- name: UpdateLoginInformationByUsern
|
||||
UPDATE login_informations
|
||||
SET
|
||||
email = COALESCE($2, email),
|
||||
password_hash = COALESCE(
|
||||
CASE
|
||||
WHEN $3::text IS NOT NULL
|
||||
THEN crypt($3::text, gen_salt('bf'))
|
||||
END,
|
||||
password_hash
|
||||
),
|
||||
password_hash = COALESCE($3::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)
|
||||
@@ -748,7 +876,7 @@ WHERE users.username = $1 AND login_informations.user_id = users.id
|
||||
type UpdateLoginInformationByUsernameParams struct {
|
||||
Username string
|
||||
Email *string
|
||||
Password string
|
||||
PasswordHash string
|
||||
TotpEncrypted *string
|
||||
Email2faEnabled *bool
|
||||
PasswordChangeDate pgtype.Timestamp
|
||||
@@ -758,7 +886,7 @@ func (q *Queries) UpdateLoginInformationByUsername(ctx context.Context, arg Upda
|
||||
_, err := q.db.Exec(ctx, updateLoginInformationByUsername,
|
||||
arg.Username,
|
||||
arg.Email,
|
||||
arg.Password,
|
||||
arg.PasswordHash,
|
||||
arg.TotpEncrypted,
|
||||
arg.Email2faEnabled,
|
||||
arg.PasswordChangeDate,
|
||||
|
||||
@@ -25,4 +25,5 @@ var (
|
||||
ErrNotImplemented = errors.New("Feature is not implemented")
|
||||
ErrBadRequest = errors.New("Bad request")
|
||||
ErrForbidden = errors.New("Access is denied")
|
||||
ErrTooManyRequests = errors.New("Too many requests")
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ type RegistrationBeginRequest struct {
|
||||
|
||||
type RegistrationCompleteRequest struct {
|
||||
Username string `json:"username" binding:"required" validate:"username"`
|
||||
VerificationCode string `json:"verification_code" binding:"required"`
|
||||
VerificationCode string `json:"verification_code" binding:"required" validate:"verification_code=reg"`
|
||||
Name string `json:"name" binding:"required" validate:"name"`
|
||||
Birthday *string `json:"birthday"`
|
||||
}
|
||||
@@ -57,3 +57,18 @@ type RefreshRequest struct {
|
||||
type RefreshResponse struct {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"easywish/config"
|
||||
"easywish/internal/database"
|
||||
errs "easywish/internal/errors"
|
||||
@@ -26,7 +27,10 @@ import (
|
||||
"easywish/internal/utils/enums"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.uber.org/zap"
|
||||
@@ -37,41 +41,85 @@ type AuthService interface {
|
||||
RegistrationComplete(request models.RegistrationCompleteRequest) (*models.RegistrationCompleteResponse, error)
|
||||
Login(request models.LoginRequest) (*models.LoginResponse, error)
|
||||
Refresh(request models.RefreshRequest) (*models.RefreshResponse, error)
|
||||
PasswordResetBegin(request models.PasswordResetBeginRequest) (bool, error)
|
||||
PasswordResetComplete(request models.PasswordResetCompleteRequest) (*models.PasswordResetCompleteResponse, error)
|
||||
}
|
||||
|
||||
type authServiceImpl struct {
|
||||
log *zap.Logger
|
||||
smtp SmtpService
|
||||
dbctx database.DbContext
|
||||
redis *redis.Client
|
||||
smtp SmtpService
|
||||
}
|
||||
|
||||
func NewAuthService(_log *zap.Logger, _dbctx database.DbContext, _smtp SmtpService) AuthService {
|
||||
return &authServiceImpl{log: _log, dbctx: _dbctx, smtp: _smtp}
|
||||
func NewAuthService(_log *zap.Logger, _dbctx database.DbContext, _redis *redis.Client, _smtp SmtpService) AuthService {
|
||||
return &authServiceImpl{log: _log, dbctx: _dbctx, redis: _redis, smtp: _smtp}
|
||||
}
|
||||
|
||||
func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) {
|
||||
|
||||
var occupationStatus database.CheckUserRegistrationAvailabilityRow
|
||||
var user database.User
|
||||
var generatedCode string
|
||||
var generatedCodeHash string
|
||||
var passwordHash string
|
||||
|
||||
helper, db, _ := database.NewDbHelperTransaction(a.dbctx)
|
||||
defer helper.Rollback()
|
||||
|
||||
var err error
|
||||
|
||||
if user, err = db.TXQueries.CreateUser(db.CTX, request.Username); err != nil {
|
||||
|
||||
if errs.MatchPgError(err, pgerrcode.UniqueViolation) {
|
||||
a.log.Warn(
|
||||
"Attempted registration for a taken username",
|
||||
zap.String("username", request.Username),
|
||||
helper, db, err := database.NewDbHelperTransaction(a.dbctx)
|
||||
if err != nil {
|
||||
a.log.Error(
|
||||
"Failed to open a transaction",
|
||||
zap.Error(err))
|
||||
|
||||
return false, errs.ErrUsernameTaken
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
defer helper.RollbackOnError(err)
|
||||
|
||||
// TODO: check occupation with redis
|
||||
|
||||
if occupationStatus, err = db.TXQueries.CheckUserRegistrationAvailability(db.CTX, database.CheckUserRegistrationAvailabilityParams{
|
||||
Email: request.Email,
|
||||
Username: request.Username,
|
||||
}); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to check credentials availability for registration",
|
||||
zap.String("username", request.Username),
|
||||
zap.String("email", request.Email),
|
||||
zap.Error(err))
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
if occupationStatus.UsernameBusy {
|
||||
a.log.Warn(
|
||||
"Attempted registration for a taken username",
|
||||
zap.String("email", request.Email),
|
||||
zap.String("username", request.Username))
|
||||
return false, errs.ErrUsernameTaken
|
||||
|
||||
} else if occupationStatus.EmailBusy {
|
||||
// Falsely confirm in order to avoid disclosing registered email addresses
|
||||
// TODO: save this email into redis
|
||||
a.log.Warn(
|
||||
"Attempted registration for a taken email",
|
||||
zap.String("email", request.Email),
|
||||
zap.String("username", request.Username))
|
||||
return true, nil
|
||||
|
||||
} else {
|
||||
if _, err := db.TXQueries.DeleteUnverifiedAccountsHavingUsernameOrEmail(db.CTX, database.DeleteUnverifiedAccountsHavingUsernameOrEmailParams{
|
||||
Username: request.Username,
|
||||
Email: request.Email,
|
||||
}); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to purge unverified accounts as part of registration",
|
||||
zap.String("email", request.Email),
|
||||
zap.String("username", request.Username),
|
||||
zap.Error(err))
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
}
|
||||
|
||||
if user, err = db.TXQueries.CreateUser(db.CTX, request.Username); err != nil {
|
||||
a.log.Error("Failed to add user to database", zap.Error(err))
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
@@ -138,12 +186,17 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
|
||||
|
||||
} else {
|
||||
a.log.Debug(
|
||||
"Declated registration code for a new user. Enable SMTP in the config to disable this message",
|
||||
"Declared registration code for a new user. Enable SMTP in the config to disable this message",
|
||||
zap.String("username", user.Username),
|
||||
zap.String("code", generatedCode))
|
||||
}
|
||||
|
||||
helper.Commit()
|
||||
if err = helper.Commit(); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to commit transaction",
|
||||
zap.Error(err))
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -157,8 +210,14 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
|
||||
var accessToken, refreshToken string
|
||||
var err error
|
||||
|
||||
helper, db, _ := database.NewDbHelperTransaction(a.dbctx)
|
||||
defer helper.Rollback()
|
||||
helper, db, err := database.NewDbHelperTransaction(a.dbctx)
|
||||
if err != nil {
|
||||
a.log.Error(
|
||||
"Failed to open a transaction",
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
defer helper.RollbackOnError(err)
|
||||
|
||||
user, err = db.TXQueries.GetUserByUsername(db.CTX, request.Username)
|
||||
|
||||
@@ -275,7 +334,12 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
helper.Commit()
|
||||
if err = helper.Commit(); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to commit transaction",
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
a.log.Info(
|
||||
"User verified registration",
|
||||
@@ -293,12 +357,17 @@ func (a *authServiceImpl) RegistrationComplete(request models.RegistrationComple
|
||||
func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginResponse, error) {
|
||||
var userRow database.GetValidUserByLoginCredentialsRow
|
||||
var session database.Session
|
||||
|
||||
helper, db, _ := database.NewDbHelperTransaction(a.dbctx)
|
||||
defer helper.Rollback()
|
||||
|
||||
var err error
|
||||
|
||||
helper, db, err := database.NewDbHelperTransaction(a.dbctx)
|
||||
if err != nil {
|
||||
a.log.Error(
|
||||
"Failed to open a transaction",
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
defer helper.RollbackOnError(err)
|
||||
|
||||
userRow, err = db.TXQueries.GetValidUserByLoginCredentials(db.CTX, database.GetValidUserByLoginCredentialsParams{
|
||||
Username: request.Username,
|
||||
Password: request.Password,
|
||||
@@ -355,7 +424,12 @@ func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginRespo
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
helper.Commit()
|
||||
if err = helper.Commit(); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to commit transaction",
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
response := models.LoginResponse{Tokens: models.Tokens{
|
||||
AccessToken: accessToken,
|
||||
@@ -368,3 +442,241 @@ func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginRespo
|
||||
func (a *authServiceImpl) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) {
|
||||
return nil, errs.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (a *authServiceImpl) PasswordResetBegin(request models.PasswordResetBeginRequest) (bool, error) {
|
||||
|
||||
var user database.User
|
||||
var generatedCode, hashedCode string
|
||||
var err error
|
||||
|
||||
helper, db, err := database.NewDbHelperTransaction(a.dbctx)
|
||||
if err != nil {
|
||||
a.log.Error(
|
||||
"Failed to open a transaction",
|
||||
zap.Error(err))
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
defer helper.RollbackOnError(err)
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
cooldownTimeUnix, redisErr := a.redis.Get(ctx, fmt.Sprintf("email::%s::reset_cooldown", request.Email)).Int64()
|
||||
if redisErr != nil && redisErr != redis.Nil {
|
||||
a.log.Error(
|
||||
"Failed to get reset_cooldown state for user",
|
||||
zap.String("email", request.Email),
|
||||
zap.Error(redisErr))
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
if time.Now().Unix() < cooldownTimeUnix {
|
||||
a.log.Warn(
|
||||
"Attempted to request a new password reset code for email on active reset cooldown",
|
||||
zap.String("email", request.Email))
|
||||
return false, errs.ErrTooManyRequests
|
||||
}
|
||||
|
||||
if user, err = db.TXQueries.GetUserByEmail(db.CTX, request.Email); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
// Enable cooldown for the email despite that account does not exist
|
||||
err := a.redis.Set(
|
||||
ctx,
|
||||
fmt.Sprintf("email::%s::reset_cooldown", request.Email),
|
||||
time.Now().Add(10*time.Minute),
|
||||
time.Duration(10*time.Minute),
|
||||
).Err()
|
||||
|
||||
if err != nil {
|
||||
a.log.Error(
|
||||
"Failed to set reset cooldown for email",
|
||||
zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
a.log.Warn(
|
||||
"Requested password reset email for unexistent user",
|
||||
zap.String("email", request.Email))
|
||||
return true, nil
|
||||
}
|
||||
a.log.Error(
|
||||
"Failed to retrieve user from database",
|
||||
zap.String("email", request.Email),
|
||||
zap.Error(err))
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
generatedCode = uuid.New().String()
|
||||
if hashedCode, err = utils.HashPassword(generatedCode); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to hash password reset code for user",
|
||||
zap.String("username", user.Username),
|
||||
zap.Error(err))
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
if _, err = db.TXQueries.CreateConfirmationCode(db.CTX, database.CreateConfirmationCodeParams{
|
||||
UserID: user.ID,
|
||||
CodeType: int32(enums.PasswordResetCodeType),
|
||||
CodeHash: hashedCode,
|
||||
}); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to save user password reset code to the database",
|
||||
zap.String("username", user.Username),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
err = a.redis.Set(
|
||||
ctx,
|
||||
fmt.Sprintf("email::%s::reset_cooldown", request.Email),
|
||||
time.Now().Add(10*time.Minute),
|
||||
time.Duration(10*time.Minute),
|
||||
).Err()
|
||||
|
||||
if err != nil {
|
||||
a.log.Error(
|
||||
"Failed to set reset cooldown for email. Cancelling password reset",
|
||||
zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err = helper.Commit(); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to commit transaction",
|
||||
zap.Error(err))
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *authServiceImpl) PasswordResetComplete(request models.PasswordResetCompleteRequest) (*models.PasswordResetCompleteResponse, error) {
|
||||
|
||||
var resetCode database.ConfirmationCode
|
||||
var user database.User
|
||||
var session database.Session
|
||||
var hashedPassword, accessToken, refreshToken string
|
||||
var err error
|
||||
|
||||
helper, db, err := database.NewDbHelperTransaction(a.dbctx)
|
||||
if err != nil {
|
||||
a.log.Error(
|
||||
"Failed to open a transaction",
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
if user, err = db.TXQueries.GetUserByEmail(db.CTX, request.Email); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
a.log.Warn(
|
||||
"Attempted to complete password reset for unregistered email",
|
||||
zap.String("email", request.Email),
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrForbidden
|
||||
}
|
||||
|
||||
a.log.Error(
|
||||
"Failed to look up user of email while trying to complete password reset",
|
||||
zap.String("email", request.Email),
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
if resetCode, err = db.TXQueries.GetValidConfirmationCodeByCode(db.CTX, database.GetValidConfirmationCodeByCodeParams{
|
||||
UserID: user.ID,
|
||||
CodeType: int32(enums.PasswordResetCodeType),
|
||||
Code: request.VerificationCode,
|
||||
}); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
a.log.Warn(
|
||||
"Attempted to reset password for user using incorrect confirmation code",
|
||||
zap.String("email", request.Email),
|
||||
zap.String("username", user.Username),
|
||||
zap.String("provided_code", request.VerificationCode),
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrForbidden
|
||||
}
|
||||
}
|
||||
|
||||
if err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{
|
||||
ID: resetCode.ID,
|
||||
Used: utils.NewPointer(true),
|
||||
}); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to invalidate password reset code upon use",
|
||||
zap.String("username", user.Username),
|
||||
zap.String("email", request.Email),
|
||||
zap.Int64("code_id", resetCode.ID),
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
if hashedPassword, err = utils.HashPassword(request.NewPassword); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to hash new password as part of user password reset",
|
||||
zap.String("email", request.Email),
|
||||
zap.String("username", user.Username),
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
if err = db.TXQueries.UpdateLoginInformationByUsername(db.CTX, database.UpdateLoginInformationByUsernameParams{
|
||||
Username: user.Username,
|
||||
PasswordHash: hashedPassword,
|
||||
}); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to save new password to database as part of user password reset",
|
||||
zap.String("username", user.Username),
|
||||
zap.String("email", request.Email),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
if request.LogOutSessions {
|
||||
if err = db.TXQueries.TerminateAllSessionsForUserByUsername(db.CTX, user.Username); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to log out older sessions as part of user password reset",
|
||||
zap.String("email", request.Email),
|
||||
zap.String("username", user.Username),
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
}
|
||||
|
||||
if session, err = db.TXQueries.CreateSession(db.CTX, database.CreateSessionParams{
|
||||
UserID: user.ID,
|
||||
Name: utils.NewPointer("First device"),
|
||||
Platform: utils.NewPointer("Unknown"),
|
||||
LatestIp: utils.NewPointer("Unknown"),
|
||||
}); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to create new session for user as part of user password reset",
|
||||
zap.String("email", request.Email),
|
||||
zap.String("username", user.Username),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
if accessToken, refreshToken, err = utils.GenerateTokens(user.Username, session.Guid.String()); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to generate tokens as part of user password reset",
|
||||
zap.String("email", request.Email),
|
||||
zap.String("username", user.Username),
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
response := models.PasswordResetCompleteResponse{
|
||||
Tokens: models.Tokens{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
},
|
||||
}
|
||||
|
||||
if err = helper.Commit(); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to commit transaction",
|
||||
zap.Error(err))
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
errs "easywish/internal/errors"
|
||||
)
|
||||
|
||||
@@ -35,23 +34,20 @@ type SmtpService interface {
|
||||
}
|
||||
|
||||
type smtpServiceImpl struct {
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func NewSmtpService(_log *zap.Logger) SmtpService {
|
||||
return &smtpServiceImpl{log: _log}
|
||||
func NewSmtpService() SmtpService {
|
||||
return &smtpServiceImpl{}
|
||||
}
|
||||
|
||||
func (s *smtpServiceImpl) SendEmail(to string, subject, body string) error {
|
||||
cfg := config.GetConfig()
|
||||
|
||||
if !cfg.SmtpEnabled {
|
||||
s.log.Error("Attempted to send an email with SMTP disabled in the config")
|
||||
return errs.ErrSmtpDisabled
|
||||
}
|
||||
|
||||
if cfg.SmtpServer == "" || cfg.SmtpPort == 0 || cfg.SmtpFrom == "" {
|
||||
s.log.Error("SMTP service settings or the SMTP From paramater are not set")
|
||||
return errs.ErrSmtpMissingConfiguration
|
||||
}
|
||||
|
||||
@@ -72,7 +68,7 @@ func (s *smtpServiceImpl) SendEmail(to string, subject, body string) error {
|
||||
sb.WriteString("\r\n" + body)
|
||||
message := []byte(sb.String())
|
||||
|
||||
hostPort := fmt.Sprintf("%s:%d", cfg.SmtpServer, cfg.SmtpPort)
|
||||
hostPort := net.JoinHostPort(cfg.SmtpServer, fmt.Sprintf("%d", cfg.SmtpPort))
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
@@ -84,14 +80,12 @@ func (s *smtpServiceImpl) SendEmail(to string, subject, body string) error {
|
||||
conn, err = net.DialTimeout("tcp", hostPort, timeout)
|
||||
}
|
||||
if err != nil {
|
||||
s.log.Error("SMTP connection failure", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, cfg.SmtpServer)
|
||||
if err != nil {
|
||||
s.log.Error("SMTP client creation failed", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
@@ -107,19 +101,16 @@ func (s *smtpServiceImpl) SendEmail(to string, subject, body string) error {
|
||||
if cfg.SmtpUser != "" && cfg.SmtpPassword != "" {
|
||||
auth := smtp.PlainAuth("", cfg.SmtpUser, cfg.SmtpPassword, cfg.SmtpServer)
|
||||
if err = client.Auth(auth); err != nil {
|
||||
s.log.Error("SMTP authentication failure", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = client.Mail(cfg.SmtpFrom); err != nil {
|
||||
s.log.Error("SMTP sender set failed", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
for _, recipient := range to {
|
||||
if err = client.Rcpt(string(recipient)); err != nil {
|
||||
s.log.Error("SMTP recipient set failed", zap.Error(err))
|
||||
for _, recipient := range toSlice {
|
||||
if err = client.Rcpt(recipient); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -127,15 +118,12 @@ func (s *smtpServiceImpl) SendEmail(to string, subject, body string) error {
|
||||
// Send email body
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
s.log.Error("SMTP data command failed", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if _, err = w.Write(message); err != nil {
|
||||
s.log.Error("SMTP message write failed", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if err = w.Close(); err != nil {
|
||||
s.log.Error("SMTP message close failed", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package validation
|
||||
|
||||
import (
|
||||
"easywish/config"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -77,6 +78,25 @@ func GetCustomHandlers() []CustomValidatorHandler {
|
||||
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))
|
||||
}},
|
||||
|
||||
}
|
||||
|
||||
return handlers
|
||||
|
||||
127
dev-compose.yml
Normal file
127
dev-compose.yml
Normal file
@@ -0,0 +1,127 @@
|
||||
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}
|
||||
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
|
||||
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"
|
||||
command: server /data
|
||||
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
|
||||
@@ -56,6 +56,29 @@ SELECT users.* FROM users
|
||||
JOIN login_informations linfo ON linfo.user_id = users.id
|
||||
WHERE linfo.email = @email::text;
|
||||
|
||||
;-- name: CheckUserRegistrationAvailability :one
|
||||
-- SELECT
|
||||
-- COUNT(users.username = @username::text) > 0 AS username_busy,
|
||||
-- COUNT(linfo.email = @email::text) > 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
|
||||
-- COUNT(
|
||||
-- SELECT confirmation_codes as codes
|
||||
-- JOIN users on users.id = codes.user_id
|
||||
-- WHERE codes.code_type = 0 AND
|
||||
-- codes.deleted IS FALSE AND
|
||||
-- codes.expires_at < CURRENT_TIMESTAMP
|
||||
-- ) = 0;
|
||||
-- )
|
||||
|
||||
;-- name: GetValidUserByLoginCredentials :one
|
||||
SELECT
|
||||
users.id,
|
||||
@@ -72,6 +95,46 @@ WHERE
|
||||
banned.user_id IS NULL AND -- Not banned
|
||||
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 {{{
|
||||
@@ -111,13 +174,7 @@ VALUES ( $1, $2, @password_hash::text ) RETURNING *;
|
||||
UPDATE login_informations
|
||||
SET
|
||||
email = COALESCE($2, email),
|
||||
password_hash = COALESCE(
|
||||
CASE
|
||||
WHEN @password::text IS NOT NULL
|
||||
THEN crypt(@password::text, gen_salt('bf'))
|
||||
END,
|
||||
password_hash
|
||||
),
|
||||
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)
|
||||
@@ -146,6 +203,15 @@ WHERE
|
||||
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
|
||||
UPDATE confirmation_codes
|
||||
SET
|
||||
|
||||
Reference in New Issue
Block a user