package routes import ( "backend/database" "backend/utils" "bytes" "database/sql" "encoding/base64" "encoding/json" "errors" "fmt" "github.com/getsentry/sentry-go" "github.com/go-chi/chi/v5" "github.com/golang-jwt/jwt/v5" "io" "io/ioutil" "log" "net/http" "os" "strings" "time" "golang.org/x/crypto/bcrypt" ) type Login struct { Login string `json:"login"` Password string `json:"password"` Recaptcha string `json:"recaptcha"` } type Register struct { Login string `json:"login"` Email string `json:"email"` Password string `json:"password"` Recaptcha string `json:"recaptcha"` } type Activate struct { Token string `json:"token"` } func RegisterAuthRoutes(r chi.Router) { r.Post("/login", resolveGenerateNewToken) r.Post("/logout", resolveDeleteToken) r.Post("/register", resolveRegisterUser) r.Post("/register/vk", resolveRegisterUserVK) r.Post("/activate", resolveActivateUser) r.Post("/activateToken", resolveGenerateActivateToken) r.Post("/restore", resolveRestorePassword) r.Post("/restoreToken", resolveGenerateRestorePassword) } var ( loginAttempts = make(map[string][]time.Time) restoreGenerations = make(map[int64]time.Time) activationGenerations = make(map[int]time.Time) ) func resolveRegisterUserVK(w http.ResponseWriter, r *http.Request) { type Body struct { Code string `json:"code"` } body := new(Body) reqBody, err := ReadBody(r) if err != nil { Error(w, err) return } err = json.Unmarshal(reqBody, body) if err != nil { Error(w, err) return } req, err := http.Get("https://oauth.vk.com/access_token?client_id=7836090&client_secret=HqlMicDyJyq4E3SvsPba&redirect_uri=http://localhost:3000/vk_auth&code=" + body.Code) if err != nil { Error(w, err) return } b, err := io.ReadAll(req.Body) if err != nil { Error(w, err) return } log.Println(string(b)) } func resolveRestorePassword(w http.ResponseWriter, r *http.Request) { type Restore struct { Token string `json:"token"` NewPassword string `json:"new_password"` } restore := new(Restore) body, err := ReadBody(r) if err != nil { w.WriteHeader(400) Error(w, err) } err = json.Unmarshal(body, restore) if err != nil { w.WriteHeader(http.StatusBadRequest) return } tokenString, err := base64.StdEncoding.DecodeString(restore.Token) if err != nil { Error(w, err) return } token, err := jwt.Parse(string(tokenString), func(token *jwt.Token) (i interface{}, e error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(os.Getenv("ACTIVATE_TOKEN_SECRET")), nil }) if err != nil { Error(w, err) return } if !token.Valid { RawError(w, "token not valid") return } claims, ok := token.Claims.(jwt.MapClaims) if !ok { RawError(w, "token not valid") return } t, ok := claims["time"].(float64) if !ok { RawError(w, "token not valid") return } id, ok := claims["id"].(float64) if !ok { RawError(w, "token not valid") return } genTime := time.Unix(int64(t), 0) if time.Since(genTime) > time.Hour*1 { TokenExpired.Ferror(w) return } user, err := database.FetchUser(int(id)) if err != nil { Error(w, err) return } samePassword := comparePasswords(restore.NewPassword, user.Password) if samePassword { SamePassword.Ferror(w) return } hashedPassword := hashAndSaltPassword([]byte(restore.NewPassword)) err = database.UpdatePassword(int(id), hashedPassword) Response(w, err, "ok") } func resolveGenerateRestorePassword(w http.ResponseWriter, r *http.Request) { type Restore struct { Email string `json:"email"` Recaptcha string `json:"recaptcha"` } var restore = &Restore{} body, err := ReadBody(r) if err != nil { w.WriteHeader(400) Error(w, err) } err = json.Unmarshal(body, restore) if err != nil { w.WriteHeader(http.StatusBadRequest) return } valid, err := validateRecaptcha(restore.Recaptcha, r.RemoteAddr) if err != nil { w.WriteHeader(http.StatusInternalServerError) Error(w, err) return } if !valid { RecaptchaFailed.Ferror(w) return } user, err := database.FetchUserByLogin(restore.Email) if errors.Is(err, sql.ErrNoRows) { NotFound.Ferror(w) return } if err != nil { Error(w, err) return } t, ok := restoreGenerations[user.ID] if !ok { restoreGenerations[user.ID] = time.Now() } else { if t.Add(time.Minute * 5).After(time.Now()) { TooOftenGeneration.Ferror(w) return } } restoreToken := createActivateToken(user) var mailData = struct { Name string URL string }{Name: user.Username, URL: fmt.Sprintf("%s/restore/%s", utils.GetFrontendURL(), restoreToken)} utils.SendMailTemplate(user.Email, "Восстановление пароля", "restore_password.html", mailData) Response(w, nil, "ok") } func resolveActivateUser(w http.ResponseWriter, r *http.Request) { var activate Activate body, err := ReadBody(r) if err != nil { w.WriteHeader(400) Error(w, err) } err = json.Unmarshal(body, &activate) if err != nil { w.WriteHeader(http.StatusBadRequest) return } tokenString, err := base64.StdEncoding.DecodeString(activate.Token) if err != nil { Error(w, err) return } token, err := jwt.Parse(string(tokenString), func(token *jwt.Token) (i interface{}, e error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(os.Getenv("ACTIVATE_TOKEN_SECRET")), nil }) if err != nil { Error(w, err) return } if !token.Valid { RawError(w, "token not valid") return } claims, ok := token.Claims.(jwt.MapClaims) if !ok { RawError(w, "token not valid") return } t, ok := claims["time"].(float64) if !ok { RawError(w, "token not valid") return } id, ok := claims["id"].(float64) if !ok { RawError(w, "token not valid") return } genTime := time.Unix(int64(t), 0) if time.Since(genTime) > time.Hour*1 { TokenExpired.Ferror(w) return } user, err := database.FetchUser(int(id)) if errors.Is(err, sql.ErrNoRows) { NotFound.Ferror(w) return } if err != nil { Error(w, err) return } if user.IsActivated { AlreadyActivated.Ferror(w) return } user, err = database.ActivateUser(int(id)) Response(w, err, user) } func resolveRegisterUser(w http.ResponseWriter, r *http.Request) { var reg Register body, err := ReadBody(r) if err != nil { w.WriteHeader(400) Error(w, err) } _ = json.Unmarshal(body, ®) if reg.Login == "" || reg.Password == "" || reg.Email == "" || reg.Recaptcha == "" { w.WriteHeader(http.StatusForbidden) RawError(w, "no") return } _, err = database.FetchUserByLogin(reg.Login) if !errors.Is(err, sql.ErrNoRows) { RawError(w, "User already register!") return } valid, err := validateRecaptcha(reg.Recaptcha, r.RemoteAddr) if err != nil || !valid { RecaptchaFailed.Ferror(w) return } user, err := database.CreateUser(reg.Login, hashAndSaltPassword([]byte(reg.Password)), reg.Email) Response(w, err, user) activateToken := createActivateToken(user) var mailData = struct { Name string URL string }{Name: user.Username, URL: fmt.Sprintf("%s/activate/%s", utils.GetFrontendURL(), activateToken)} utils.SendMailTemplate(user.Email, "Активируйте аккаунт", "activate_account.html", mailData) embeds := make([]utils.Embed, 1) embeds = append(embeds, utils.Embed{ Title: "Nix13", Type: "rich", Description: "Был зарегестрирован новый пользователь!", Timestamp: time.Now(), URL: fmt.Sprintf("%s/user/%d", utils.GetFrontendURL(), 1), }) err = utils.ExecuteWebhook( "https://discord.com/api/webhooks/790593203862241320/vGyrLPzbmtKXHLaOpVrwNwn15tBK58CrsL3rQdAiUQSbi_jBvclpfUT1SoM75j98hBKY", utils.WebhookContent{Embeds: embeds}, ) if err != nil { sentry.CaptureException(err) log.Println(err) } } func resolveDeleteToken(w http.ResponseWriter, _ *http.Request) { cookie := &http.Cookie{ Name: "token", Value: "", Path: "/", SameSite: http.SameSiteLaxMode, Expires: time.Time{}, } http.SetCookie(w, cookie) } func resolveGenerateNewToken(w http.ResponseWriter, r *http.Request) { var login Login body, err := ReadBody(r) if err != nil { Error(w, err) return } err = json.Unmarshal(body, &login) if err != nil { Error(w, err) return } userAttempts := append(loginAttempts[r.RemoteAddr], time.Now()) for index, t := range userAttempts { if t.Add(time.Hour).Before(time.Now()) { userAttempts = append(userAttempts[:index], userAttempts[index+1:]...) } } if len(loginAttempts[r.RemoteAddr])+1 >= 5 { if len(login.Recaptcha) == 0 { RecaptchaNeeded.Ferror(w) return } valid, err := validateRecaptcha(login.Recaptcha, r.RemoteAddr) if err != nil { Error(w, err) return } if !valid { RecaptchaFailed.Ferror(w) return } } if len(login.Password) == 0 || len(login.Login) == 0 { w.WriteHeader(http.StatusBadRequest) RawError(w, "No login or/and password") return } user, err := database.FetchUserByLogin(login.Login) log.Println(login.Login) if errors.Is(err, sql.ErrNoRows) || user == nil { WrongPassword.Ferror(w) return } if !comparePasswords(login.Password, user.Password) { WrongPassword.Ferror(w) return } claims := jwt.MapClaims{ "id": user.ID, "is_superuser": user.IsSuperuser, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString([]byte(os.Getenv("AUTH_TOKEN_SECRET"))) if err != nil { Error(w, err) return } expires := time.Now().Add(365 * 24 * time.Hour) cookie := &http.Cookie{ Name: "token", Value: tokenString, Path: "/", Expires: expires, Secure: false, HttpOnly: true, SameSite: http.SameSiteLaxMode, } http.SetCookie(w, cookie) params := r.URL.Query() authType := params.Get("auth_type") switch authType { case "token": Response(w, err, tokenString) default: Response(w, err, user) } } func resolveGenerateActivateToken(w http.ResponseWriter, r *http.Request) { tokenCookie, err := r.Cookie("token") if errors.Is(err, http.ErrNoCookie) { w.WriteHeader(http.StatusUnauthorized) return } if err != nil { Error(w, err) return } token, err := jwt.Parse(tokenCookie.Value, func(token *jwt.Token) (i interface{}, e error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(os.Getenv("AUTH_TOKEN_SECRET")), nil }) if err != nil { w.WriteHeader(http.StatusInternalServerError) Error(w, err) return } if !token.Valid { w.WriteHeader(http.StatusInternalServerError) RawError(w, "Unhandled exception!") return } claims := token.Claims.(jwt.MapClaims) id := int(claims["id"].(float64)) t, ok := activationGenerations[id] if !ok { activationGenerations[id] = time.Now() } else { if t.Add(time.Minute * 5).After(time.Now()) { TooOftenGeneration.Ferror(w) return } } user, err := database.FetchUser(id) if err != nil { Error(w, err) return } if user.IsActivated { AlreadyActivated.Ferror(w) return } activateToken := createActivateToken(user) var mailData = struct { Name string URL string }{Name: user.Username, URL: fmt.Sprintf("%s/activate/%s", utils.GetFrontendURL(), activateToken)} utils.SendMailTemplate(user.Email, "Активируйте аккаунт", "activate_account.html", mailData) Response(w, nil, "ok") } func hashAndSaltPassword(password []byte) string { hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) if err != nil { log.Println(err) sentry.CaptureException(err) } return string(hash) } func comparePasswords(password, hashedPassword string) bool { return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil } func createActivateToken(user *database.User) string { claims := jwt.MapClaims{ "id": user.ID, "time": time.Now().Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, _ := token.SignedString([]byte(os.Getenv("ACTIVATE_TOKEN_SECRET"))) hashed := base64.StdEncoding.EncodeToString([]byte(tokenString)) return hashed } func hasPermission(user *database.User, permission string) bool { if user == nil { return false } permissions := strings.Split(user.Group.Permissions, ",") if user.IsSuperuser || user.Group.IsSuperuser { return true } for _, perm := range permissions { if perm == permission { return true } } return false } func validateRecaptcha(token string, ip string) (bool, error) { url := fmt.Sprintf("https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s", os.Getenv("RECAPTCHA_SECRET"), token, ip) resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(""))) if err != nil { return false, err } validateBody, err := ioutil.ReadAll(resp.Body) if err != nil { return false, err } err = resp.Body.Close() if err != nil { return false, err } var validate map[string]interface{} err = json.Unmarshal(validateBody, &validate) if err != nil { return false, err } if !validate["success"].(bool) { return false, nil } return true, nil } func JWTMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet || r.Method == http.MethodOptions || strings.HasPrefix(r.URL.Path, "/v1/auth") { next.ServeHTTP(w, r) return } tokenCookie, err := r.Cookie("token") if errors.Is(err, http.ErrNoCookie) { w.WriteHeader(http.StatusUnauthorized) return } if err != nil { w.WriteHeader(http.StatusInternalServerError) Error(w, err) return } tokenString := tokenCookie.Value if len(tokenString) == 0 { w.WriteHeader(http.StatusUnauthorized) return } token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, e error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(os.Getenv("AUTH_TOKEN_SECRET")), nil }) if err != nil { w.WriteHeader(http.StatusInternalServerError) Error(w, err) return } if !token.Valid { w.WriteHeader(http.StatusInternalServerError) RawError(w, "Unhandled exception!") return } if strings.HasSuffix(r.URL.Path, "comments") { next.ServeHTTP(w, r) return } claims := token.Claims.(jwt.MapClaims) user, err := GetCurrentUser(r) if err != nil { Error(w, err) return } if strings.HasPrefix(r.URL.Path, "/v1/users") { id := IURLParam(r, "userId") if !strings.HasPrefix(r.URL.Path, "/v1/users/me") && id != int(claims["id"].(float64)) && !hasPermission(user, "users") { NoAccess.Ferror(w) return } } else { path := strings.Split(r.URL.Path, "/") if len(path) < 3 { RawError(w, "something wrong") return } category := path[2] if !hasPermission(user, category) { NoAccess.Ferror(w) return } } next.ServeHTTP(w, r) }) }