package utils import ( "crypto/rand" "encoding/base64" "fmt" "os" "strings" "time" "rijig/config" "github.com/golang-jwt/jwt/v5" ) type TokenType string const ( TokenTypePartial TokenType = "partial" TokenTypeFull TokenType = "full" TokenTypeRefresh TokenType = "refresh" ) const ( RegStatusIncomplete = "uncomplete" RegStatusPending = "awaiting_approval" RegStatusConfirmed = "approved" RegStatusComplete = "complete" RegStatusRejected = "rejected" ) const ( ProgressOTPVerified = 1 ProgressDataSubmitted = 2 ProgressComplete = 3 ) type JWTClaims struct { UserID string `json:"user_id"` Role string `json:"role"` DeviceID string `json:"device_id"` RegistrationStatus string `json:"registration_status"` RegistrationProgress int `json:"registration_progress"` TokenType TokenType `json:"token_type"` SessionID string `json:"session_id,omitempty"` jwt.RegisteredClaims } type RefreshTokenData struct { RefreshToken string `json:"refresh_token"` ExpiresAt int64 `json:"expires_at"` DeviceID string `json:"device_id"` Role string `json:"role"` RegistrationStatus string `json:"registration_status"` RegistrationProgress int `json:"registration_progress"` SessionID string `json:"session_id"` CreatedAt int64 `json:"created_at"` } type TokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token,omitempty"` ExpiresIn int64 `json:"expires_in"` TokenType TokenType `json:"token_type"` RegistrationStatus string `json:"registration_status"` RegistrationProgress int `json:"registration_progress"` NextStep string `json:"next_step,omitempty"` SessionID string `json:"session_id"` RequiresAdminApproval bool `json:"requires_admin_approval,omitempty"` } type RegistrationStepInfo struct { Step int `json:"step"` Status string `json:"status"` Description string `json:"description"` RequiresAdminApproval bool `json:"requires_admin_approval"` IsAccessible bool `json:"is_accessible"` IsCompleted bool `json:"is_completed"` } func GetTTLFromEnv(key string, fallback time.Duration) time.Duration { raw := os.Getenv(key) if raw == "" { return fallback } ttl, err := time.ParseDuration(raw) if err != nil { return fallback } return ttl } var ( ACCESS_TOKEN_EXPIRY = GetTTLFromEnv("ACCESS_TOKEN_EXPIRY", 23*time.Hour) REFRESH_TOKEN_EXPIRY = GetTTLFromEnv("REFRESH_TOKEN_EXPIRY", 28*24*time.Hour) PARTIAL_TOKEN_EXPIRY = GetTTLFromEnv("PARTIAL_TOKEN_EXPIRY", 2*time.Hour) ) func GenerateSessionID() (string, error) { bytes := make([]byte, 16) _, err := rand.Read(bytes) if err != nil { return "", fmt.Errorf("failed to generate session ID: %v", err) } return base64.URLEncoding.EncodeToString(bytes), nil } func GenerateJTI() string { bytes := make([]byte, 16) if _, err := rand.Read(bytes); err != nil { return fmt.Sprintf("%d", time.Now().UnixNano()) } return base64.URLEncoding.EncodeToString(bytes) } func ConstantTimeCompare(a, b string) bool { if len(a) != len(b) { return false } result := 0 for i := 0; i < len(a); i++ { result |= int(a[i]) ^ int(b[i]) } return result == 0 } func ExtractTokenFromHeader(authHeader string) (string, error) { if authHeader == "" { return "", fmt.Errorf("authorization header is empty") } const bearerPrefix = "Bearer " if len(authHeader) < len(bearerPrefix) || !strings.HasPrefix(authHeader, bearerPrefix) { return "", fmt.Errorf("invalid authorization header format") } token := strings.TrimSpace(authHeader[len(bearerPrefix):]) if token == "" { return "", fmt.Errorf("token is empty") } return token, nil } func GenerateAccessToken(userID, role, deviceID, registrationStatus string, registrationProgress int, tokenType TokenType, sessionID string) (string, error) { secretKey := config.GetSecretKey() if secretKey == "" { return "", fmt.Errorf("secret key not found") } if userID == "" || role == "" || deviceID == "" { return "", fmt.Errorf("required fields cannot be empty") } now := time.Now() expiry := ACCESS_TOKEN_EXPIRY if tokenType == TokenTypePartial { expiry = PARTIAL_TOKEN_EXPIRY } claims := JWTClaims{ UserID: userID, Role: role, DeviceID: deviceID, RegistrationStatus: registrationStatus, RegistrationProgress: registrationProgress, TokenType: tokenType, SessionID: sessionID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(expiry)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), Issuer: "rijig-api", Subject: userID, ID: GenerateJTI(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(secretKey)) if err != nil { return "", fmt.Errorf("failed to sign token: %v", err) } return tokenString, nil } func GenerateRefreshToken() (string, error) { bytes := make([]byte, 32) _, err := rand.Read(bytes) if err != nil { return "", fmt.Errorf("failed to generate refresh token: %v", err) } return base64.URLEncoding.EncodeToString(bytes), nil } func ValidateAccessToken(tokenString string) (*JWTClaims, error) { secretKey := config.GetSecretKey() if secretKey == "" { return nil, fmt.Errorf("secret key not found") } 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(secretKey), nil }) if err != nil { return nil, fmt.Errorf("failed to parse token: %v", err) } if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { if IsTokenBlacklisted(claims.ID) { return nil, fmt.Errorf("token has been revoked") } return claims, nil } return nil, fmt.Errorf("invalid token") } func ValidateTokenWithChecks(tokenString string, requiredTokenType TokenType, requireCompleteReg bool) (*JWTClaims, error) { claims, err := ValidateAccessToken(tokenString) if err != nil { return nil, err } if requiredTokenType != "" && claims.TokenType != requiredTokenType { return nil, fmt.Errorf("invalid token type: expected %s, got %s", requiredTokenType, claims.TokenType) } if requireCompleteReg && !IsRegistrationComplete(claims.RegistrationStatus) { return nil, fmt.Errorf("registration not complete") } return claims, nil } func ValidateTokenForStep(tokenString string, role string, requiredStep int) (*JWTClaims, error) { claims, err := ValidateAccessToken(tokenString) if err != nil { return nil, err } if claims.Role != role { return nil, fmt.Errorf("role mismatch") } if claims.RegistrationProgress < requiredStep { return nil, fmt.Errorf("step not accessible yet: current step %d, required step %d", claims.RegistrationProgress, requiredStep) } return claims, nil } func StoreRefreshToken(userID, deviceID, refreshToken, role, registrationStatus string, registrationProgress int, sessionID string) error { key := fmt.Sprintf("refresh_token:%s:%s", userID, deviceID) DeleteCache(key) data := RefreshTokenData{ RefreshToken: refreshToken, ExpiresAt: time.Now().Add(REFRESH_TOKEN_EXPIRY).Unix(), DeviceID: deviceID, Role: role, RegistrationStatus: registrationStatus, RegistrationProgress: registrationProgress, SessionID: sessionID, CreatedAt: time.Now().Unix(), } err := SetCache(key, data, REFRESH_TOKEN_EXPIRY) if err != nil { return fmt.Errorf("failed to store refresh token: %v", err) } return nil } func ValidateRefreshToken(userID, deviceID, refreshToken string) (*RefreshTokenData, error) { key := fmt.Sprintf("refresh_token:%s:%s", userID, deviceID) var data RefreshTokenData err := GetCache(key, &data) if err != nil { return nil, fmt.Errorf("refresh token not found or invalid") } if !ConstantTimeCompare(data.RefreshToken, refreshToken) { return nil, fmt.Errorf("refresh token mismatch") } if time.Now().Unix() > data.ExpiresAt { DeleteCache(key) return nil, fmt.Errorf("refresh token expired") } return &data, nil } func RefreshAccessToken(userID, deviceID, refreshToken string) (*TokenResponse, error) { data, err := ValidateRefreshToken(userID, deviceID, refreshToken) if err != nil { return nil, err } tokenType := DetermineTokenType(data.RegistrationStatus) accessToken, err := GenerateAccessToken( userID, data.Role, deviceID, data.RegistrationStatus, data.RegistrationProgress, tokenType, data.SessionID, ) if err != nil { return nil, fmt.Errorf("failed to generate new access token: %v", err) } expiry := ACCESS_TOKEN_EXPIRY if tokenType == TokenTypePartial { expiry = PARTIAL_TOKEN_EXPIRY } nextStep := GetNextRegistrationStep(data.Role, data.RegistrationProgress, data.RegistrationStatus) requiresAdminApproval := RequiresAdminApproval(data.Role, data.RegistrationProgress, data.RegistrationStatus) return &TokenResponse{ AccessToken: accessToken, ExpiresIn: int64(expiry.Seconds()), TokenType: tokenType, RegistrationStatus: data.RegistrationStatus, RegistrationProgress: data.RegistrationProgress, NextStep: nextStep, SessionID: data.SessionID, RequiresAdminApproval: requiresAdminApproval, }, nil } func GenerateTokenPair(userID, role, deviceID, registrationStatus string, registrationProgress int) (*TokenResponse, error) { if userID == "" || role == "" || deviceID == "" { return nil, fmt.Errorf("required parameters cannot be empty") } sessionID, err := GenerateSessionID() if err != nil { return nil, fmt.Errorf("failed to generate session ID: %v", err) } tokenType := DetermineTokenType(registrationStatus) accessToken, err := GenerateAccessToken( userID, role, deviceID, registrationStatus, registrationProgress, tokenType, sessionID, ) if err != nil { return nil, err } refreshToken, err := GenerateRefreshToken() if err != nil { return nil, err } err = StoreRefreshToken(userID, deviceID, refreshToken, role, registrationStatus, registrationProgress, sessionID) if err != nil { return nil, err } expiry := ACCESS_TOKEN_EXPIRY if tokenType == TokenTypePartial { expiry = PARTIAL_TOKEN_EXPIRY } nextStep := GetNextRegistrationStep(role, registrationProgress, registrationStatus) requiresAdminApproval := RequiresAdminApproval(role, registrationProgress, registrationStatus) return &TokenResponse{ AccessToken: accessToken, RefreshToken: refreshToken, ExpiresIn: int64(expiry.Seconds()), TokenType: tokenType, RegistrationStatus: registrationStatus, RegistrationProgress: registrationProgress, NextStep: nextStep, SessionID: sessionID, RequiresAdminApproval: requiresAdminApproval, }, nil } func GenerateTokenForRole(userID, role, deviceID string, progress int, status string) (*TokenResponse, error) { switch role { case RoleAdministrator: return GenerateTokenPair(userID, role, deviceID, RegStatusComplete, ProgressComplete) default: return GenerateTokenPair(userID, role, deviceID, status, progress) } } func DetermineTokenType(registrationStatus string) TokenType { if registrationStatus == RegStatusComplete { return TokenTypeFull } return TokenTypePartial } func IsRegistrationComplete(registrationStatus string) bool { return registrationStatus == RegStatusComplete } func RequiresAdminApproval(role string, progress int, status string) bool { switch role { case RolePengelola, RolePengepul: return progress == ProgressDataSubmitted && status == RegStatusPending default: return false } } func GetNextRegistrationStep(role string, progress int, status string) string { switch role { case RoleAdministrator: return "completed" case RoleMasyarakat: switch progress { case ProgressOTPVerified: return "complete_personal_data" case ProgressDataSubmitted: return "create_pin" case ProgressComplete: return "completed" } case RolePengepul: switch progress { case ProgressOTPVerified: return "upload_ktp" case ProgressDataSubmitted: if status == RegStatusPending { return "awaiting_admin_approval" } else if status == RegStatusConfirmed { return "create_pin" } case ProgressComplete: return "completed" } case RolePengelola: switch progress { case ProgressOTPVerified: return "complete_company_data" case ProgressDataSubmitted: if status == RegStatusPending { return "awaiting_admin_approval" } else if status == RegStatusConfirmed { return "create_pin" } case ProgressComplete: return "completed" } } return "unknown" } func GetRegistrationStepInfo(role string, currentProgress int, currentStatus string) *RegistrationStepInfo { switch role { case RoleAdministrator: return &RegistrationStepInfo{ Step: ProgressComplete, Status: RegStatusComplete, Description: "Administrator registration complete", IsAccessible: true, IsCompleted: true, } case RoleMasyarakat: return getMasyarakatStepInfo(currentProgress, currentStatus) case RolePengepul: return getPengepulStepInfo(currentProgress, currentStatus) case RolePengelola: return getPengelolaStepInfo(currentProgress, currentStatus) } return &RegistrationStepInfo{ Step: 0, Status: "unknown", Description: "Unknown role", IsAccessible: false, IsCompleted: false, } } func getMasyarakatStepInfo(progress int, status string) *RegistrationStepInfo { switch progress { case ProgressOTPVerified: return &RegistrationStepInfo{ Step: ProgressOTPVerified, Status: status, Description: "Complete personal data", IsAccessible: true, IsCompleted: false, } case ProgressDataSubmitted: return &RegistrationStepInfo{ Step: ProgressDataSubmitted, Status: status, Description: "Create PIN", IsAccessible: true, IsCompleted: false, } case ProgressComplete: return &RegistrationStepInfo{ Step: ProgressComplete, Status: RegStatusComplete, Description: "Registration complete", IsAccessible: true, IsCompleted: true, } } return nil } func getPengepulStepInfo(progress int, status string) *RegistrationStepInfo { switch progress { case ProgressOTPVerified: return &RegistrationStepInfo{ Step: ProgressOTPVerified, Status: status, Description: "Upload KTP", IsAccessible: true, IsCompleted: false, } case ProgressDataSubmitted: if status == RegStatusPending { return &RegistrationStepInfo{ Step: ProgressDataSubmitted, Status: status, Description: "Awaiting admin approval", RequiresAdminApproval: true, IsAccessible: false, IsCompleted: false, } } else if status == RegStatusConfirmed { return &RegistrationStepInfo{ Step: ProgressDataSubmitted, Status: status, Description: "Create PIN", IsAccessible: true, IsCompleted: false, } } case ProgressComplete: return &RegistrationStepInfo{ Step: ProgressComplete, Status: RegStatusComplete, Description: "Registration complete", IsAccessible: true, IsCompleted: true, } } return nil } func getPengelolaStepInfo(progress int, status string) *RegistrationStepInfo { switch progress { case ProgressOTPVerified: return &RegistrationStepInfo{ Step: ProgressOTPVerified, Status: status, Description: "Complete company data", IsAccessible: true, IsCompleted: false, } case ProgressDataSubmitted: if status == RegStatusPending { return &RegistrationStepInfo{ Step: ProgressDataSubmitted, Status: status, Description: "Awaiting admin approval", RequiresAdminApproval: true, IsAccessible: false, IsCompleted: false, } } else if status == RegStatusConfirmed { return &RegistrationStepInfo{ Step: ProgressDataSubmitted, Status: status, Description: "Create PIN", IsAccessible: true, IsCompleted: false, } } case ProgressComplete: return &RegistrationStepInfo{ Step: ProgressComplete, Status: RegStatusComplete, Description: "Registration complete", IsAccessible: true, IsCompleted: true, } } return nil } func RevokeRefreshToken(userID, deviceID string) error { key := fmt.Sprintf("refresh_token:%s:%s", userID, deviceID) err := DeleteCache(key) if err != nil { return fmt.Errorf("failed to revoke refresh token: %v", err) } return nil } func RevokeAllRefreshTokens(userID string) error { pattern := fmt.Sprintf("refresh_token:%s:*", userID) err := ScanAndDelete(pattern) if err != nil { return fmt.Errorf("failed to revoke all refresh tokens: %v", err) } return nil } func BlacklistToken(jti string, expiresAt time.Time) error { key := fmt.Sprintf("blacklist:%s", jti) ttl := time.Until(expiresAt) if ttl <= 0 { return nil } return SetCache(key, true, ttl) } func IsTokenBlacklisted(jti string) bool { key := fmt.Sprintf("blacklist:%s", jti) var exists bool err := GetCache(key, &exists) return err == nil && exists }