首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package crypto
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// HashPassword hashes a plain text password using bcrypt.
|
||||
// HashPassword 使用 bcrypt 对明文密码进行加密。
|
||||
//
|
||||
// Parameters:
|
||||
// - password: The plain text password to hash.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The hashed password.
|
||||
// - error: An error if hashing fails.
|
||||
//
|
||||
// Note:
|
||||
// - Uses bcrypt.DefaultCost (10) for hashing.
|
||||
// - The resulting hash is approximately 60 characters long.
|
||||
//
|
||||
// 参数:
|
||||
// - password: 要加密的明文密码。
|
||||
//
|
||||
// 返回值:
|
||||
// - string: 加密后的密码。
|
||||
// - error: 如果加密失败则返回错误。
|
||||
//
|
||||
// 注意:
|
||||
// - 使用 bcrypt.DefaultCost (10) 进行加密。
|
||||
// - 生成的哈希值大约为 60 个字符。
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CheckPasswordHash compares a plain text password with a hashed password.
|
||||
// CheckPasswordHash 比较明文密码和加密密码。
|
||||
//
|
||||
// Parameters:
|
||||
// - password: The plain text password to check.
|
||||
// - hash: The hashed password to compare against.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: true if the password matches the hash, false otherwise.
|
||||
//
|
||||
// 参数:
|
||||
// - password: 要检查的明文密码。
|
||||
// - hash: 要比较的加密密码。
|
||||
//
|
||||
// 返回值:
|
||||
// - bool: 如果密码匹配哈希值则返回 true,否则返回 false。
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package errors
|
||||
|
||||
import "net/http"
|
||||
|
||||
// AppError represents an application-specific error.
|
||||
// AppError 表示应用程序特定的错误。
|
||||
//
|
||||
// This struct extends the standard error interface with additional fields
|
||||
// for HTTP status codes and custom error codes.
|
||||
//
|
||||
// 此结构体扩展了标准的 error 接口,添加了 HTTP 状态码和自定义错误码字段。
|
||||
type AppError struct {
|
||||
StatusCode int
|
||||
Code int
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
// Error 实现了 error 接口。
|
||||
//
|
||||
// Returns:
|
||||
// - string: The error message, including the wrapped error if present.
|
||||
//
|
||||
// 返回值:
|
||||
// - string: 错误消息,如果存在包装的错误则包含该错误。
|
||||
func (e *AppError) Error() string {
|
||||
if e.Err != nil {
|
||||
return e.Message + ": " + e.Err.Error()
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// New creates a new AppError with the specified code and message.
|
||||
// New 创建一个具有指定代码和消息的新 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - code: The custom error code.
|
||||
// - message: The error message.
|
||||
//
|
||||
// Returns:
|
||||
// - *AppError: A pointer to the new AppError instance.
|
||||
//
|
||||
// 参数:
|
||||
// - code: 自定义错误代码。
|
||||
// - message: 错误消息。
|
||||
//
|
||||
// 返回值:
|
||||
// - *AppError: 新创建的 AppError 实例指针。
|
||||
func New(code int, message string) *AppError {
|
||||
return &AppError{
|
||||
StatusCode: http.StatusOK,
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWithErr creates a new AppError with the specified code, message, and wrapped error.
|
||||
// NewWithErr 创建一个具有指定代码、消息和包装错误的新 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - code: The custom error code.
|
||||
// - message: The error message.
|
||||
// - err: The wrapped error.
|
||||
//
|
||||
// Returns:
|
||||
// - *AppError: A pointer to the new AppError instance.
|
||||
//
|
||||
// 参数:
|
||||
// - code: 自定义错误代码。
|
||||
// - message: 错误消息。
|
||||
// - err: 包装的错误。
|
||||
//
|
||||
// 返回值:
|
||||
// - *AppError: 新创建的 AppError 实例指针。
|
||||
func NewWithErr(code int, message string, err error) *AppError {
|
||||
return &AppError{
|
||||
StatusCode: http.StatusOK,
|
||||
Code: code,
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// BadRequest creates an AppError for 400 Bad Request.
|
||||
// BadRequest 创建一个 400 Bad Request 的 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The error message.
|
||||
//
|
||||
// Returns:
|
||||
// - *AppError: A pointer to the BadRequest AppError.
|
||||
//
|
||||
// 参数:
|
||||
// - message: 错误消息。
|
||||
//
|
||||
// 返回值:
|
||||
// - *AppError: BadRequest AppError 的指针。
|
||||
func BadRequest(message string) *AppError {
|
||||
return &AppError{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Code: 400,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Unauthorized creates an AppError for 401 Unauthorized.
|
||||
// Unauthorized 创建一个 401 Unauthorized 的 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The error message.
|
||||
//
|
||||
// Returns:
|
||||
// - *AppError: A pointer to the Unauthorized AppError.
|
||||
//
|
||||
// 参数:
|
||||
// - message: 错误消息。
|
||||
//
|
||||
// 返回值:
|
||||
// - *AppError: Unauthorized AppError 的指针。
|
||||
func Unauthorized(message string) *AppError {
|
||||
return &AppError{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Code: 401,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Forbidden creates an AppError for 403 Forbidden.
|
||||
// Forbidden 创建一个 403 Forbidden 的 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The error message.
|
||||
//
|
||||
// Returns:
|
||||
// - *AppError: A pointer to the Forbidden AppError.
|
||||
//
|
||||
// 参数:
|
||||
// - message: 错误消息。
|
||||
//
|
||||
// 返回值:
|
||||
// - *AppError: Forbidden AppError 的指针。
|
||||
func Forbidden(message string) *AppError {
|
||||
return &AppError{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Code: 403,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Conflict creates an AppError for 409 Conflict.
|
||||
// Conflict 创建一个 409 Conflict 的 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The error message.
|
||||
//
|
||||
// Returns:
|
||||
// - *AppError: A pointer to the Conflict AppError.
|
||||
//
|
||||
// 参数:
|
||||
// - message: 错误消息。
|
||||
//
|
||||
// 返回值:
|
||||
// - *AppError: Conflict AppError 的指针。
|
||||
func Conflict(message string) *AppError {
|
||||
return &AppError{
|
||||
StatusCode: http.StatusConflict,
|
||||
Code: 409,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound creates an AppError for 404 Not Found.
|
||||
// NotFound 创建一个 404 Not Found 的 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The error message.
|
||||
//
|
||||
// Returns:
|
||||
// - *AppError: A pointer to the NotFound AppError.
|
||||
//
|
||||
// 参数:
|
||||
// - message: 错误消息。
|
||||
//
|
||||
// 返回值:
|
||||
// - *AppError: NotFound AppError 的指针。
|
||||
func NotFound(message string) *AppError {
|
||||
return &AppError{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Code: 404,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// InternalError creates an AppError for 500 Internal Server Error.
|
||||
// InternalError 创建一个 500 Internal Server Error 的 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The error message.
|
||||
// - err: The wrapped error (typically the underlying cause).
|
||||
//
|
||||
// Returns:
|
||||
// - *AppError: A pointer to the InternalError AppError.
|
||||
//
|
||||
// 参数:
|
||||
// - message: 错误消息。
|
||||
// - err: 包装的错误(通常是根本原因)。
|
||||
//
|
||||
// 返回值:
|
||||
// - *AppError: InternalError AppError 的指针。
|
||||
func InternalError(message string, err error) *AppError {
|
||||
return &AppError{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Code: 500,
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// IsAppError checks if an error is an AppError.
|
||||
// IsAppError 检查错误是否是 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - err: The error to check.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: true if the error is an AppError, false otherwise.
|
||||
//
|
||||
// 参数:
|
||||
// - err: 要检查的错误。
|
||||
//
|
||||
// 返回值:
|
||||
// - bool: 如果错误是 AppError 则返回 true,否则返回 false。
|
||||
func IsAppError(err error) bool {
|
||||
_, ok := err.(*AppError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// AsAppError attempts to convert an error to an AppError.
|
||||
// AsAppError 尝试将错误转换为 AppError。
|
||||
//
|
||||
// Parameters:
|
||||
// - err: The error to convert.
|
||||
//
|
||||
// Returns:
|
||||
// - *AppError: The converted AppError if successful.
|
||||
// - bool: true if the conversion was successful, false otherwise.
|
||||
//
|
||||
// 参数:
|
||||
// - err: 要转换的错误。
|
||||
//
|
||||
// 返回值:
|
||||
// - *AppError: 如果转换成功则返回转换后的 AppError。
|
||||
// - bool: 如果转换成功则返回 true,否则返回 false。
|
||||
func AsAppError(err error) (*AppError, bool) {
|
||||
e, ok := err.(*AppError)
|
||||
return e, ok
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"cloudnest/internal/pkg/errors"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
UserCode string `json:"user_code"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(userID uint, username, userCode string, secret string, expiresIn time.Duration) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
UserCode: userCode,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)),
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
func ParseToken(tokenString, secret string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Unauthorized("invalid token")
|
||||
}
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
return nil, errors.Unauthorized("invalid token")
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Level represents log level.
|
||||
type Level int
|
||||
|
||||
const (
|
||||
LevelDebug Level = iota
|
||||
LevelInfo
|
||||
LevelWarn
|
||||
LevelError
|
||||
)
|
||||
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case LevelDebug:
|
||||
return "DEBUG"
|
||||
case LevelInfo:
|
||||
return "INFO"
|
||||
case LevelWarn:
|
||||
return "WARN"
|
||||
case LevelError:
|
||||
return "ERROR"
|
||||
default:
|
||||
return "INFO"
|
||||
}
|
||||
}
|
||||
|
||||
// Logger is a simple JSON logger compatible with Go 1.20.
|
||||
type Logger struct {
|
||||
level Level
|
||||
writer io.Writer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Default is the global default logger.
|
||||
var Default atomic.Value
|
||||
|
||||
func init() {
|
||||
Default.Store(&Logger{level: LevelInfo, writer: os.Stderr})
|
||||
}
|
||||
|
||||
func getDefault() *Logger {
|
||||
if l, ok := Default.Load().(*Logger); ok && l != nil {
|
||||
return l
|
||||
}
|
||||
return &Logger{level: LevelInfo, writer: os.Stderr}
|
||||
}
|
||||
|
||||
// Init initializes the logger with the specified log level.
|
||||
func Init(level string) {
|
||||
InitWithWriter(level, os.Stdout)
|
||||
}
|
||||
|
||||
// InitWithWriter initializes the logger with the specified log level and writer.
|
||||
func InitWithWriter(level string, writer io.Writer) {
|
||||
var lvl Level
|
||||
switch level {
|
||||
case "debug":
|
||||
lvl = LevelDebug
|
||||
case "info":
|
||||
lvl = LevelInfo
|
||||
case "warn":
|
||||
lvl = LevelWarn
|
||||
case "error":
|
||||
lvl = LevelError
|
||||
default:
|
||||
lvl = LevelInfo
|
||||
}
|
||||
if writer == nil {
|
||||
writer = os.Stdout
|
||||
}
|
||||
Default.Store(&Logger{level: lvl, writer: writer})
|
||||
}
|
||||
|
||||
func (l *Logger) log(level Level, msg string, args ...any) {
|
||||
if level < l.level {
|
||||
return
|
||||
}
|
||||
record := map[string]any{
|
||||
"time": time.Now().Format(time.RFC3339Nano),
|
||||
"level": level.String(),
|
||||
"msg": msg,
|
||||
}
|
||||
for i := 0; i < len(args)-1; i += 2 {
|
||||
key, ok := args[i].(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
record[key] = args[i+1]
|
||||
}
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
data, _ := json.Marshal(record)
|
||||
fmt.Fprintln(l.writer, string(data))
|
||||
}
|
||||
|
||||
// Debug logs a message at Debug level.
|
||||
func Debug(msg string, args ...any) {
|
||||
getDefault().log(LevelDebug, msg, args...)
|
||||
}
|
||||
|
||||
// Info logs a message at Info level.
|
||||
func Info(msg string, args ...any) {
|
||||
getDefault().log(LevelInfo, msg, args...)
|
||||
}
|
||||
|
||||
// Warn logs a message at Warn level.
|
||||
func Warn(msg string, args ...any) {
|
||||
getDefault().log(LevelWarn, msg, args...)
|
||||
}
|
||||
|
||||
// Error logs a message at Error level.
|
||||
func Error(msg string, args ...any) {
|
||||
getDefault().log(LevelError, msg, args...)
|
||||
}
|
||||
|
||||
// Fatal logs a message at Error level and exits.
|
||||
func Fatal(msg string, args ...any) {
|
||||
getDefault().log(LevelError, msg, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Response represents the standard API response structure.
|
||||
// Response 表示标准的 API 响应结构。
|
||||
//
|
||||
// This struct provides a consistent format for all API responses.
|
||||
//
|
||||
// 此结构体为所有 API 响应提供了一致的格式。
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Success sends a successful response with data.
|
||||
// Success 发送包含数据的成功响应。
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Gin context.
|
||||
// - data: The data to include in the response.
|
||||
//
|
||||
// Note:
|
||||
// - Sets HTTP status code to 200 OK.
|
||||
// - Sets code to 0 (success).
|
||||
//
|
||||
// 参数:
|
||||
// - c: Gin 上下文。
|
||||
// - data: 要包含在响应中的数据。
|
||||
//
|
||||
// 注意:
|
||||
// - 设置 HTTP 状态码为 200 OK。
|
||||
// - 设置 code 为 0(成功)。
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// SuccessMsg sends a successful response with only a message.
|
||||
// SuccessMsg 发送仅包含消息的成功响应。
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Gin context.
|
||||
// - message: The success message.
|
||||
//
|
||||
// Note:
|
||||
// - Sets HTTP status code to 200 OK.
|
||||
// - Sets code to 0 (success).
|
||||
//
|
||||
// 参数:
|
||||
// - c: Gin 上下文。
|
||||
// - message: 成功消息。
|
||||
//
|
||||
// 注意:
|
||||
// - 设置 HTTP 状态码为 200 OK。
|
||||
// - 设置 code 为 0(成功)。
|
||||
func SuccessMsg(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: 0,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Error sends an error response with a custom code and message.
|
||||
// Error 发送包含自定义代码和消息的错误响应。
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Gin context.
|
||||
// - code: The custom error code.
|
||||
// - message: The error message.
|
||||
//
|
||||
// Note:
|
||||
// - Sets HTTP status code to 200 OK.
|
||||
// - The custom code indicates the specific error type.
|
||||
//
|
||||
// 参数:
|
||||
// - c: Gin 上下文。
|
||||
// - code: 自定义错误代码。
|
||||
// - message: 错误消息。
|
||||
//
|
||||
// 注意:
|
||||
// - 设置 HTTP 状态码为 200 OK。
|
||||
// - 自定义代码表示特定的错误类型。
|
||||
func Error(c *gin.Context, code int, message string) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// BadRequest sends a 400 Bad Request response.
|
||||
// BadRequest 发送 400 Bad Request 响应。
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Gin context.
|
||||
// - message: The error message.
|
||||
//
|
||||
// 参数:
|
||||
// - c: Gin 上下文。
|
||||
// - message: 错误消息。
|
||||
func BadRequest(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusBadRequest, Response{
|
||||
Code: 400,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Unauthorized sends a 401 Unauthorized response.
|
||||
// Unauthorized 发送 401 Unauthorized 响应。
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Gin context.
|
||||
// - message: The error message.
|
||||
//
|
||||
// 参数:
|
||||
// - c: Gin 上下文。
|
||||
// - message: 错误消息。
|
||||
func Unauthorized(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusUnauthorized, Response{
|
||||
Code: 401,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// Forbidden sends a 403 Forbidden response.
|
||||
// Forbidden 发送 403 Forbidden 响应。
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Gin context.
|
||||
// - message: The error message.
|
||||
//
|
||||
// 参数:
|
||||
// - c: Gin 上下文。
|
||||
// - message: 错误消息。
|
||||
func Forbidden(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusForbidden, Response{
|
||||
Code: 403,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// NotFound sends a 404 Not Found response.
|
||||
// NotFound 发送 404 Not Found 响应。
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Gin context.
|
||||
// - message: The error message.
|
||||
//
|
||||
// 参数:
|
||||
// - c: Gin 上下文。
|
||||
// - message: 错误消息。
|
||||
func NotFound(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusNotFound, Response{
|
||||
Code: 404,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// InternalError sends a 500 Internal Server Error response.
|
||||
// InternalError 发送 500 Internal Server Error 响应。
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Gin context.
|
||||
// - message: The error message.
|
||||
//
|
||||
// 参数:
|
||||
// - c: Gin 上下文。
|
||||
// - message: 错误消息。
|
||||
func InternalError(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusInternalServerError, Response{
|
||||
Code: 500,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"cloudnest/internal/pkg/logger"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// Validator is the global validator instance.
|
||||
// Validator 是全局验证器实例。
|
||||
var Validator *validator.Validate
|
||||
|
||||
// Init initializes the validator.
|
||||
// Init 初始化验证器。
|
||||
//
|
||||
// Note:
|
||||
// - Should be called once during application startup.
|
||||
// - Creates a new validator instance with default settings.
|
||||
//
|
||||
// 注意:
|
||||
// - 应在应用程序启动时调用一次。
|
||||
// - 使用默认设置创建新的验证器实例。
|
||||
func Init() {
|
||||
Validator = validator.New()
|
||||
logger.Info("validator initialized")
|
||||
}
|
||||
Reference in New Issue
Block a user