migration to chi

This commit is contained in:
2025-03-12 16:30:40 +03:00
parent ae81fbcce9
commit 45bb32e7a6
46 changed files with 1352 additions and 1323 deletions
+669
View File
@@ -0,0 +1,669 @@
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, &reg)
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)
})
}