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) }) }