├── websocket ├── chat │ ├── route.go │ └── ws.go └── timer │ ├── route.go │ └── ws.go ├── .env ├── services ├── config.go ├── randomString.go ├── bearer.go ├── bcrypt.go ├── sendemail.go └── jwt.go ├── storage ├── db.go ├── table.go └── user-query.go ├── main.go ├── go.mod ├── route ├── email.go ├── user.go └── auth.go └── go.sum /websocket/chat/route.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func RunWSRoutes(r *gin.Engine) { 6 | r.GET("/ws", func(c *gin.Context) { 7 | handleConnections(c) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /websocket/timer/route.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func RunGameRoutes(r *gin.Engine) { 6 | r.GET("/sub/timer", func(c *gin.Context) { 7 | handleConnections(c) 8 | }) 9 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DSN=postgresql://ethaningenium:uP5LJZt9HDAF@ep-ancient-firefly-a2iwfoe3.eu-central-1.aws.neon.tech/mafia?sslmode=require 2 | SECRET_KEY = Ethan023974 3 | 4 | SENDER_EMAIL = erdanaerboluly@gmail.com 5 | SENDER_EMAIL_PASSWORD = qatklkfslehqoggx -------------------------------------------------------------------------------- /services/config.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/joho/godotenv" 8 | ) 9 | 10 | func InitConfig() { 11 | err := godotenv.Load() 12 | if err != nil { 13 | log.Fatal("Error loading .env file") 14 | } 15 | } 16 | 17 | 18 | func GetEnv(key string) string { 19 | return os.Getenv(key) 20 | } -------------------------------------------------------------------------------- /services/randomString.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | ) 7 | 8 | func GenerateRandomString(length int) (string, error) { 9 | bytes := make([]byte, length/2) 10 | _, err := rand.Read(bytes) 11 | if err != nil { 12 | return "", err 13 | } 14 | return hex.EncodeToString(bytes)[:length], nil 15 | } -------------------------------------------------------------------------------- /services/bearer.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | func ExtractToken(authorizationHeader string) string { 4 | // Проверка, содержит ли заголовок префикс "Bearer " 5 | const bearerPrefix = "Bearer " 6 | if len(authorizationHeader) > len(bearerPrefix) && authorizationHeader[:len(bearerPrefix)] == bearerPrefix { 7 | return authorizationHeader[len(bearerPrefix):] 8 | } 9 | return authorizationHeader 10 | } -------------------------------------------------------------------------------- /services/bcrypt.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | // HashPassword хеширует пароль с использованием bcrypt 6 | func HashPassword(password string) (string, error) { 7 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 5) 8 | if err != nil { 9 | return "", err 10 | } 11 | return string(hashedPassword), nil 12 | } 13 | 14 | // ComparePassword сравнивает пароль с хешем 15 | func ComparePassword(password, hashedPassword string) error { 16 | err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 17 | return err 18 | } -------------------------------------------------------------------------------- /storage/db.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "mafia/services" 7 | 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | type Database struct { 12 | *sql.DB 13 | } 14 | 15 | var database *Database 16 | 17 | func Start(){ 18 | connStr := services.GetEnv("DSN") 19 | db, err := sql.Open("postgres", connStr) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | err = db.Ping() 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | database = &Database{db} 29 | 30 | createTables(database) 31 | } 32 | 33 | func GetDatabase() *Database { 34 | return database 35 | } 36 | 37 | -------------------------------------------------------------------------------- /services/sendemail.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "net/smtp" 5 | ) 6 | 7 | func SendEmail(subject string , html string, to string) error { 8 | email := GetEnv("SENDER_EMAIL") 9 | password := GetEnv("SENDER_EMAIL_PASSWORD") 10 | auth := smtp.PlainAuth( 11 | "", 12 | email, 13 | password, 14 | "smtp.gmail.com", 15 | ) 16 | 17 | headers := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";" 18 | 19 | msg := "Subject: " + subject + "\n" + headers + "\n\n" + html 20 | 21 | err := smtp.SendMail( 22 | "smtp.gmail.com:587", 23 | auth, 24 | email, 25 | []string{to}, 26 | []byte(msg), 27 | ) 28 | 29 | return err 30 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "mafia/route" 5 | "mafia/services" 6 | "mafia/storage" 7 | "mafia/websocket/chat" 8 | "mafia/websocket/timer" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func main() { 14 | // Инициализация конфигурации 15 | services.InitConfig() 16 | 17 | 18 | // Инициализация базы данных 19 | storage.Start() 20 | 21 | // Создание записи в таблице UserAccount 22 | db := storage.GetDatabase() 23 | defer db.Close() 24 | 25 | 26 | // Создание роутов 27 | r := gin.Default() 28 | r.Use(func(c *gin.Context) { 29 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 30 | c.Next() 31 | }) 32 | route.RunAuthRoutes(r) 33 | route.RunVerifyRoutes(r) 34 | route.RunUserRoutes(r) 35 | 36 | 37 | chat.RunWSRoutes(r) 38 | timer.RunGameRoutes(r) 39 | r.Run(":8082") 40 | 41 | } -------------------------------------------------------------------------------- /storage/table.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | 9 | func createTables(db *Database){ 10 | // Создание таблицы UserAccount 11 | _, err := db.Exec(` 12 | CREATE TABLE IF NOT EXISTS UserAccount ( 13 | ID SERIAL PRIMARY KEY, 14 | Email VARCHAR(255) UNIQUE NOT NULL, 15 | Password VARCHAR(255) NOT NULL, 16 | CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 17 | UpdatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP 18 | ) 19 | `) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | // Создание таблицы UserAccessData с внешним ключом, указывающим на UserAccount 25 | _, err = db.Exec(` 26 | CREATE TABLE IF NOT EXISTS UserAccessData ( 27 | ID SERIAL PRIMARY KEY, 28 | IsEmailVerified BOOLEAN NOT NULL, 29 | VerificationCode VARCHAR(255) NOT NULL, 30 | RefreshToken VARCHAR(255) NOT NULL, 31 | UpdatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 32 | UserID INT UNIQUE, 33 | FOREIGN KEY (UserID) REFERENCES UserAccount(ID) 34 | ) 35 | `) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | fmt.Println("Таблицы и индексы успешно созданы.") 41 | } -------------------------------------------------------------------------------- /services/jwt.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/dgrijalva/jwt-go" 8 | ) 9 | 10 | var secretKey = []byte(GetEnv("SECRET_KEY")) 11 | 12 | type CustomClaims struct { 13 | Email string `json:"email"` 14 | jwt.StandardClaims 15 | } 16 | 17 | func GenerateJWTToken(email string, expirationTime time.Time) (string, error) { 18 | 19 | 20 | // Создаем структуру CustomClaims с пользовательскими полями 21 | claims := &CustomClaims{ 22 | Email: email, 23 | StandardClaims: jwt.StandardClaims{ 24 | ExpiresAt: expirationTime.Unix(), 25 | }, 26 | } 27 | 28 | // Создаем новый токен с настройками подписи 29 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 30 | 31 | // Подписываем токен с использованием секретного ключа 32 | signedToken, err := token.SignedString(secretKey) 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | return signedToken, nil 38 | } 39 | 40 | // ParseJWTToken проверяет и парсит JWT токен 41 | func ParseJWTToken(tokenString string) (*CustomClaims, error) { 42 | // Парсим токен с использованием секретного ключа 43 | token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { 44 | return secretKey, nil 45 | }) 46 | 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // Проверяем, что токен действителен и преобразуем его в пользовательские данные 52 | if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { 53 | return claims, nil 54 | } 55 | 56 | return nil, fmt.Errorf("неверный токен") 57 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module mafia 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/bytedance/sonic v1.9.1 // indirect 7 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 9 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 10 | github.com/gin-contrib/sse v0.1.0 // indirect 11 | github.com/gin-gonic/gin v1.9.1 // indirect 12 | github.com/go-playground/locales v0.14.1 // indirect 13 | github.com/go-playground/universal-translator v0.18.1 // indirect 14 | github.com/go-playground/validator/v10 v10.14.0 // indirect 15 | github.com/go-sql-driver/mysql v1.7.1 // indirect 16 | github.com/goccy/go-json v0.10.2 // indirect 17 | github.com/gorilla/websocket v1.5.1 // indirect 18 | github.com/joho/godotenv v1.5.1 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 21 | github.com/leodido/go-urn v1.2.4 // indirect 22 | github.com/lib/pq v1.10.9 // indirect 23 | github.com/mattn/go-isatty v0.0.19 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 27 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 28 | github.com/ugorji/go/codec v1.2.11 // indirect 29 | golang.org/x/arch v0.3.0 // indirect 30 | golang.org/x/crypto v0.18.0 // indirect 31 | golang.org/x/net v0.17.0 // indirect 32 | golang.org/x/sys v0.16.0 // indirect 33 | golang.org/x/text v0.14.0 // indirect 34 | google.golang.org/protobuf v1.30.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /route/email.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "fmt" 5 | "mafia/services" 6 | "mafia/storage" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type emailRequest struct { 12 | Email string `json:"email" binding:"required"` 13 | 14 | } 15 | 16 | func RunVerifyRoutes(r *gin.Engine) { 17 | verify := r.Group("/verify") 18 | verify.GET("/:code", verifyToken) 19 | verify.POST("/send", sendEmail) 20 | } 21 | 22 | func sendEmail (ctx *gin.Context) { 23 | //bind json 24 | var req emailRequest 25 | err := ctx.ShouldBindJSON(&req) 26 | if err != nil { 27 | ctx.JSON(400, gin.H{"error": err.Error()}) 28 | return 29 | } 30 | 31 | //get access to db 32 | db := storage.GetDatabase() 33 | 34 | code, err := storage.GetVerificationCode(db, req.Email) 35 | if err != nil { 36 | ctx.JSON(400, gin.H{"error": err.Error()}) 37 | return 38 | } 39 | 40 | msg := fmt.Sprintf("

Your verification code is:

http://localhost:8082/verify/%s

", code) 41 | 42 | //send email 43 | err = services.SendEmail("Verification", msg, req.Email) 44 | if err != nil { 45 | ctx.JSON(400, gin.H{"error": err.Error()}) 46 | return 47 | } 48 | 49 | //return code 50 | ctx.JSON(200, gin.H{"message": "Email sent"}) 51 | } 52 | 53 | 54 | 55 | 56 | func verifyToken(ctx *gin.Context) { 57 | //get code parameter 58 | code := ctx.Param("code") 59 | 60 | //get access to db 61 | db := storage.GetDatabase() 62 | 63 | //set isEmailVerified to true 64 | userData , err := storage.UpdateIsEmailVerified(db, code, true) 65 | if err != nil { 66 | ctx.JSON(400, gin.H{"error": err.Error()}) 67 | return 68 | } 69 | 70 | //return userData 71 | ctx.JSON(200, gin.H{"data": userData}) 72 | } 73 | 74 | -------------------------------------------------------------------------------- /route/user.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "mafia/services" 5 | "mafia/storage" 6 | 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type userInfo struct { 13 | FullName string `json:"name" binding:"required"` 14 | AvatarUrl string `json:"avatar" binding:"required"` 15 | } 16 | 17 | 18 | func RunUserRoutes(r *gin.Engine) { 19 | user := r.Group("/user") 20 | user.Use(authMiddleware) 21 | user.POST("/info", createInfo) 22 | 23 | } 24 | 25 | func authMiddleware(c *gin.Context) { 26 | 27 | token := c.GetHeader("Authorization") 28 | 29 | 30 | 31 | // Проверка токена 32 | if token == "" { 33 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Неверный JWT токен", "token": token}) 34 | return 35 | } 36 | token = services.ExtractToken(token) 37 | 38 | 39 | 40 | // Парсинг токена 41 | claims , err := services.ParseJWTToken(token) 42 | if err != nil { 43 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error(), "token": token}) 44 | return 45 | } 46 | 47 | c.Set("email", claims.Email) 48 | 49 | // Продолжите выполнение запроса, если токен валиден 50 | c.Next() 51 | } 52 | 53 | func createInfo(c *gin.Context) { 54 | 55 | //get email from context 56 | email, _ := c.Get("email") 57 | emails, _ := email.(string) 58 | 59 | 60 | //get access to db 61 | db := storage.GetDatabase() 62 | 63 | //get id by email 64 | id, err := storage.GetIDByEmail(db, emails) 65 | if err != nil { 66 | c.JSON(400, gin.H{"error": err.Error()}) 67 | return 68 | } 69 | 70 | 71 | //bind json 72 | var req userInfo 73 | err = c.ShouldBindJSON(&req) 74 | if err != nil { 75 | c.JSON(400, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | 79 | //save data 80 | err = storage.CreateUserInfo(db, id, req.FullName, req.AvatarUrl) 81 | if err != nil { 82 | c.JSON(400, gin.H{"error": err.Error()}) 83 | return 84 | } 85 | 86 | 87 | 88 | c.JSON(200, gin.H{"message": "User info created", "id": id, "name": req.FullName, "avatar": req.AvatarUrl}) 89 | } -------------------------------------------------------------------------------- /websocket/chat/ws.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | var upgrader = websocket.Upgrader{ 12 | CheckOrigin: func(r *http.Request) bool { 13 | return true 14 | }, 15 | } 16 | 17 | type Message struct { 18 | Room string `json:"room"` 19 | Sender string `json:"sender"` 20 | Message string `json:"message"` 21 | Command string `json:"command"` 22 | } 23 | 24 | var clients = make(map[*websocket.Conn]*Message) 25 | 26 | func handleConnections(c *gin.Context) { 27 | // Upgrade HTTP request to WebSocket 28 | conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 29 | if err != nil { 30 | log.Fatal(err) 31 | return 32 | } 33 | defer conn.Close() 34 | 35 | 36 | //registration client 37 | var msg Message 38 | err = conn.ReadJSON(&msg) 39 | if err != nil { 40 | log.Println(err) 41 | return 42 | } 43 | defer delete(clients, conn) 44 | defer notifyJoinLeave(msg.Sender, msg.Room, "left the room") 45 | 46 | 47 | clients[conn] = &Message{ 48 | Room: msg.Room, 49 | Sender: msg.Sender, 50 | Message: msg.Message, 51 | Command: msg.Command, 52 | } 53 | notifyJoinLeave(msg.Sender, msg.Room, "joined the room") 54 | 55 | 56 | for { 57 | var msg Message 58 | err := conn.ReadJSON(&msg) 59 | log.Println("message: ", msg) 60 | if err != nil { 61 | log.Println(err) 62 | } 63 | 64 | // Рассылка сообщения всем клиентам в комнате 65 | broadcastMessage(msg) 66 | } 67 | } 68 | 69 | func notifyJoinLeave(sender, room, action string) { 70 | log.Println("notify: ", sender, room, action) 71 | // Создание и отправка уведомления о подключении/отключении 72 | notification := Message{ 73 | Room: room, 74 | Sender: sender, 75 | Message: action, 76 | } 77 | 78 | broadcastMessage(notification) 79 | } 80 | 81 | func broadcastMessage(msg Message) { 82 | for conn, room := range clients { 83 | if room.Room == msg.Room { 84 | err := conn.WriteJSON(msg) 85 | if err != nil { 86 | log.Println(err) 87 | } 88 | } 89 | } 90 | } 91 | 92 | 93 | -------------------------------------------------------------------------------- /websocket/timer/ws.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/gorilla/websocket" 11 | ) 12 | 13 | var Phases = map[string]string{"waiting": "chat", "chat": "action", "action": "choice", "choice" : "chat"} 14 | 15 | type Response struct { 16 | Phase string `json:"phase"` 17 | Time int `json:"time"` 18 | } 19 | 20 | type Message struct { 21 | Room string `json:"room"` 22 | Sender string `json:"sender"` 23 | Message string `json:"message"` 24 | } 25 | 26 | type Game struct { 27 | Owner *websocket.Conn 28 | Players map[*websocket.Conn]string 29 | Timer *time.Ticker // Глобальный таймер 30 | Current int 31 | Phase string 32 | } 33 | 34 | func createGame(owner *websocket.Conn) *Game { 35 | var players = make(map[*websocket.Conn]string) 36 | 37 | players[owner] = "owner" 38 | // Инициализация таймера с периодом 1 секунда 39 | ticker := time.NewTicker(1 * time.Second) 40 | 41 | // Возвращаем созданный объект Game 42 | return &Game{ 43 | Owner: owner, 44 | Players: players, 45 | Timer: ticker, 46 | Current: 0, 47 | Phase: "waiting", 48 | } 49 | } 50 | 51 | var upgrader = websocket.Upgrader{ 52 | CheckOrigin: func(r *http.Request) bool { 53 | return true 54 | }, 55 | } 56 | 57 | var games = make(map[string]*Game) 58 | 59 | func handleConnections(c *gin.Context) { 60 | // Upgrade HTTP request to WebSocket 61 | conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 62 | if err != nil { 63 | log.Fatal(err) 64 | return 65 | } 66 | defer conn.Close() 67 | 68 | // Регистрация клиента 69 | var msg Message 70 | err = conn.ReadJSON(&msg) 71 | if err != nil { 72 | log.Println(err) 73 | return 74 | } 75 | 76 | var game *Game 77 | 78 | // Проверка наличия игры в карте 79 | value, exists := games[msg.Room] 80 | if exists { 81 | value.Players[conn] = msg.Sender 82 | } else { 83 | // Если игры нет, создаем новую 84 | game = createGame(conn) 85 | games[msg.Room] = game 86 | 87 | // Запускаем горутину для управления глобальным таймером 88 | go manageGlobalTimer(game) 89 | 90 | } 91 | for { 92 | time.Sleep(10 * time.Second) 93 | var msg Message 94 | err := conn.ReadJSON(&msg) 95 | log.Println("message: ", msg) 96 | if err != nil { 97 | log.Println(err) 98 | break 99 | } 100 | } 101 | 102 | } 103 | 104 | func manageGlobalTimer(game *Game) { 105 | for range game.Timer.C { 106 | // Обновляем текущее время 107 | if game.Current >= 30 { 108 | game.Phase = Phases[game.Phase] 109 | game.Current = 0 110 | }else{ 111 | game.Current++ 112 | } 113 | 114 | if len(game.Players) == 0 { 115 | game.Phase = "waiting" 116 | game.Current = 0 117 | game.Timer.Stop() 118 | } 119 | // Отправляем всем клиентам обновленные данные 120 | broadcastTimeUpdate(game) 121 | } 122 | } 123 | 124 | func broadcastTimeUpdate(game *Game) { 125 | fmt.Println("Players", game.Players) 126 | // Создаем сообщение с обновленным временем 127 | response := Response{ 128 | Phase: game.Phase, 129 | Time: game.Current, 130 | } 131 | 132 | // Создаем временный срез для соединений 133 | var connectionsToRemove []*websocket.Conn 134 | 135 | // Рассылаем сообщение всем подключенным клиентам 136 | for conn := range game.Players { 137 | if err := conn.WriteJSON(response); err != nil { 138 | // Если произошла ошибка, удаляем соединение 139 | log.Println(err) 140 | // Добавляем соединение в срез для последующего удаления 141 | connectionsToRemove = append(connectionsToRemove, conn) 142 | } 143 | } 144 | 145 | // Удаляем соединения после завершения итерации 146 | for _, conn := range connectionsToRemove { 147 | delete(game.Players, conn) 148 | conn.Close() 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /route/auth.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "mafia/services" 5 | "mafia/storage" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type userRequest struct { 12 | Email string `json:"email" binding:"required"` 13 | Password string `json:"password" binding:"required"` 14 | } 15 | 16 | 17 | 18 | func RunAuthRoutes (r *gin.Engine) { 19 | auth := r.Group("/auth") 20 | auth.POST("/login", login) 21 | auth.POST("/register", register) 22 | } 23 | 24 | func login(c *gin.Context) { 25 | //bind json 26 | var req userRequest 27 | err := c.ShouldBindJSON(&req) 28 | if err != nil { 29 | c.JSON(400, gin.H{"error": err.Error()}) 30 | return 31 | } 32 | 33 | //get access to db 34 | db := storage.GetDatabase() 35 | 36 | //get user 37 | userID, email, password, isEmailVerified, verificationCode, refreshToken, err := storage.GetUserByEmail(db, req.Email) 38 | if err != nil { 39 | c.JSON(400, gin.H{"error": err.Error()}) 40 | return 41 | } 42 | 43 | //compare password 44 | err = services.ComparePassword(req.Password, password) 45 | if err != nil { 46 | c.JSON(400, gin.H{"error": err.Error()}) 47 | return 48 | } 49 | 50 | //get jwt refresh token 51 | token, err := services.GenerateJWTToken(req.Email, time.Now().Add(time.Hour * 30 * 30)) 52 | if err != nil { 53 | c.JSON(400, gin.H{"error": err.Error()}) 54 | return 55 | } 56 | 57 | //save refresh token 58 | userAccess, err := storage.UpdateRefresh(db, userID, token) 59 | if err != nil { 60 | c.JSON(400, gin.H{"error": err.Error()}) 61 | return 62 | } 63 | 64 | 65 | //create access token 66 | accessToken , err := services.GenerateJWTToken(req.Email, time.Now().Add(time.Minute * 20)) 67 | if err != nil { 68 | c.JSON(400, gin.H{"error": err.Error()}) 69 | return 70 | } 71 | 72 | //set headers and cookies 73 | c.Header("access-token", accessToken) 74 | c.SetCookie("refresh-token", userAccess.RefreshToken, 3600, "/", "localhost", false, true) 75 | 76 | 77 | 78 | c.JSON(200, gin.H{"userID": userID, "email": email, "isEmailVerified": isEmailVerified, "verificationCode": verificationCode, "refreshToken": refreshToken}) 79 | } 80 | 81 | func register(c *gin.Context) { 82 | //bind json 83 | var req userRequest 84 | err := c.ShouldBindJSON(&req) 85 | if err != nil { 86 | c.JSON(400, gin.H{"error": err.Error()}) 87 | return 88 | } 89 | 90 | //get access to db 91 | db := storage.GetDatabase() 92 | 93 | //hash password 94 | hashedPassword, err := services.HashPassword(req.Password) 95 | if err != nil { 96 | c.JSON(400, gin.H{"error": err.Error()}) 97 | return 98 | } 99 | 100 | //create user 101 | userID, err := storage.CreateUserAccount(db, req.Email, hashedPassword) 102 | if err != nil { 103 | c.JSON(400, gin.H{"error": err.Error()}) 104 | return 105 | } 106 | 107 | //get jwt refresh token 108 | token, err := services.GenerateJWTToken(req.Email, time.Now().Add(time.Hour * 30 * 30)) 109 | if err != nil { 110 | c.JSON(400, gin.H{"error": err.Error()}) 111 | return 112 | } 113 | 114 | link, err := services.GenerateRandomString(20) 115 | if err != nil { 116 | c.JSON(400, gin.H{"error": err.Error()}) 117 | return 118 | } 119 | 120 | //save refresh token 121 | err = storage.CreateUserAccessData(db, false, link, token, userID) 122 | if err != nil { 123 | c.JSON(400, gin.H{"error": err.Error()}) 124 | return 125 | } 126 | 127 | 128 | //create access token 129 | accessToken , err := services.GenerateJWTToken(req.Email, time.Now().Add(time.Minute * 20)) 130 | if err != nil { 131 | c.JSON(400, gin.H{"error": err.Error()}) 132 | return 133 | } 134 | 135 | //set headers and cookies 136 | c.Header("access-token", accessToken) 137 | c.SetCookie("refresh-token", token, 3600, "/", "localhost", false, true) 138 | 139 | 140 | c.JSON(200, gin.H{"message": "success", "id": userID, "email": req.Email, }) 141 | } -------------------------------------------------------------------------------- /storage/user-query.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | // UserAccount представляет модель данных для таблицы UserAccount. 9 | type UserAccount struct { 10 | ID int 11 | Email string 12 | Password string 13 | CreatedAt time.Time 14 | UpdatedAt time.Time 15 | } 16 | 17 | // UserAccessData представляет модель данных для таблицы UserAccessData. 18 | type UserAccessData struct { 19 | ID int 20 | IsEmailVerified bool 21 | RefreshToken string 22 | UpdatedAt time.Time 23 | UserID int 24 | } 25 | 26 | // CreateUserAccount создает новую запись в таблице UserAccount. 27 | func CreateUserAccount(db *Database, email, password string) (int, error) { 28 | var userID int 29 | 30 | err := db.QueryRow(` 31 | INSERT INTO UserAccount (Email, Password) 32 | VALUES ($1, $2) 33 | RETURNING ID 34 | `, email, password).Scan(&userID) 35 | 36 | if err != nil { 37 | log.Print(err) 38 | return 0, err 39 | } 40 | 41 | return userID, nil 42 | } 43 | 44 | // CreateUserAccessData создает новую запись в таблице UserAccessData. 45 | func CreateUserAccessData(db *Database, isEmailVerified bool, verificationCode string , refreshToken string, userID int) error { 46 | _, err := db.Exec(` 47 | INSERT INTO UserAccessData (IsEmailVerified, VerificationCode, RefreshToken, UserID) 48 | VALUES ($1, $2, $3, $4) 49 | `, isEmailVerified,verificationCode, refreshToken, userID) 50 | 51 | if err != nil { 52 | log.Print(err) 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // GetUserByEmail получает данные пользователя по его электронной почте 60 | func GetUserByEmail(db *Database, email string) (int, string, string, bool, string, string, error) { 61 | var ID int 62 | var password string 63 | var isEmailVerified bool 64 | var verificationCode string 65 | var refreshToken string 66 | 67 | // Выполняем SQL-запрос с использованием INNER JOIN для объединения данных из обеих таблиц 68 | query := ` 69 | SELECT ua.ID, ua.Email, ua.Password, uad.IsEmailVerified, uad.VerificationCode, uad.RefreshToken 70 | FROM UserAccount ua 71 | INNER JOIN UserAccessData uad ON ua.ID = uad.UserID 72 | WHERE ua.Email = $1 73 | ` 74 | 75 | err := db.QueryRow(query, email).Scan(&ID, &email, &password, &isEmailVerified, &verificationCode, &refreshToken) 76 | if err != nil { 77 | log.Fatal(err) 78 | return 0, "", "", false, "", "", err 79 | } 80 | 81 | return ID, email, password, isEmailVerified,verificationCode, refreshToken, nil 82 | } 83 | 84 | 85 | // UpdateAndGetUserAccessData обновляет поля IsEmailVerified и RefreshToken в таблице UserAccessData по UserID и возвращает обновленные данные 86 | func UpdateRefresh(db *Database, userID int, refreshToken string) (UserAccessData, error) { 87 | // Выполняем SQL-запрос UPDATE 88 | updateQuery := ` 89 | UPDATE UserAccessData 90 | SET RefreshToken = $1 91 | WHERE UserID = $2 92 | RETURNING ID, IsEmailVerified, RefreshToken, UpdatedAt, UserID 93 | ` 94 | 95 | var updatedData UserAccessData 96 | err := db.QueryRow(updateQuery, refreshToken, userID).Scan(&updatedData.ID, &updatedData.IsEmailVerified, &updatedData.RefreshToken, &updatedData.UpdatedAt, &updatedData.UserID) 97 | if err != nil { 98 | log.Fatal(err) 99 | return UserAccessData{}, err 100 | } 101 | 102 | return updatedData, nil 103 | } 104 | 105 | func UpdateIsEmailVerified(db *Database, verificationCode string, IsEmailVerified bool) (UserAccessData, error) { 106 | // Выполняем SQL-запрос UPDATE 107 | updateQuery := ` 108 | UPDATE UserAccessData 109 | SET IsEmailVerified = $1 110 | WHERE VerificationCode = $2 111 | RETURNING ID, IsEmailVerified, RefreshToken, UpdatedAt, UserID 112 | ` 113 | 114 | var updatedData UserAccessData 115 | err := db.QueryRow(updateQuery, IsEmailVerified, verificationCode).Scan(&updatedData.ID, &updatedData.IsEmailVerified, &updatedData.RefreshToken, &updatedData.UpdatedAt, &updatedData.UserID) 116 | if err != nil { 117 | log.Fatal(err) 118 | return UserAccessData{}, err 119 | } 120 | 121 | return updatedData, nil 122 | } 123 | 124 | func GetVerificationCode (db *Database, email string) (string, error) { 125 | var verificationCode string 126 | err := db.QueryRow("SELECT VerificationCode FROM UserAccessData WHERE UserID = (SELECT ID FROM UserAccount WHERE Email = $1)", email).Scan(&verificationCode) 127 | if err != nil { 128 | log.Fatal(err) 129 | return "", err 130 | } 131 | return verificationCode, nil 132 | } 133 | 134 | func GetIDByEmail(db *Database, email string) (int, error) { 135 | var id int 136 | err := db.QueryRow("SELECT ID FROM UserAccount WHERE Email = $1", email).Scan(&id) 137 | if err != nil { 138 | log.Fatal(err) 139 | return 0, err 140 | } 141 | return id, nil 142 | } 143 | 144 | func CreateUserInfo (db *Database, ID int, FullName string, AvatarUrl string) (error) { 145 | 146 | err := db.QueryRow("INSERT INTO user_info (fullname, avatar_url, user_id) VALUES ($1, $2, $3) RETURNING ID", FullName, AvatarUrl, ID) 147 | if err != nil { 148 | return err.Err() 149 | } 150 | return nil 151 | } 152 | 153 | 154 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 3 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 4 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 5 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 11 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 12 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 13 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 14 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 15 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 16 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 17 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 18 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 19 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 20 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 21 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 22 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 23 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 24 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 25 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 26 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 27 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 28 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 30 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 31 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 32 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 33 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 34 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 35 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 36 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 37 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 38 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 39 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 40 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 41 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 42 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 43 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 44 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 45 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 48 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 49 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 50 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 51 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 55 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 56 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 57 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 58 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 59 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 60 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 61 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 62 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 63 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 64 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 65 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 66 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 67 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 68 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 69 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 70 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= 71 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 72 | golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= 73 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 74 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 75 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 76 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 77 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 78 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 81 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 83 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 84 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 85 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 86 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 87 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 88 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 89 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 90 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 91 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 94 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 95 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 97 | --------------------------------------------------------------------------------