migration to chi
This commit is contained in:
+669
@@ -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, ®)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func RegisterCharactersRoutes(r chi.Router) {
|
||||
r.Route("/", func(s chi.Router) {
|
||||
s.Get("/", getCharacters)
|
||||
s.Post("/", createCharacter)
|
||||
})
|
||||
r.Route("/{id:[0-9]+}", func(s chi.Router) {
|
||||
s.Get("/", getCharacter)
|
||||
s.Put("/", updateCharacter)
|
||||
s.Delete("/", deleteCharacter)
|
||||
})
|
||||
r.Get("/characters/{id:[0-9]+}/novels", getCharacterNovels)
|
||||
}
|
||||
|
||||
func getCharacters(w http.ResponseWriter, r *http.Request) {
|
||||
characters, err := database.FetchCharacters()
|
||||
Response(w, err, characters)
|
||||
}
|
||||
func createCharacter(w http.ResponseWriter, r *http.Request) {
|
||||
Response(w, nil, "todo")
|
||||
}
|
||||
|
||||
func getCharacter(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
char, err := database.FetchCharacter(id)
|
||||
Response(w, err, char)
|
||||
}
|
||||
func updateCharacter(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := GetCurrentUser(r)
|
||||
if err != nil {
|
||||
NoAccess.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
hasPerm := hasPermission(user, "characters")
|
||||
if !hasPerm {
|
||||
NoAccess.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
character := new(database.Character)
|
||||
err = json.Unmarshal(body, character)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
character, err = database.UpdateCharacter(character)
|
||||
Response(w, err, character)
|
||||
}
|
||||
func deleteCharacter(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
|
||||
user, err := GetCurrentUser(r)
|
||||
if err != nil {
|
||||
NoAccess.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
hasPerm := hasPermission(user, "characters")
|
||||
if !hasPerm {
|
||||
NoAccess.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
err = database.DeleteCharacter(id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
NotFound.Ferror(w)
|
||||
return
|
||||
}
|
||||
Response(w, err, "ok")
|
||||
}
|
||||
|
||||
func getCharacterNovels(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
novels, err := database.FetchCharacterNovels(id)
|
||||
Response(w, err, novels)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func RegisterClubRoutes(r chi.Router) {
|
||||
r.Route("/", func(s chi.Router) {
|
||||
s.Get("/", getClubs)
|
||||
})
|
||||
r.Route("/{id:[0-9]+}", func(s chi.Router) {
|
||||
s.Get("/", getClub)
|
||||
})
|
||||
}
|
||||
|
||||
func getClubs(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := GetCurrentUserID(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
clubs, err := database.GetClubs(int(userId))
|
||||
Response(w, err, clubs)
|
||||
}
|
||||
|
||||
func getClub(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
|
||||
userId, err := GetCurrentUserID(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
club, err := database.GetClub(id, userId)
|
||||
Response(w, err, club)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
type ApiError struct {
|
||||
Code uint8 `json:"code"`
|
||||
Text string `json:"text"`
|
||||
HTTPCode uint16 `json:"-"`
|
||||
}
|
||||
|
||||
var (
|
||||
WrongPassword = ApiError{
|
||||
Code: 1,
|
||||
Text: "Wrong login and/or password!",
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
}
|
||||
RecaptchaFailed = ApiError{
|
||||
Code: 2,
|
||||
Text: "Recaptcha check failed!",
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
}
|
||||
NoToken = ApiError{
|
||||
Code: 3,
|
||||
Text: "No authorization token passed!",
|
||||
HTTPCode: http.StatusUnauthorized,
|
||||
}
|
||||
NoAccess = ApiError{
|
||||
Code: 4,
|
||||
Text: "No Access!",
|
||||
HTTPCode: http.StatusForbidden,
|
||||
}
|
||||
AlreadyActivated = ApiError{
|
||||
Code: 5,
|
||||
Text: "Account already activated!",
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
}
|
||||
TokenExpired = ApiError{
|
||||
Code: 6,
|
||||
Text: "Activate token has expired",
|
||||
HTTPCode: 400,
|
||||
}
|
||||
TooOftenGeneration = ApiError{
|
||||
Code: 7,
|
||||
Text: "Too often generating links",
|
||||
HTTPCode: 400,
|
||||
}
|
||||
SamePassword = ApiError{
|
||||
Code: 8,
|
||||
Text: "Same password",
|
||||
HTTPCode: 400,
|
||||
}
|
||||
RecaptchaNeeded = ApiError{
|
||||
Code: 9,
|
||||
Text: "Recaptcha needed",
|
||||
HTTPCode: 0,
|
||||
}
|
||||
NotFound = ApiError{
|
||||
Code: 100,
|
||||
Text: "Not found!",
|
||||
HTTPCode: http.StatusNotFound,
|
||||
}
|
||||
TooManyRequests = ApiError{
|
||||
Code: 101,
|
||||
Text: "Too many requests",
|
||||
HTTPCode: 429,
|
||||
}
|
||||
Unauthorized = ApiError{
|
||||
Code: 102,
|
||||
Text: "Unauthorized",
|
||||
HTTPCode: http.StatusUnauthorized,
|
||||
}
|
||||
)
|
||||
|
||||
func (e ApiError) Marshall() string {
|
||||
data, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (e ApiError) Ferror(w http.ResponseWriter) {
|
||||
const baseError = `{"error":true,"error_data":%v}`
|
||||
if e.HTTPCode > 0 {
|
||||
w.WriteHeader(int(e.HTTPCode))
|
||||
}
|
||||
|
||||
data, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(w, baseError, string(data))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func RegisterGamesRoutes(r chi.Router) {
|
||||
r.Get("/{id:[0-9]+}", getGame)
|
||||
}
|
||||
|
||||
func getGame(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
game, err := database.FetchGame(int32(id))
|
||||
Response(w, err, game)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func RegisterGenresRoutes(r chi.Router) {
|
||||
r.Route("/", func(s chi.Router) {
|
||||
s.Get("/", getGenres)
|
||||
s.Post("/", addGenre)
|
||||
})
|
||||
r.Route("/{id:[0-9]+}", func(s chi.Router) {
|
||||
s.Get("/", getGenre)
|
||||
})
|
||||
r.Route("/{id:[0-9]+}/novels", func(s chi.Router) {
|
||||
s.Get("/", getGenreNovels)
|
||||
})
|
||||
}
|
||||
|
||||
func getGenres(w http.ResponseWriter, r *http.Request) {
|
||||
genres, err := database.FetchGenres()
|
||||
Response(w, err, genres)
|
||||
}
|
||||
func addGenre(w http.ResponseWriter, r *http.Request) {
|
||||
RawError(w, "method not implemented")
|
||||
}
|
||||
|
||||
func getGenre(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
|
||||
genre, err := database.FetchGenre(id)
|
||||
Response(w, err, genre)
|
||||
}
|
||||
|
||||
func getGenreNovels(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
|
||||
novels, err := database.FetchGenreNovels(id)
|
||||
Response(w, err, novels)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func RegisterGroupsRoutes(r chi.Router) {
|
||||
r.Route("/groups", func(s chi.Router) {
|
||||
s.Get("/", getGroups)
|
||||
})
|
||||
r.Route("/groups/{id:[0-9]+}", func(s chi.Router) {
|
||||
s.Get("/", getGroup)
|
||||
})
|
||||
}
|
||||
|
||||
func getGroups(w http.ResponseWriter, r *http.Request) {
|
||||
groups, err := database.GetGroups()
|
||||
Response(w, err, groups)
|
||||
}
|
||||
|
||||
func getGroup(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
group, err := database.GetGroup(id)
|
||||
Response(w, err, group)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func RegisterLongPollRoutes(r chi.Router) {
|
||||
r.Get("/", resolveLongPoll)
|
||||
}
|
||||
|
||||
func resolveLongPoll(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := GetCurrentUserID(r)
|
||||
if errors.Is(err, database.ErrNotAuthorized) {
|
||||
NoToken.Ferror(w)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, http.ErrNoCookie) {
|
||||
NoToken.Ferror(w)
|
||||
return
|
||||
}
|
||||
events, err := lpServer.GetEvents(userId)
|
||||
Response(w, err, events)
|
||||
}
|
||||
|
||||
var NoEventsErr = fmt.Errorf("no events")
|
||||
|
||||
type LongPollServer struct {
|
||||
clients map[uint64]*LongPollClient
|
||||
}
|
||||
|
||||
type LongPollClient struct {
|
||||
events []*LongPollEvent
|
||||
}
|
||||
|
||||
type LongPollEvent struct {
|
||||
Action string `json:"action"`
|
||||
TS int64 `json:"ts"`
|
||||
Payload interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
func InitLongPollServer() *LongPollServer {
|
||||
return &LongPollServer{
|
||||
clients: make(map[uint64]*LongPollClient),
|
||||
}
|
||||
}
|
||||
|
||||
func (lpServer *LongPollServer) PushEvent(payload interface{}, userId uint64, action string) {
|
||||
lpClient, ok := lpServer.clients[userId]
|
||||
if !ok {
|
||||
lpClient = new(LongPollClient)
|
||||
lpServer.clients[userId] = lpClient
|
||||
}
|
||||
|
||||
lpClient.events = append(lpClient.events, &LongPollEvent{
|
||||
action,
|
||||
time.Now().Unix(),
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
func (lpServer *LongPollServer) PushEventToAll(payload interface{}, action string) {
|
||||
for _, lpClient := range lpServer.clients {
|
||||
lpClient.events = append(lpClient.events, &LongPollEvent{
|
||||
action,
|
||||
time.Now().Unix(),
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (lpServer *LongPollServer) GetEvents(userId uint64) ([]*LongPollEvent, error) {
|
||||
lpClient, ok := lpServer.clients[userId]
|
||||
if !ok {
|
||||
lpServer.clients[userId] = new(LongPollClient)
|
||||
return nil, NoEventsErr
|
||||
}
|
||||
|
||||
if len(lpClient.events) == 0 {
|
||||
return nil, NoEventsErr
|
||||
}
|
||||
|
||||
events := lpClient.events
|
||||
lpClient.events = make([]*LongPollEvent, 0)
|
||||
return events, nil
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"encoding/json"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func RegisterNewsRoutes(r chi.Router) {
|
||||
r.Route("/", func(s chi.Router) {
|
||||
s.Get("/", getNews)
|
||||
s.Post("/", addPost)
|
||||
})
|
||||
r.Route("/{id:[0-9]+}", func(s chi.Router) {
|
||||
s.Get("/", getPost)
|
||||
s.Put("/", updatePost)
|
||||
s.Delete("/", deletePost)
|
||||
})
|
||||
}
|
||||
|
||||
func getNews(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
|
||||
offset, _ := strconv.Atoi(q.Get("offset"))
|
||||
limit, _ := strconv.Atoi(q.Get("limit"))
|
||||
if limit == 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
total, err := database.FetchTotalNews()
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
pagination := map[string]interface{}{
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
}
|
||||
|
||||
news, err := database.FetchNews(offset, limit)
|
||||
ResponsePagination(w, err, news, pagination)
|
||||
}
|
||||
|
||||
func addPost(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
post := new(database.Post)
|
||||
err = json.Unmarshal(body, post)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
uid, err := GetCurrentUserID(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
post.AuthorID = uid
|
||||
|
||||
createdPost, err := database.CreatePost(post)
|
||||
Response(w, err, createdPost)
|
||||
}
|
||||
|
||||
func getPost(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
post, err := database.FetchPost(id)
|
||||
Response(w, err, post)
|
||||
}
|
||||
func updatePost(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
post, err := database.FetchPost(id)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, post)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = database.UpdatePost(post)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
func deletePost(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "id")
|
||||
err := database.DeletePost(id)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func RegisterNovelsRoutes(r chi.Router) {
|
||||
r.Route("/", func(s chi.Router) {
|
||||
s.Get("/", getNovels)
|
||||
s.Post("/", createNovel)
|
||||
})
|
||||
r.Route("/{novelId:[0-9]+}", func(s chi.Router) {
|
||||
s.Get("/", getNovel)
|
||||
s.Put("/", updateNovel)
|
||||
s.Delete("/", deleteNovel)
|
||||
})
|
||||
r.Post("/{novelId:[0-9]+}/cover", uploadNovelCover)
|
||||
r.Route("/{novelId:[0-9]+}/comments", func(s chi.Router) {
|
||||
s.Get("/", getNovelComments)
|
||||
s.Post("/", createNovelComment)
|
||||
})
|
||||
r.Get("/{novelId:[0-9]+}/characters", resolveNovelCharacters)
|
||||
r.Get("/{novelId:[0-9]+}/genres", resolveNovelGenres)
|
||||
}
|
||||
|
||||
func getNovels(w http.ResponseWriter, r *http.Request) {
|
||||
sortBy, ok := r.URL.Query()["sort"]
|
||||
|
||||
if !ok || len(sortBy) <= 0 {
|
||||
sortBy = append(sortBy, "rating")
|
||||
}
|
||||
|
||||
query, ok := r.URL.Query()["q"]
|
||||
if ok {
|
||||
novels, err := database.SearchNovels(query[0])
|
||||
Response(w, err, novels)
|
||||
return
|
||||
}
|
||||
|
||||
novels, err := database.FetchNovels(sortBy[0])
|
||||
Response(w, err, &novels)
|
||||
}
|
||||
func createNovel(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var novel database.Novel
|
||||
err = json.Unmarshal(body, &novel)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
novel, err = database.CreateNovel(&novel)
|
||||
if err == nil {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Header().Add("Location", fmt.Sprintf("https://%v%v/%v", r.Host, r.URL, novel.ID))
|
||||
}
|
||||
Response(w, err, &novel)
|
||||
}
|
||||
|
||||
func getNovel(w http.ResponseWriter, r *http.Request) {
|
||||
novelId := IURLParam(r, "novelId")
|
||||
novel, err := database.FetchNovel(novelId)
|
||||
Response(w, err, novel)
|
||||
}
|
||||
func updateNovel(w http.ResponseWriter, r *http.Request) {
|
||||
novelId := IURLParam(r, "novelId")
|
||||
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
novel, err := database.FetchNovel(novelId)
|
||||
if err != nil {
|
||||
Response(w, err, nil)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, novel)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
novel, err = database.UpdateNovel(novel)
|
||||
Response(w, err, novel)
|
||||
}
|
||||
func deleteNovel(w http.ResponseWriter, r *http.Request) {
|
||||
novelId := IURLParam(r, "novelId")
|
||||
_, err := database.FetchNovel(novelId)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
NotFound.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
err = database.DeleteNovel(novelId)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
JSON(w, "")
|
||||
}
|
||||
|
||||
func uploadNovelCover(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := GetCurrentUser(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !hasPermission(user, "novel") {
|
||||
NoAccess.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
novelId := IURLParam(r, "novelId")
|
||||
|
||||
err = r.ParseMultipartForm(1024 * 1024 * 2)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
file, handler, err := r.FormFile("cover")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(handler.Header["Content-Type"][0], "image") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
RawError(w, "Wrong document type!")
|
||||
return
|
||||
}
|
||||
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.Split(handler.Header["Content-Type"][0], "/")[1]
|
||||
path := filepath.Join(os.Getenv("CDN_PATH"), "novels", "logo", fmt.Sprintf("%v.%v", novelId, ext))
|
||||
|
||||
err = os.WriteFile(path, fileBytes, 0644)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
coverPath := fmt.Sprintf("%v/novels/logo/%v.%v", os.Getenv("CDN_URL"), novelId, ext)
|
||||
err = database.UpdateNovelCover(novelId, coverPath)
|
||||
Response(w, err, coverPath)
|
||||
}
|
||||
|
||||
func getNovelComments(w http.ResponseWriter, r *http.Request) {
|
||||
novelId := IURLParam(r, "novelId")
|
||||
offsetParam, ok := r.URL.Query()["offset"]
|
||||
if !ok || len(offsetParam) <= 0 {
|
||||
offsetParam = append(offsetParam, "0")
|
||||
}
|
||||
|
||||
countParam, ok := r.URL.Query()["count"]
|
||||
if !ok || len(countParam) <= 0 {
|
||||
countParam = append(countParam, "5")
|
||||
}
|
||||
|
||||
offset, _ := strconv.Atoi(offsetParam[0])
|
||||
count, _ := strconv.Atoi(countParam[0])
|
||||
|
||||
var response = make(map[string]interface{}, 2)
|
||||
comments, err := database.FetchNovelComments(novelId)
|
||||
response["count"] = len(comments)
|
||||
|
||||
if offset+count > len(comments) {
|
||||
comments = comments[offset:]
|
||||
} else {
|
||||
comments = comments[offset : offset+count]
|
||||
}
|
||||
response["comments"] = comments
|
||||
Response(w, err, response)
|
||||
}
|
||||
|
||||
func createNovelComment(w http.ResponseWriter, r *http.Request) {
|
||||
novelId := IURLParam(r, "novelId")
|
||||
user, err := GetCurrentUser(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
commentData := database.CommentNovel{NovelID: int16(novelId), UserID: user.ID}
|
||||
err = json.Unmarshal(body, &commentData)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := database.AddNovelComment(commentData)
|
||||
Response(w, err, comment)
|
||||
}
|
||||
|
||||
func resolveNovelGenres(w http.ResponseWriter, r *http.Request) {
|
||||
novelId := IURLParam(r, "novelId")
|
||||
genres, err := database.FetchNovelGenres(novelId)
|
||||
Response(w, err, genres)
|
||||
}
|
||||
|
||||
func resolveNovelCharacters(w http.ResponseWriter, r *http.Request) {
|
||||
novelId := IURLParam(r, "novelId")
|
||||
characters, err := database.FetchNovelCharacters(novelId)
|
||||
Response(w, err, characters)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type visitor struct {
|
||||
limiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
var visitors = make(map[string]*visitor)
|
||||
var mu sync.Mutex
|
||||
|
||||
func CleanupVisitors() {
|
||||
for {
|
||||
time.Sleep(time.Minute)
|
||||
|
||||
mu.Lock()
|
||||
for ip, v := range visitors {
|
||||
if time.Since(v.lastSeen) > time.Minute*3 {
|
||||
delete(visitors, ip)
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func getVisitor(ip string) *rate.Limiter {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
v, exists := visitors[ip]
|
||||
if !exists {
|
||||
limiter := rate.NewLimiter(1, 20)
|
||||
visitors[ip] = &visitor{limiter, time.Now()}
|
||||
return limiter
|
||||
}
|
||||
|
||||
v.lastSeen = time.Now()
|
||||
return v.limiter
|
||||
}
|
||||
|
||||
func LimitMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
limiter := getVisitor(ip)
|
||||
if !limiter.Allow() {
|
||||
TooManyRequests.Ferror(w)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"backend/utils"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var lpServer *LongPollServer
|
||||
|
||||
func RegisterRouters(r chi.Router) {
|
||||
lpServer = InitLongPollServer()
|
||||
|
||||
r.Use(apiMiddleware)
|
||||
r.Use(LimitMiddleware)
|
||||
go CleanupVisitors()
|
||||
r.Use(JWTMiddleware)
|
||||
|
||||
r.Get("/version", func(w http.ResponseWriter, _ *http.Request) {
|
||||
Response(w, nil, map[string]string{
|
||||
"version": os.Getenv("VERSION"),
|
||||
"build": os.Getenv("BUILD"),
|
||||
})
|
||||
})
|
||||
|
||||
r.Route("/auth", RegisterAuthRoutes)
|
||||
r.Route("/users", RegisterUsersRoutes)
|
||||
r.Route("/novels", RegisterNovelsRoutes)
|
||||
r.Route("/characters", RegisterCharactersRoutes)
|
||||
r.Route("/genres", RegisterGenresRoutes)
|
||||
r.Route("/games", RegisterGamesRoutes)
|
||||
r.Route("/clubs", RegisterClubRoutes)
|
||||
r.Route("/groups", RegisterGroupsRoutes)
|
||||
r.Route("/news", RegisterNewsRoutes)
|
||||
r.Route("/tokens", RegisterTokensRouter)
|
||||
r.Route("/lp", RegisterLongPollRoutes)
|
||||
|
||||
if utils.IsDevelopment() && utils.IsDebug() {
|
||||
err := chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
log.Printf("[%s]: %s\n", method, route)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Response(w http.ResponseWriter, err error, data interface{}) {
|
||||
const baseResponse = `{"error":false,"data":%v}`
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
NotFound.Ferror(w)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
}
|
||||
} else {
|
||||
j, err := json.Marshal(&data)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(w, baseResponse, string(j))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ResponsePagination(w http.ResponseWriter, err error, data interface{}, pagination interface{}) {
|
||||
const baseResponse = `{"error":false,"pagination":%v,"data":%v}`
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
NotFound.Ferror(w)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
}
|
||||
} else {
|
||||
jsonData, err := json.Marshal(&data)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonPagination, err := json.Marshal(&pagination)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(w, baseResponse, string(jsonPagination), string(jsonData))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RawError(w http.ResponseWriter, e string) {
|
||||
const baseError = `{"error":true,"error_data":{"code":-1,"text":"%v"}}`
|
||||
|
||||
_, err := fmt.Fprintf(w, baseError, strings.ReplaceAll(e, `"`, `\"`))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Error(w http.ResponseWriter, e error) {
|
||||
const baseError = `{"error":true,"error_data":{"code":-1,"text":"%v"}}`
|
||||
|
||||
_, err := fmt.Fprintf(w, baseError, strings.ReplaceAll(e.Error(), `"`, `\"`))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
}
|
||||
}
|
||||
|
||||
func JSON(w http.ResponseWriter, data string) {
|
||||
_, err := fmt.Fprint(w, data)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
sentry.CaptureException(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ReadBody(r *http.Request) ([]byte, error) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.Body.Close()
|
||||
return body, err
|
||||
}
|
||||
|
||||
func IURLParam(r *http.Request, key string) int {
|
||||
par := chi.URLParam(r, key)
|
||||
i, err := strconv.Atoi(par)
|
||||
if err != nil {
|
||||
return 0
|
||||
} else {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
func GetCurrentUserID(r *http.Request) (uint64, error) {
|
||||
tokenCookie, err := r.Cookie("token")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
token, err := jwt.Parse(tokenCookie.Value, func(token *jwt.Token) (interface{}, error) {
|
||||
if token.Method != jwt.SigningMethodHS256 {
|
||||
return nil, fmt.Errorf("invalid singning method: %s", token.Method)
|
||||
}
|
||||
return []byte(os.Getenv("AUTH_TOKEN_SECRET")), nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
id := int(claims["id"].(float64))
|
||||
return uint64(id), nil
|
||||
}
|
||||
|
||||
func GetCurrentUser(r *http.Request) (*database.User, error) {
|
||||
cookie, err := r.Cookie("token")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(cookie.Value) == 0 {
|
||||
return nil, fmt.Errorf("token cookie is empty")
|
||||
}
|
||||
|
||||
user := new(database.User)
|
||||
tokenString := cookie.Value
|
||||
|
||||
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 {
|
||||
return user, err
|
||||
}
|
||||
if !token.Valid {
|
||||
return user, fmt.Errorf("token not valid")
|
||||
}
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
user, err = database.FetchUser(int(claims["id"].(float64)))
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
|
||||
func apiMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Access-Control-Allow-Origin", utils.GetFrontendURL())
|
||||
w.Header().Add("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Add("Content-Type", "application/json; charset=utf-8")
|
||||
//w.Header().Add("Cache-Control", "max-age=60")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.Header().Add("Access-Control-Allow-Headers", "Content-Type, Date, Content-Length, Location, dev, cache")
|
||||
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS")
|
||||
return
|
||||
}
|
||||
|
||||
if database.Database == nil {
|
||||
log.Println("db not initialized")
|
||||
return
|
||||
}
|
||||
err := database.Database.Ping()
|
||||
if err != nil && strings.Contains(err.Error(), "broken pipe") {
|
||||
database.ConnectToDB()
|
||||
log.Println("connection to db reopened")
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func RegisterTokensRouter(r chi.Router) {
|
||||
r.Post("/auth", auth)
|
||||
r.Route("/apps", func(s chi.Router) {
|
||||
s.Get("/", getApps)
|
||||
s.Get("/{id:[0-9]+}", getApp)
|
||||
})
|
||||
}
|
||||
|
||||
func auth(w http.ResponseWriter, r *http.Request) {
|
||||
tokenCookie, err := r.Cookie("token")
|
||||
if errors.Is(err, http.ErrNoCookie) || len(tokenCookie.Value) == 0 {
|
||||
Unauthorized.Ferror(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getApps(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := GetCurrentUser(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
if !hasPermission(user, "apps") {
|
||||
NoAccess.Ferror(w)
|
||||
return
|
||||
}
|
||||
apps, err := database.FetchApps()
|
||||
Response(w, err, apps)
|
||||
}
|
||||
|
||||
func getApp(w http.ResponseWriter, r *http.Request) {}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func RegisterUsersRoutes(r chi.Router) {
|
||||
r.Get("/", getUsers)
|
||||
r.Get("/me", getCurrentUser)
|
||||
r.Post("/me/avatar", uploadAvatar)
|
||||
r.Route("/me/settings", func(s chi.Router) {
|
||||
s.Get("/", getSettings)
|
||||
s.Post("/", updateSettings)
|
||||
})
|
||||
r.Route("/{userId:[0-9]+}", func(s chi.Router) {
|
||||
s.Get("/", getUser)
|
||||
s.Put("/", updateUser)
|
||||
})
|
||||
r.Route("/{userId:[0-9]+}/novels", func(s chi.Router) {
|
||||
s.Get("/", getUserNovels)
|
||||
s.Post("/", addUserNovel)
|
||||
})
|
||||
r.Route("/{userId:[0-9]+}/novels/{novelId:[0-9]+}", func(s chi.Router) {
|
||||
s.Get("/", getUserNovel)
|
||||
s.Put("/", updateUserNovel)
|
||||
s.Delete("/", deleteUserNovel)
|
||||
})
|
||||
r.Get("/{userId:[0-9]+}/novels/comments", resolveUserNovelsComments)
|
||||
}
|
||||
|
||||
func getUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := database.FetchUsers()
|
||||
Response(w, err, users)
|
||||
}
|
||||
|
||||
func getCurrentUser(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := GetCurrentUser(r)
|
||||
if errors.Is(err, database.ErrNotAuthorized) {
|
||||
NoToken.Ferror(w)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, http.ErrNoCookie) {
|
||||
NoToken.Ferror(w)
|
||||
return
|
||||
}
|
||||
Response(w, err, user)
|
||||
}
|
||||
|
||||
func uploadAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := GetCurrentUser(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = r.ParseMultipartForm(user.Group.MaxAvaWeight)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
file, handler, err := r.FormFile("avatar")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(handler.Header["Content-Type"][0], "image") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
RawError(w, "Wrong document type!")
|
||||
return
|
||||
}
|
||||
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
img, _, err := image.DecodeConfig(bytes.NewReader(fileBytes))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
width, height := img.Width, img.Width
|
||||
if width > user.Group.MaxAvatarSize && height > user.Group.MaxAvatarSize {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
RawError(w, "Image too big!")
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.Split(handler.Header["Content-Type"][0], "/")[1]
|
||||
path := filepath.Join(os.Getenv("CDN_PATH"), "users", "avatars", fmt.Sprintf("%v.%v", user.ID, ext))
|
||||
|
||||
err = os.WriteFile(path, fileBytes, 0644)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
avatarPath := fmt.Sprintf("%v/avatars/%v.%v", os.Getenv("CDN_URL"), user.ID, ext)
|
||||
Response(w, nil, avatarPath)
|
||||
}
|
||||
|
||||
func getSettings(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := GetCurrentUserID(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
settings, err := database.FetchUserSettings(id)
|
||||
Response(w, err, settings)
|
||||
}
|
||||
func updateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := GetCurrentUserID(r)
|
||||
settings, err := database.FetchUserSettings(id)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, settings)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := database.UpdateUserSettings(id, settings)
|
||||
Response(w, err, user)
|
||||
}
|
||||
|
||||
func getUser(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.Atoi(chi.URLParam(r, "userId"))
|
||||
user, err := database.FetchUser(id)
|
||||
Response(w, err, user)
|
||||
}
|
||||
func updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.Atoi(chi.URLParam(r, "userId"))
|
||||
|
||||
currentUser, err := GetCurrentUser(r)
|
||||
if err != nil && !errors.Is(err, database.ErrNotAuthorized) {
|
||||
Error(w, err)
|
||||
return
|
||||
} else if errors.Is(err, database.ErrNotAuthorized) || currentUser == nil {
|
||||
currentUser = &database.User{ID: -1}
|
||||
}
|
||||
if currentUser.ID != int64(id) && !hasPermission(currentUser, "users") {
|
||||
NoAccess.Ferror(w)
|
||||
return
|
||||
}
|
||||
_, err = database.FetchUser(id)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var u *database.User
|
||||
err = json.Unmarshal(body, u)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
user, err := database.UpdateUser(u)
|
||||
Response(w, err, user)
|
||||
}
|
||||
|
||||
func getUserNovels(w http.ResponseWriter, r *http.Request) {
|
||||
userId, _ := strconv.Atoi(chi.URLParam(r, "userId"))
|
||||
|
||||
novels, err := database.FetchUserNovels(userId)
|
||||
Response(w, err, novels)
|
||||
}
|
||||
func addUserNovel(w http.ResponseWriter, r *http.Request) {
|
||||
userId, _ := strconv.Atoi(chi.URLParam(r, "userId"))
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
var novel = &database.UserNovels{}
|
||||
err = json.Unmarshal(body, novel)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
novel.UserID = int64(userId)
|
||||
|
||||
novel, err = database.CreateUserNovel(novel)
|
||||
if err == nil {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Header().Add("Location", fmt.Sprintf("https://%v%v/%v", r.Host, r.URL, novel.NovelID))
|
||||
}
|
||||
Response(w, err, novel)
|
||||
}
|
||||
|
||||
func getUserNovel(w http.ResponseWriter, r *http.Request) {
|
||||
userId := IURLParam(r, "userId")
|
||||
novelId := IURLParam(r, "novelId")
|
||||
novel, err := database.FetchUserNovel(userId, novelId)
|
||||
Response(w, err, novel)
|
||||
|
||||
}
|
||||
func updateUserNovel(w http.ResponseWriter, r *http.Request) {
|
||||
userId := IURLParam(r, "userId")
|
||||
novelId := IURLParam(r, "novelId")
|
||||
|
||||
_, err := database.FetchNovel(novelId)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
NotFound.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = database.FetchUser(userId)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
NotFound.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
novel, _ := database.FetchUserNovel(userId, novelId)
|
||||
body, err := ReadBody(r)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if novel == nil {
|
||||
NotFound.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &novel)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedNovel, err := database.UpdateUserNovel(novel)
|
||||
Response(w, err, updatedNovel)
|
||||
}
|
||||
func deleteUserNovel(w http.ResponseWriter, r *http.Request) {
|
||||
userId := IURLParam(r, "userId")
|
||||
novelId := IURLParam(r, "novelId")
|
||||
_, err := database.FetchUserNovel(userId, novelId)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
NotFound.Ferror(w)
|
||||
return
|
||||
}
|
||||
|
||||
err = database.DeleteUserNovel(userId, novelId)
|
||||
if err != nil {
|
||||
Error(w, err)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func resolveUserNovelsComments(w http.ResponseWriter, r *http.Request) {
|
||||
id := IURLParam(r, "userId")
|
||||
|
||||
comments, err := database.FetchUserNovelsComments(id)
|
||||
Response(w, err, comments)
|
||||
}
|
||||
Reference in New Issue
Block a user