UPDATE: Auth module, User module
Some checks failed
Build and Release / release (push) Failing after 1m25s

This commit is contained in:
2026-03-30 00:27:57 +07:00
parent 92d44bb00c
commit f04441bf2a
59 changed files with 4246 additions and 521 deletions

83
pkg/cache/redis.go vendored
View File

@@ -5,18 +5,22 @@ import (
"encoding/json"
"fmt"
"history-api/pkg/config"
"history-api/pkg/constants"
"time"
"github.com/redis/go-redis/v9"
)
type Cache interface {
Set(ctx context.Context, key string, value any, ttl time.Duration) error
Get(ctx context.Context, key string, dest any) error
Del(ctx context.Context, keys ...string) error
DelByPattern(ctx context.Context, pattern string) error
MGet(ctx context.Context, keys ...string) [][]byte
MSet(ctx context.Context, pairs map[string]any, ttl time.Duration) error
Set(ctx context.Context, key string, value any, ttl time.Duration) error
Get(ctx context.Context, key string, dest any) error
Del(ctx context.Context, keys ...string) error
DelByPattern(ctx context.Context, pattern string) error
MGet(ctx context.Context, keys ...string) [][]byte
MSet(ctx context.Context, pairs map[string]any, ttl time.Duration) error
Exists(ctx context.Context, key string) (bool, error)
GetRawClient() *redis.Client
PublishTask(ctx context.Context, streamName string, taskType constants.TaskType, payload any) error
}
type RedisClient struct {
@@ -49,33 +53,45 @@ func NewRedisClient() (Cache, error) {
return &RedisClient{client: rdb}, nil
}
func (r *RedisClient) GetRawClient() *redis.Client {
return r.client
}
func (r *RedisClient) Exists(ctx context.Context, key string) (bool, error) {
count, err := r.client.Exists(ctx, key).Result()
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *RedisClient) Del(ctx context.Context, keys ...string) error {
if len(keys) == 0 {
return nil
}
return r.client.Del(ctx, keys...).Err()
if len(keys) == 0 {
return nil
}
return r.client.Del(ctx, keys...).Err()
}
func (r *RedisClient) DelByPattern(ctx context.Context, pattern string) error {
var cursor uint64
for {
keys, nextCursor, err := r.client.Scan(ctx, cursor, pattern, 100).Result()
if err != nil {
return fmt.Errorf("error scanning keys with pattern %s: %v", pattern, err)
}
var cursor uint64
for {
keys, nextCursor, err := r.client.Scan(ctx, cursor, pattern, 1000).Result()
if err != nil {
return fmt.Errorf("error scanning keys with pattern %s: %v", pattern, err)
}
if len(keys) > 0 {
if err := r.client.Del(ctx, keys...).Err(); err != nil {
return fmt.Errorf("error deleting keys during scan: %v", err)
if len(keys) > 0 {
if err := r.client.Unlink(ctx, keys...).Err(); err != nil {
return fmt.Errorf("error unlinking keys during scan: %v", err)
}
}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
return nil
cursor = nextCursor
if cursor == 0 {
break
}
}
return nil
}
func (r *RedisClient) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
@@ -121,6 +137,21 @@ func (r *RedisClient) MGet(ctx context.Context, keys ...string) [][]byte {
return results
}
func (r *RedisClient) PublishTask(ctx context.Context, streamName string, taskType constants.TaskType, payload any) error {
payloadBytes, err := json.Marshal(payload)
if err != nil {
return err
}
return r.client.XAdd(ctx, &redis.XAddArgs{
Stream: streamName,
Values: map[string]interface{}{
"task_type": taskType.String(),
"payload": string(payloadBytes),
},
}).Err()
}
func GetMultiple[T any](ctx context.Context, c Cache, keys []string) ([]T, error) {
raws := c.MGet(ctx, keys...)
final := make([]T, 0)

View File

@@ -34,3 +34,11 @@ func GetConfig(config string) (string, error) {
return data, nil
}
func GetConfigWithDefault(config, defaultValue string) string {
var data string = os.Getenv(config)
if data == "" {
return defaultValue
}
return data
}

View File

@@ -1,4 +1,4 @@
package constant
package constants
import (
"errors"

View File

@@ -1,4 +1,4 @@
package constant
package constants
type Role string
@@ -18,10 +18,15 @@ func (r Role) Compare(other Role) bool {
return r == other
}
func (r Role) IsValid() bool {
return CheckValidRole(r)
}
func CheckValidRole(r Role) bool {
return r == ADMIN || r == MOD || r == HISTORIAN || r == USER || r == BANNED
}
func ParseRole(s string) (Role, bool) {
r := Role(s)
if CheckValidRole(r) {

6
pkg/constants/sream.go Normal file
View File

@@ -0,0 +1,6 @@
package constants
const (
StreamEmailName = "stream:email_tasks"
GroupEmailName = "email_workers_group"
)

View File

@@ -1,4 +1,4 @@
package constant
package constants
type StatusType int16

11
pkg/constants/task.go Normal file
View File

@@ -0,0 +1,11 @@
package constants
type TaskType string
const (
TaskTypeSendEmailOTP TaskType = "SEND_EMAIL_OTP"
)
func (t TaskType) String() string {
return string(t)
}

13
pkg/constants/time.go Normal file
View File

@@ -0,0 +1,13 @@
package constants
import "time"
const (
TokenCooldownDuration = 1 * time.Minute
TokenExpirationDuration = 20 * time.Minute
NormalCacheDuration = 15 * time.Minute
ListCacheDuration = 10 * time.Minute
AccessTokenDuration = 15 * time.Minute
RefreshTokenDuration = 7 * 24 * time.Hour
TokenVerifiedDuration = 10 * time.Minute
)

View File

@@ -1,4 +1,4 @@
package constant
package constants
type TokenType int16
@@ -24,6 +24,10 @@ func (t TokenType) String() string {
}
}
func (t TokenType) Value() int16 {
return int16(t)
}
func ParseTokenType(v int16) TokenType {
switch v {
case 1:
@@ -37,4 +41,19 @@ func ParseTokenType(v int16) TokenType {
default:
return 0
}
}
func ParseTokenTypeFromString(s string) TokenType {
switch s {
case "PASSWORD_RESET":
return TokenPasswordReset
case "EMAIL_VERIFY":
return TokenEmailVerify
case "LOGIN_MAGIC_LINK":
return TokenMagicLink
case "REFRESH_TOKEN":
return TokenRefreshToken
default:
return 0
}
}

View File

@@ -1,4 +1,4 @@
package constant
package constants
type VerifyType int16

View File

@@ -13,6 +13,15 @@ func UUIDToString(v pgtype.UUID) string {
return ""
}
func StringToUUID(s string) (pgtype.UUID, error) {
var pgId pgtype.UUID
err := pgId.Scan(s)
if err != nil {
return pgtype.UUID{}, err
}
return pgId, nil
}
func TextToString(v pgtype.Text) string {
if v.Valid {
return v.String

70
pkg/email/email.go Normal file
View File

@@ -0,0 +1,70 @@
package email
import (
"fmt"
"history-api/assets"
"history-api/pkg/config"
"history-api/pkg/constants"
"strings"
"github.com/wneessen/go-mail"
)
func SendMailOTP(toEmail, otpCode string, tokenType constants.TokenType) error {
userSmtp, err := config.GetConfig("SMTP_USER")
if err != nil {
return err
}
passSmtp, err := config.GetConfig("SMTP_PASS")
if err != nil {
return err
}
var subject string
var templatePath string
switch tokenType {
case constants.TokenPasswordReset:
subject = "Your Password Reset Code"
templatePath = "resources/password_reset.html"
case constants.TokenEmailVerify:
subject = "Verify your email address"
templatePath = "resources/email_verify.html"
default:
return fmt.Errorf("invalid token type: %v", tokenType)
}
htmlTemplate, err := assets.GetFileContent(templatePath)
if err != nil {
return fmt.Errorf("failed to read email template: %s", err)
}
message := mail.NewMsg()
if err := message.From(userSmtp); err != nil {
return fmt.Errorf("failed to set From email address: %s", err)
}
if err := message.To(toEmail); err != nil {
return fmt.Errorf("failed to set To email address: %s", err)
}
finalHTML := strings.ReplaceAll(htmlTemplate, "{{OTP_CODE}}", otpCode)
message.Subject(subject)
message.SetBodyString(mail.TypeTextHTML, finalHTML)
client, err := mail.NewClient(
"smtp.gmail.com",
mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover),
mail.WithTLSPortPolicy(mail.TLSMandatory),
mail.WithUsername(userSmtp),
mail.WithPassword(passSmtp),
)
if err != nil {
return fmt.Errorf("failed to create mail client: %s", err)
}
err = client.DialAndSend(message)
if err != nil {
return fmt.Errorf("failed to send mail: %s", err)
}
return nil
}