This commit is contained in:
127
pkg/cache/redis.go
vendored
127
pkg/cache/redis.go
vendored
@@ -2,31 +2,136 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"history-api/pkg/config"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var RI *redis.Client
|
||||
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
|
||||
}
|
||||
|
||||
func Connect() error {
|
||||
connectionURI, err := config.GetConfig("REDIS_CONNECTION_URI")
|
||||
type RedisClient struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func NewRedisClient() (Cache, error) {
|
||||
uri, err := config.GetConfig("REDIS_CONNECTION_URI")
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: connectionURI,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
Addr: uri,
|
||||
MinIdleConns: 10,
|
||||
DialTimeout: 5 * time.Second,
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
|
||||
MaxRetries: 3,
|
||||
MinRetryBackoff: 8 * time.Millisecond,
|
||||
MaxRetryBackoff: 512 * time.Millisecond,
|
||||
|
||||
DisableIdentity: true,
|
||||
})
|
||||
|
||||
if err := rdb.Ping(context.Background()).Err(); err != nil {
|
||||
return fmt.Errorf("Could not connect to Redis: %v", err)
|
||||
return nil, fmt.Errorf("could not connect to Redis: %v", err)
|
||||
}
|
||||
|
||||
RI = rdb
|
||||
return nil
|
||||
return &RedisClient{client: rdb}, nil
|
||||
}
|
||||
|
||||
func (r *RedisClient) Del(ctx context.Context, keys ...string) error {
|
||||
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)
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
if err := r.client.Del(ctx, keys...).Err(); err != nil {
|
||||
return fmt.Errorf("error deleting keys during scan: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
cursor = nextCursor
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RedisClient) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.client.Set(ctx, key, data, ttl).Err()
|
||||
}
|
||||
|
||||
func (r *RedisClient) Get(ctx context.Context, key string, dest any) error {
|
||||
data, err := r.client.Get(ctx, key).Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, dest)
|
||||
}
|
||||
|
||||
func (r *RedisClient) MSet(ctx context.Context, pairs map[string]any, ttl time.Duration) error {
|
||||
pipe := r.client.Pipeline()
|
||||
for key, value := range pairs {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal key %s: %v", key, err)
|
||||
}
|
||||
pipe.Set(ctx, key, data, ttl)
|
||||
}
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *RedisClient) MGet(ctx context.Context, keys ...string) [][]byte {
|
||||
res, err := r.client.MGet(ctx, keys...).Result()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
results := make([][]byte, len(res))
|
||||
for i, val := range res {
|
||||
if val != nil {
|
||||
results[i] = []byte(val.(string))
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func GetMultiple[T any](ctx context.Context, c Cache, keys []string) ([]T, error) {
|
||||
raws := c.MGet(ctx, keys...)
|
||||
final := make([]T, 0)
|
||||
for _, b := range raws {
|
||||
if b == nil {
|
||||
continue
|
||||
}
|
||||
var item T
|
||||
if err := json.Unmarshal(b, &item); err == nil {
|
||||
final = append(final, item)
|
||||
}
|
||||
}
|
||||
return final, nil
|
||||
}
|
||||
|
||||
39
pkg/constant/regex.go
Normal file
39
pkg/constant/regex.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// Password components (Go-compatible)
|
||||
hasUpper = regexp.MustCompile(`[A-Z]`)
|
||||
hasLower = regexp.MustCompile(`[a-z]`)
|
||||
hasNumber = regexp.MustCompile(`\d`)
|
||||
hasSpecial = regexp.MustCompile(`[!@#$%^&*()_+{}|:<>?~-]`)
|
||||
|
||||
// Standard Regexes
|
||||
PHONE_NUMBER_REGEX = regexp.MustCompile(`^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$`)
|
||||
EMAIL_REGEX = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
|
||||
YOUTUBE_VIDEO_ID_REGEX = regexp.MustCompile(`(?:\/|v=|\/v\/|embed\/|watch\?v=|watch\?.+&v=)([\w-]{11})`)
|
||||
BANK_INPUT = regexp.MustCompile(`[__]{2,}`)
|
||||
)
|
||||
|
||||
func ValidatePassword(password string) error {
|
||||
if len(password) < 8 {
|
||||
return errors.New("password must be at least 8 characters long")
|
||||
}
|
||||
if !hasUpper.MatchString(password) {
|
||||
return errors.New("password must contain at least one uppercase letter")
|
||||
}
|
||||
if !hasLower.MatchString(password) {
|
||||
return errors.New("password must contain at least one lowercase letter")
|
||||
}
|
||||
if !hasNumber.MatchString(password) {
|
||||
return errors.New("password must contain at least one number")
|
||||
}
|
||||
if !hasSpecial.MatchString(password) {
|
||||
return errors.New("password must contain at least one special character")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -22,6 +22,13 @@ func CheckValidRole(r Role) bool {
|
||||
return r == ADMIN || r == MOD || r == HISTORIAN || r == USER || r == BANNED
|
||||
}
|
||||
|
||||
func (r Role) ToSlice() []string {
|
||||
return []string{r.String()}
|
||||
func ParseRole(s string) (Role, bool) {
|
||||
r := Role(s)
|
||||
if CheckValidRole(r) {
|
||||
return r, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
func (r Role) ToSlice() []Role {
|
||||
return []Role{r}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package convert
|
||||
|
||||
import "github.com/jackc/pgx/v5/pgtype"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func UUIDToString(v pgtype.UUID) string {
|
||||
if v.Valid {
|
||||
@@ -23,10 +27,10 @@ func BoolVal(v pgtype.Bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func TimeToPtr(v pgtype.Timestamptz) *string {
|
||||
if v.Valid {
|
||||
t := v.Time.Format("2006-01-02T15:04:05Z07:00")
|
||||
return &t
|
||||
func TimeToPtr(v pgtype.Timestamptz) *time.Time {
|
||||
if !v.Valid {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
t := v.Time
|
||||
return &t
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func Connect() (*pgxpool.Pool, error) {
|
||||
func NewPostgresqlDB() (*pgxpool.Pool, error) {
|
||||
ctx := context.Background()
|
||||
connectionURI, err := config.GetConfig("PGX_CONNECTION_URI")
|
||||
if err != nil {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package request
|
||||
|
||||
type SignUpDto struct {
|
||||
Password string `json:"password" validate:"required"`
|
||||
DiscordUserId string `json:"discord_user_id" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
}
|
||||
|
||||
type LoginDto struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package request
|
||||
|
||||
type PreSignedDto struct {
|
||||
FileName string `json:"fileName" validate:"required"`
|
||||
ContentType string `json:"contentType" validate:"required"`
|
||||
}
|
||||
|
||||
type PreSignedCompleteDto struct {
|
||||
FileName string `json:"fileName" validate:"required"`
|
||||
MediaId string `json:"mediaId" validate:"required"`
|
||||
PublicUrl string `json:"publicUrl" validate:"required"`
|
||||
}
|
||||
|
||||
type SearchMediaDto struct {
|
||||
MediaId string `query:"media_id" validate:"omitempty"`
|
||||
FileName string `query:"file_name" validate:"omitempty"`
|
||||
SortBy string `query:"sort_by" default:"created_at" validate:"oneof=created_at updated_at"`
|
||||
Order string `query:"order" default:"desc" validate:"oneof=asc desc"`
|
||||
Page int `query:"page" default:"1" validate:"min=1"`
|
||||
Limit int `query:"limit" default:"10" validate:"min=1,max=100"`
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package request
|
||||
|
||||
import "history-api/pkg/constant"
|
||||
|
||||
type CreateUserDto struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
DiscordUserId string `json:"discord_user_id" validate:"required"`
|
||||
Role []constant.Role `json:"role" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateUserDto struct {
|
||||
Password *string `json:"password" validate:"omitempty"`
|
||||
DiscordUserId *string `json:"discord_user_id" validate:"omitempty"`
|
||||
Role *[]constant.Role `json:"role" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type SearchUserDto struct {
|
||||
Username *string `query:"username" validate:"omitempty"`
|
||||
DiscordUserId *string `query:"discord_user_id" validate:"omitempty"`
|
||||
Role *[]constant.Role `query:"role" validate:"omitempty"`
|
||||
SortBy string `query:"sort_by" default:"created_at" validate:"oneof=created_at updated_at"`
|
||||
Order string `query:"order" default:"desc" validate:"oneof=asc desc"`
|
||||
Page int `query:"page" default:"1" validate:"min=1"`
|
||||
Limit int `query:"limit" default:"10" validate:"min=1,max=100"`
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package response
|
||||
|
||||
type AuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"history-api/pkg/constant"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type CommonResponse struct {
|
||||
Status bool `json:"status"`
|
||||
Data any `json:"data"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type JWTClaims struct {
|
||||
UId string `json:"uid"`
|
||||
Roles []constant.Role `json:"roles"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package response
|
||||
|
||||
type PreSignedResponse struct {
|
||||
UploadUrl string `json:"uploadUrl"`
|
||||
PublicUrl string `json:"publicUrl"`
|
||||
FileName string `json:"fileName"`
|
||||
MediaId string `json:"mediaId"`
|
||||
SignedHeaders map[string]string `json:"signedHeaders"`
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package response
|
||||
|
||||
type RoleSimpleResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type RoleResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt *string `json:"created_at"`
|
||||
UpdatedAt *string `json:"updated_at"`
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package response
|
||||
|
||||
type TokenResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
TokenType int16 `json:"token_type"`
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
CreatedAt *string `json:"created_at"`
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package response
|
||||
|
||||
type UserResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt *string `json:"created_at"`
|
||||
UpdatedAt *string `json:"updated_at"`
|
||||
Roles []*RoleSimpleResponse `json:"roles"`
|
||||
}
|
||||
26
pkg/mbtiles/db.go
Normal file
26
pkg/mbtiles/db.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package mbtiles
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/glebarez/go-sqlite"
|
||||
)
|
||||
|
||||
func NewMBTilesDB(path string) (*sql.DB, error) {
|
||||
dsn := fmt.Sprintf("file:%s?mode=ro&_journal_mode=off&_synchronous=off", path)
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(5)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"history-api/pkg/dtos/response"
|
||||
"history-api/pkg/convert"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type RoleSimple struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (r *RoleSimple) ToResponse() *response.RoleSimpleResponse {
|
||||
return &response.RoleSimpleResponse{
|
||||
ID: convert.UUIDToString(r.ID),
|
||||
Name: r.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func RolesToResponse(rs []*RoleSimple) []*response.RoleSimpleResponse {
|
||||
out := make([]*response.RoleSimpleResponse, len(rs))
|
||||
for i := range rs {
|
||||
out[i] = rs[i].ToResponse()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type RoleEntity struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (r *RoleEntity) ToResponse() *response.RoleResponse {
|
||||
return &response.RoleResponse{
|
||||
ID: convert.UUIDToString(r.ID),
|
||||
Name: r.Name,
|
||||
IsDeleted: convert.BoolVal(r.IsDeleted),
|
||||
CreatedAt: convert.TimeToPtr(r.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(r.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func RolesEntityToResponse(rs []*RoleEntity) []*response.RoleResponse {
|
||||
out := make([]*response.RoleResponse, len(rs))
|
||||
for i := range rs {
|
||||
out[i] = rs[i].ToResponse()
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"history-api/pkg/convert"
|
||||
"history-api/pkg/dtos/response"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type TokenEntity struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
TokenType int16 `json:"token_type"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func (t *TokenEntity) ToResponse() *response.TokenResponse {
|
||||
return &response.TokenResponse{
|
||||
ID: convert.UUIDToString(t.ID),
|
||||
UserID: convert.UUIDToString(t.UserID),
|
||||
TokenType: t.TokenType,
|
||||
ExpiresAt: convert.TimeToPtr(t.ExpiresAt),
|
||||
CreatedAt: convert.TimeToPtr(t.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func TokensEntityToResponse(ts []*TokenEntity) []*response.TokenResponse {
|
||||
out := make([]*response.TokenResponse, len(ts))
|
||||
for i := range ts {
|
||||
out[i] = ts[i].ToResponse()
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"history-api/pkg/convert"
|
||||
"history-api/pkg/dtos/response"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type UserEntity struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||||
IsActive pgtype.Bool `json:"is_active"`
|
||||
IsVerified pgtype.Bool `json:"is_verified"`
|
||||
TokenVersion int32 `json:"token_version"`
|
||||
RefreshToken pgtype.Text `json:"refresh_token"`
|
||||
IsDeleted pgtype.Bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Roles []*RoleSimple `json:"roles"`
|
||||
}
|
||||
|
||||
func (u *UserEntity) ParseRoles(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
u.Roles = []*RoleSimple{}
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, &u.Roles)
|
||||
}
|
||||
|
||||
func (u *UserEntity) ToResponse() *response.UserResponse {
|
||||
return &response.UserResponse{
|
||||
ID: convert.UUIDToString(u.ID),
|
||||
Name: u.Name,
|
||||
Email: u.Email,
|
||||
AvatarUrl: convert.TextToString(u.AvatarUrl),
|
||||
IsActive: convert.BoolVal(u.IsActive),
|
||||
IsVerified: convert.BoolVal(u.IsVerified),
|
||||
TokenVersion: u.TokenVersion,
|
||||
IsDeleted: convert.BoolVal(u.IsDeleted),
|
||||
CreatedAt: convert.TimeToPtr(u.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(u.UpdatedAt),
|
||||
Roles: RolesToResponse(u.Roles),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func UsersEntityToResponse(rs []*UserEntity) []*response.UserResponse {
|
||||
out := make([]*response.UserResponse, len(rs))
|
||||
for i := range rs {
|
||||
out[i] = rs[i].ToResponse()
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -41,8 +41,18 @@ func formatValidationError(err error) []ErrorResponse {
|
||||
element.FailedField = fieldError.Field()
|
||||
element.Tag = fieldError.Tag()
|
||||
element.Value = fieldError.Param()
|
||||
element.Message = "Field " + fieldError.Field() + " failed validation on tag '" + fieldError.Tag() + "'"
|
||||
|
||||
switch fieldError.Tag() {
|
||||
case "required":
|
||||
element.Message = fieldError.Field() + " is required"
|
||||
case "min":
|
||||
element.Message = fieldError.Field() + " must be at least " + fieldError.Param() + " characters"
|
||||
case "max":
|
||||
element.Message = fieldError.Field() + " must be at most " + fieldError.Param() + " characters"
|
||||
case "email":
|
||||
element.Message = "Invalid email format"
|
||||
default:
|
||||
element.Message = fieldError.Error()
|
||||
}
|
||||
errorsList = append(errorsList, element)
|
||||
}
|
||||
}
|
||||
@@ -79,4 +89,4 @@ func ValidateBodyDto(c fiber.Ctx, dto any) error {
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user