initial commit on github
This commit is contained in:
@@ -0,0 +1,666 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/gorilla/mux"
|
||||
"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 *mux.Router) {
|
||||
r.HandleFunc("/auth/login", resolveGenerateNewToken).Methods("OPTIONS", "POST")
|
||||
r.HandleFunc("/auth/logout", resolveDeleteToken).Methods("OPTIONS", "POST")
|
||||
r.HandleFunc("/auth/register", resolveRegisterUser).Methods("OPTIONS", "POST")
|
||||
r.HandleFunc("/auth/register/vk", resolveRegisterUserVK).Methods("OPTIONS", "POST")
|
||||
|
||||
r.HandleFunc("/auth/activate", resolveActivateUser).Methods("OPTIONS", "POST")
|
||||
r.HandleFunc("/auth/activateToken", resolveGenerateActivateToken).Methods("OPTIONS", "POST")
|
||||
|
||||
r.HandleFunc("/auth/restore", resolveRestorePassword).Methods("OPTIONS", "POST")
|
||||
r.HandleFunc("/auth/restoreToken", resolveGenerateRestorePassword).Methods("OPTIONS", "POST")
|
||||
}
|
||||
|
||||
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 := ioutil.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(privateConfig.ActivateTokenSecret), 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 := 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 = 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 := FetchUserByLogin(restore.Email)
|
||||
if 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", config.FrontendUrl, restoreToken)}
|
||||
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(privateConfig.ActivateTokenSecret), 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 := FetchUser(int(id))
|
||||
if err == sql.ErrNoRows {
|
||||
NotFound.Ferror(w)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
if user.IsActivated {
|
||||
AlreadyActivated.Ferror(w)
|
||||
return
|
||||
}
|
||||
user, err = 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 = FetchUserByLogin(reg.Login)
|
||||
if 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 := 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", config.FrontendUrl, activateToken)}
|
||||
SendMailTemplate(user.Email, "Активируйте аккаунт", "activate_account.html", mailData)
|
||||
|
||||
embeds := make([]Embed, 1)
|
||||
embeds = append(embeds, Embed{
|
||||
Title: "Nix13",
|
||||
Type: "rich",
|
||||
Description: "Был зарегестрирован новый пользователь!",
|
||||
Timestamp: time.Now(),
|
||||
URL: fmt.Sprintf("https://unmei.space/user/%d", 1),
|
||||
})
|
||||
err = ExecuteWebhook(
|
||||
"https://discord.com/api/webhooks/790593203862241320/vGyrLPzbmtKXHLaOpVrwNwn15tBK58CrsL3rQdAiUQSbi_jBvclpfUT1SoM75j98hBKY",
|
||||
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 := FetchUserByLogin(login.Login)
|
||||
log.Println(login.Login)
|
||||
if 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(privateConfig.AuthTokenSecret))
|
||||
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 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(privateConfig.AuthTokenSecret), 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 := 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", config.FrontendUrl, activateToken)}
|
||||
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 *User) string {
|
||||
claims := jwt.MapClaims{
|
||||
"id": user.ID,
|
||||
"time": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, _ := token.SignedString([]byte(privateConfig.ActivateTokenSecret))
|
||||
hashed := base64.StdEncoding.EncodeToString([]byte(tokenString))
|
||||
return hashed
|
||||
}
|
||||
|
||||
func hasPermission(user *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", privateConfig.RecaptchaSecret, 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 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(privateConfig.AuthTokenSecret), 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 := FetchCurrentUser(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/v1/users") {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.Atoi(vars["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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user