FILE / Unmei/Backend
routes/auth.go
Исходный файл и его история в репозитории.
670 lines
15 KiB
Go
670 lines
15 KiB
Go
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)
|
|
})
|
|
}
|