diff --git a/controllers/health.go b/controllers/health.go index 4d57d4c..eb5c129 100644 --- a/controllers/health.go +++ b/controllers/health.go @@ -1,9 +1,32 @@ package controllers -import "net/http" +import ( + "fmt" + "net/http" + + "github.com/hbjydev/mangadex-next/database" +) type HealthController struct{} func (h *HealthController) Healthy(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + +func (h *HealthController) Metrics(w http.ResponseWriter, r *http.Request) { + var output string + + output += fmt.Sprintf("mangadex_item_count{type=\"user\"} %v\n", countUsers()) + + w.Write([]byte(output)) + w.WriteHeader(http.StatusOK) +} + +func countUsers() int { + row := database.DB.QueryRow("SELECT count(id) FROM users") + var count int + if err := row.Scan(&count); err != nil { + return -1 + } + return count +} diff --git a/controllers/user.go b/controllers/user.go new file mode 100644 index 0000000..4bb3dbd --- /dev/null +++ b/controllers/user.go @@ -0,0 +1,56 @@ +package controllers + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/hbjydev/mangadex-next/models" +) + +type UserController struct{} + +func (u *UserController) GetAll(w http.ResponseWriter, r *http.Request) { + users, err := models.GetUsers() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + usersJSON, err := json.Marshal(users) + if err != nil { + log.Println("/users: failed to marshal users array") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write(usersJSON) +} + +func (u *UserController) GetOne(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + user, err := models.UserByUsername(vars["username"]) + if err != nil { + if err.Error() == "sql: no rows in result set" { + w.WriteHeader(http.StatusNotFound) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + return + } + + userJSON, err := user.Normalize() + if err != nil { + log.Printf("/user/%v: failed to marshal user", vars["username"]) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte(*userJSON)) +} diff --git a/docs/openapi.yml b/docs/openapi.yml index 4f0e46e..dec75ef 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -17,6 +17,8 @@ schemes: tags: - name: health description: 'Service health endpoints' + - name: users + description: 'Service user endpoints' paths: /-/healthy: @@ -28,4 +30,39 @@ paths: responses: '200': description: 'Everything is healthy.' + /-/metrics: + get: + summary: Retrieves some metrics about the API server and database. + operationId: metrics + tags: + - health + responses: + '200': + description: 'A Prometheus-compatible metrics list.' + /users: + get: + summary: Retrieves every user in the database. + operationId: getAllUsers + tags: + - users + responses: + '200': + description: 'All users in the database.' + + /users/{username}: + get: + summary: Retrieves a single user from the database. + operationId: getOneUser + tags: + - users + parameters: + - in: path + name: username + schema: + type: string + required: true + description: The username of the user you want information on. + responses: + '200': + description: 'A single user from the database.' diff --git a/main.go b/main.go index 0218b77..4c12e52 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,9 @@ func main() { healthRouter := routers.HealthRouter{} healthRouter.RegisterRoutes(r) + userRouter := routers.UserRouter{} + userRouter.RegisterRoutes(r) + log.Println("Starting server on :3000...") log.Fatal(http.ListenAndServe(":3000", r)) } diff --git a/models/user.go b/models/user.go index cda68bb..af56ac1 100644 --- a/models/user.go +++ b/models/user.go @@ -11,11 +11,10 @@ import ( "golang.org/x/crypto/bcrypt" ) -type password string type User struct { ID string `json:"id"` Username string `json:"username"` - Password password `json:"password"` + Password string `json:"-"` Email string `json:"email"` LevelID string `json:"level_id"` JoinedAt time.Time `json:"joined_at"` @@ -30,8 +29,59 @@ type User struct { AvatarURL string `json:"avatar"` } -func (password) MarshalJSON() ([]byte, error) { - return []byte(`""`), nil +func GetUsers() ([]User, error) { + rows, err := database.DB.Query(` + SELECT + hex(id), username, email, password, level_id, last_seen, website, + biography, views, uploads, premium, md_at_home, avatar_url, + joined_at, update_at + FROM users + `) + if err != nil { + log.Printf("Users: error at SQL query: %v\n", err) + return nil, err + } + defer rows.Close() + + users := make([]User, 0) + for rows.Next() { + var u User + var ls mysql.NullTime + var ja mysql.NullTime + var ua mysql.NullTime + + if err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.Password, &u.LevelID, + &ls, &u.Website, &u.Biography, &u.Views, &u.Uploads, &u.Premium, + &u.MDAtHome, &u.AvatarURL, &ja, &ua); err != nil { + log.Printf("UserByUsername: error at SQL query: %v\n", err) + return nil, err + } + + if ls.Valid { + u.LastSeen = ls.Time + } else { + log.Printf("UserByUsername: invalid SQL datetime value (id %v)\n", u.ID) + return nil, errors.New("invalid sql datetime value") + } + + if ja.Valid { + u.JoinedAt = ja.Time + } else { + log.Printf("UserByUsername: invalid SQL datetime value (id %v)\n", u.ID) + return nil, errors.New("invalid sql datetime value") + } + + if ua.Valid { + u.UpdateAt = ua.Time + } else { + log.Printf("UserByUsername: invalid SQL datetime value (id %v)\n", u.ID) + return nil, errors.New("invalid sql datetime value") + } + + users = append(users, u) + } + + return users, nil } func UserByUsername(username string) (*User, error) { @@ -39,7 +89,8 @@ func UserByUsername(username string) (*User, error) { SELECT hex(id), username, email, password, level_id, last_seen, website, biography, views, uploads, premium, md_at_home, avatar_url, - joined_at, update_at FROM users + joined_at, update_at + FROM users WHERE username = ? `, username) diff --git a/routers/health.go b/routers/health.go index fab32ea..3c3b36d 100644 --- a/routers/health.go +++ b/routers/health.go @@ -7,17 +7,19 @@ import ( "github.com/hbjydev/mangadex-next/controllers" ) -type HealthRouter struct { - Controller controllers.HealthController -} +type HealthRouter struct{} func (h *HealthRouter) healthcheck(w http.ResponseWriter, r *http.Request) { controller := controllers.HealthController{} controller.Healthy(w, r) } -func (h *HealthRouter) RegisterRoutes(r *mux.Router) { - pathPrefix := r.PathPrefix("/-") - - pathPrefix.Path("/healthy").HandlerFunc(h.healthcheck).Methods("GET") +func (h *HealthRouter) metrics(w http.ResponseWriter, r *http.Request) { + controller := controllers.HealthController{} + controller.Metrics(w, r) +} + +func (h *HealthRouter) RegisterRoutes(r *mux.Router) { + r.HandleFunc("/-/healthy", h.healthcheck) + r.HandleFunc("/-/metrics", h.metrics) } diff --git a/routers/user.go b/routers/user.go new file mode 100644 index 0000000..b39a6b9 --- /dev/null +++ b/routers/user.go @@ -0,0 +1,25 @@ +package routers + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/hbjydev/mangadex-next/controllers" +) + +type UserRouter struct{} + +func (u *UserRouter) getAll(w http.ResponseWriter, r *http.Request) { + controller := controllers.UserController{} + controller.GetAll(w, r) +} + +func (u *UserRouter) getOne(w http.ResponseWriter, r *http.Request) { + controller := controllers.UserController{} + controller.GetOne(w, r) +} + +func (u *UserRouter) RegisterRoutes(r *mux.Router) { + r.HandleFunc("/users", u.getAll) + r.HandleFunc("/users/{username}", u.getOne) +}