migration to chi

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