Initial commit
This commit is contained in:
1
pkg/auth/interface.go
Normal file
1
pkg/auth/interface.go
Normal file
@@ -0,0 +1 @@
|
||||
package auth
|
||||
1
pkg/auth/jwt.go
Normal file
1
pkg/auth/jwt.go
Normal file
@@ -0,0 +1 @@
|
||||
package auth
|
||||
14
pkg/cache/cache.go
vendored
Normal file
14
pkg/cache/cache.go
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cache defines the interface for caching operations
|
||||
type Cache interface {
|
||||
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error
|
||||
Get(ctx context.Context, key string) (string, error)
|
||||
Del(ctx context.Context, key string) error
|
||||
Close() error
|
||||
}
|
||||
1
pkg/cache/interface.go
vendored
Normal file
1
pkg/cache/interface.go
vendored
Normal file
@@ -0,0 +1 @@
|
||||
package cache
|
||||
43
pkg/cache/redis.go
vendored
Normal file
43
pkg/cache/redis.go
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type redisCache struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
// NewRedisCache creates a new instance of Redis cache implementing Cache interface
|
||||
func NewRedisCache(addr, password string, db int) (Cache, error) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
Password: password,
|
||||
DB: db,
|
||||
})
|
||||
|
||||
if err := rdb.Ping(context.Background()).Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &redisCache{client: rdb}, nil
|
||||
}
|
||||
|
||||
func (c *redisCache) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
return c.client.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
|
||||
func (c *redisCache) Get(ctx context.Context, key string) (string, error) {
|
||||
return c.client.Get(ctx, key).Result()
|
||||
}
|
||||
|
||||
func (c *redisCache) Del(ctx context.Context, key string) error {
|
||||
return c.client.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
func (c *redisCache) Close() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
17
pkg/database/postgres.go
Normal file
17
pkg/database/postgres.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Connect(dsn string) (*gorm.DB, error) {
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
51
pkg/logger/logger.go
Normal file
51
pkg/logger/logger.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Logger defines the interface for logging
|
||||
type Logger interface {
|
||||
Info(msg string, fields ...interface{})
|
||||
Error(msg string, fields ...interface{})
|
||||
Debug(msg string, fields ...interface{})
|
||||
Warn(msg string, fields ...interface{})
|
||||
}
|
||||
|
||||
type zapLogger struct {
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func NewLogger(mode string) Logger {
|
||||
var l *zap.Logger
|
||||
var err error
|
||||
if mode == "release" {
|
||||
l, err = zap.NewProduction()
|
||||
} else {
|
||||
l, err = zap.NewDevelopment()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize logger: %v", err)
|
||||
}
|
||||
|
||||
return &zapLogger{logger: l.Sugar()}
|
||||
}
|
||||
|
||||
func (l *zapLogger) Info(msg string, fields ...interface{}) {
|
||||
l.logger.Infow(msg, fields...)
|
||||
}
|
||||
|
||||
func (l *zapLogger) Error(msg string, fields ...interface{}) {
|
||||
l.logger.Errorw(msg, fields...)
|
||||
}
|
||||
|
||||
func (l *zapLogger) Debug(msg string, fields ...interface{}) {
|
||||
l.logger.Debugw(msg, fields...)
|
||||
}
|
||||
|
||||
func (l *zapLogger) Warn(msg string, fields ...interface{}) {
|
||||
l.logger.Warnw(msg, fields...)
|
||||
}
|
||||
44
pkg/response/response.go
Normal file
44
pkg/response/response.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Success sends a success response with 200 OK
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: http.StatusOK,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Created sends a success response with 201 Created
|
||||
func Created(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusCreated, Response{
|
||||
Code: http.StatusCreated,
|
||||
Message: "created",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// Error sends an error response with the specified status code
|
||||
func Error(c *gin.Context, code int, message string) {
|
||||
c.AbortWithStatusJSON(code, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Fail sends an internal server error response (500)
|
||||
func Fail(c *gin.Context, message string) {
|
||||
Error(c, http.StatusInternalServerError, message)
|
||||
}
|
||||
69
pkg/storage/s3.go
Normal file
69
pkg/storage/s3.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"stream.api/internal/config"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
GeneratePresignedURL(key string, expire time.Duration) (string, error)
|
||||
Delete(key string) error
|
||||
}
|
||||
|
||||
type s3Provider struct {
|
||||
client *s3.Client
|
||||
presignClient *s3.PresignClient
|
||||
bucket string
|
||||
}
|
||||
|
||||
func NewS3Provider(cfg *config.Config) (Provider, error) {
|
||||
awsCfg, err := awsconfig.LoadDefaultConfig(
|
||||
context.TODO(),
|
||||
awsconfig.WithRegion(cfg.AWS.Region),
|
||||
awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.AWS.AccessKey, cfg.AWS.SecretKey, "")),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.UsePathStyle = cfg.AWS.ForcePathStyle
|
||||
if cfg.AWS.Endpoint != "" {
|
||||
o.BaseEndpoint = aws.String(cfg.AWS.Endpoint)
|
||||
}
|
||||
})
|
||||
|
||||
return &s3Provider{
|
||||
client: client,
|
||||
presignClient: s3.NewPresignClient(client),
|
||||
bucket: cfg.AWS.Bucket,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *s3Provider) GeneratePresignedURL(key string, expire time.Duration) (string, error) {
|
||||
req, err := s.presignClient.PresignPutObject(context.TODO(), &s3.PutObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(key),
|
||||
}, func(o *s3.PresignOptions) {
|
||||
o.Expires = expire
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return req.URL, nil
|
||||
}
|
||||
|
||||
func (s *s3Provider) Delete(key string) error {
|
||||
_, err := s.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
return err
|
||||
}
|
||||
26
pkg/token/interface.go
Normal file
26
pkg/token/interface.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package token
|
||||
|
||||
// TokenPair contains the access and refresh tokens
|
||||
type TokenPair struct {
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
AccessUUID string
|
||||
RefreshUUID string
|
||||
AtExpires int64
|
||||
RtExpires int64
|
||||
}
|
||||
|
||||
// Claims defines the JWT claims (User)
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
TokenID string `json:"token_id"`
|
||||
}
|
||||
|
||||
// Provider defines the interface for token operations
|
||||
type Provider interface {
|
||||
GenerateTokenPair(userID, email, role string) (*TokenPair, error)
|
||||
ParseToken(tokenString string) (*Claims, error)
|
||||
ParseMapToken(tokenString string) (map[string]interface{}, error)
|
||||
}
|
||||
119
pkg/token/jwt.go
Normal file
119
pkg/token/jwt.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// jwtClaims is an internal struct to satisfy jwt.Claims interface
|
||||
type jwtClaims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
TokenID string `json:"token_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// jwt.go implements the Provider interface using JWT
|
||||
|
||||
type jwtProvider struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
// NewJWTProvider creates a new instance of JWT provider
|
||||
func NewJWTProvider(secret string) Provider {
|
||||
return &jwtProvider{
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateTokenPair generates new access and refresh tokens
|
||||
func (p *jwtProvider) GenerateTokenPair(userID, email, role string) (*TokenPair, error) {
|
||||
td := &TokenPair{}
|
||||
td.AtExpires = time.Now().Add(time.Minute * 15).Unix()
|
||||
td.AccessUUID = uuid.New().String()
|
||||
|
||||
td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix() // Expires in 7 days
|
||||
td.RefreshUUID = uuid.New().String()
|
||||
|
||||
// Access Token
|
||||
atClaims := &jwtClaims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
TokenID: td.AccessUUID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Unix(td.AtExpires, 0)),
|
||||
Issuer: "stream.api",
|
||||
},
|
||||
}
|
||||
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
|
||||
var err error
|
||||
td.AccessToken, err = at.SignedString([]byte(p.secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Refresh Token
|
||||
// Refresh token can just be a random string or a JWT.
|
||||
// Common practice: JWT for stateless verification, or Opaque string for stateful.
|
||||
// Here we use JWT so we can carry some metadata if needed, but we check Redis anyway.
|
||||
rtClaims := jwt.MapClaims{}
|
||||
rtClaims["refresh_uuid"] = td.RefreshUUID
|
||||
rtClaims["user_id"] = userID
|
||||
rtClaims["exp"] = td.RtExpires
|
||||
rt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims)
|
||||
td.RefreshToken, err = rt.SignedString([]byte(p.secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return td, nil
|
||||
}
|
||||
|
||||
// ParseToken parses the access token returning Claims
|
||||
func (p *jwtProvider) ParseToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &jwtClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(p.secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*jwtClaims); ok && token.Valid {
|
||||
return &Claims{
|
||||
UserID: claims.UserID,
|
||||
Email: claims.Email,
|
||||
Role: claims.Role,
|
||||
TokenID: claims.TokenID,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// ParseMapToken parses token returning map[string]interface{} (generic)
|
||||
func (p *jwtProvider) ParseMapToken(tokenString string) (map[string]interface{}, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(p.secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, jwt.ErrTokenInvalidClaims
|
||||
}
|
||||
Reference in New Issue
Block a user