package controllers import ( "encoding/json" "log" "net/http" "strconv" "time" "git.aiterp.net/lucifer/lucifer/internal/respond" "git.aiterp.net/lucifer/lucifer/models" "github.com/gorilla/mux" ) // The UserController is a controller for all users. type UserController struct { users models.UserRepository sessions models.SessionRepository } // getUsers (`GET /`): List users func (c *UserController) getUsers(w http.ResponseWriter, r *http.Request) { if session := models.SessionFromContext(r.Context()); session == nil { respond.Error(w, http.StatusForbidden, "permission_denied", "You must log in") return } // TODO: Only admin should be allowed to do this? users, err := c.users.List(r.Context()) if err != nil { respond.Error(w, 500, "db_error", err.Error()) return } respond.Data(w, users) } // getUser (`GET /:id`): Get user by id func (c *UserController) getUser(w http.ResponseWriter, r *http.Request) { session := models.SessionFromContext(r.Context()) if session == nil { respond.Error(w, http.StatusForbidden, "permission_denied", "You must log in") return } // TODO: Only admin should be allowed to do this? var user models.User id, err := strconv.Atoi(mux.Vars(r)["id"]) if err == nil { user, err = c.users.FindByID(r.Context(), id) if err != nil { respond.Error(w, 404, "not_found", err.Error()) return } } else { user, err = c.users.FindByName(r.Context(), mux.Vars(r)["id"]) if err != nil { respond.Error(w, 404, "not_found", err.Error()) return } } respond.Data(w, user) } func (c *UserController) session(w http.ResponseWriter, r *http.Request) { type response struct { LoggedIn bool `json:"loggedIn"` User *models.User `json:"user"` } session := models.SessionFromContext(r.Context()) if session == nil { respond.Data(w, response{}) return } user, err := c.users.FindByID(r.Context(), session.UserID) if err != nil { respond.Data(w, response{}) return } respond.Data(w, response{ LoggedIn: true, User: &user, }) } // login (`POST /login`): Log in as user func (c *UserController) login(w http.ResponseWriter, r *http.Request) { loginData := struct { Username string `json:"username"` Password string `json:"password"` }{} err := json.NewDecoder(r.Body).Decode(&loginData) if err != nil { respond.Error(w, 400, "invalid_json", "Input is not valid JSON.") return } user, err := c.users.FindByName(r.Context(), loginData.Username) if err != nil { respond.Error(w, http.StatusUnauthorized, "login_failed", "Login failed.") return } if err := user.CheckPassword(loginData.Password); err != nil { respond.Error(w, http.StatusUnauthorized, "login_failed", "Login failed.") return } session := models.Session{ Expires: time.Now().Add(7 * 24 * time.Hour), UserID: user.ID, } session.GenerateID() if err := c.sessions.Insert(r.Context(), session); err != nil { log.Printf("Session create for user %s (%d) failed: %s", user.Name, user.ID, err) respond.Error(w, http.StatusInternalServerError, "internal_error", "Failed to open session.") return } http.SetCookie(w, session.Cookie()) log.Printf("User %s logged in", user.Name) respond.Data(w, user) } func (c *UserController) register(w http.ResponseWriter, r *http.Request) { registerData := struct { Username string `json:"username"` Password string `json:"password"` }{} if err := json.NewDecoder(r.Body).Decode(®isterData); err != nil { respond.Error(w, http.StatusBadRequest, "invalid_json", "Input is not valid JSON.") return } if len(registerData.Username) < 1 { respond.Error(w, http.StatusBadRequest, "invalid_username", "The username cannot be empty.") return } if _, err := strconv.Atoi(registerData.Username); err == nil { respond.Error(w, http.StatusBadRequest, "invalid_username", "The username cannot start with a number.") return } user := models.User{Name: registerData.Username} if err := user.SetPassword(registerData.Password); err != nil { respond.Error(w, http.StatusBadRequest, "invalid_password", "The password is not valid: "+err.Error()) return } user, err := c.users.Insert(r.Context(), user) if err != nil { respond.Error(w, http.StatusBadRequest, "invalid_password", "The password is not valid: "+err.Error()) return } log.Printf("User %s registered", user.Name) respond.Data(w, user) } // login (`POST /logout`): Log in as user func (c *UserController) logout(w http.ResponseWriter, r *http.Request) { logoutData := struct { ClearAll bool `json:"clearAll"` }{} session := models.SessionFromContext(r.Context()) if session == nil { respond.Error(w, http.StatusUnauthorized, "permission_denied", "You are not logged in (that's what you wanted anyway, wasn't it?)") return } json.NewDecoder(r.Body).Decode(&logoutData) user, err := c.users.FindByID(r.Context(), session.UserID) if err != nil { respond.Error(w, http.StatusNotFound, "not_found", "You user was not found") return } if logoutData.ClearAll { err := c.sessions.Clear(r.Context(), user) if err != nil { log.Printf("Session clear for user %s (%d) failed: %s", user.Name, user.ID, err) respond.Error(w, http.StatusInternalServerError, "clear_failed", "Sesison clear failed") return } } else { err := c.sessions.Remove(r.Context(), *session) if err != nil { log.Printf("Session remove for user %s (%d) with id %s failed: %s", user.Name, user.ID, session.ID, err) respond.Error(w, http.StatusInternalServerError, "remove_failed", "Session remove failed") return } } cookie := session.Cookie() cookie.Expires = time.Unix(0, 0) http.SetCookie(w, cookie) log.Printf("User %s logged out (clearAll: %t)", user.Name, logoutData.ClearAll) respond.Data(w, logoutData) } // Mount mounts the controller func (c *UserController) Mount(router *mux.Router, prefix string) { sub := router.PathPrefix(prefix).Subrouter() sub.HandleFunc("/", c.getUsers).Methods("GET") sub.HandleFunc("/session", c.session).Methods("GET") sub.HandleFunc("/{id}", c.getUser).Methods("GET") sub.HandleFunc("/login", c.login).Methods("POST") sub.HandleFunc("/logout", c.logout).Methods("POST") sub.HandleFunc("/register", c.register).Methods("POST") } // NewUserController creates a new UserController. func NewUserController(users models.UserRepository, sessions models.SessionRepository) *UserController { return &UserController{users: users, sessions: sessions} }