Files
bot-pontos/backend/internal/httpapi/handlers_users.go
2026-10-23 22:31:02 -03:00

290 lines
7.3 KiB
Go

package httpapi
import (
"net/http"
"botpontos/internal/adapters/postgres"
"botpontos/internal/app"
"botpontos/internal/domain"
"botpontos/internal/httpx"
)
func actorOf(r *http.Request) app.Actor {
return app.WebActor(sessionFrom(r).AccountID)
}
// --- autenticação ---
func (a *API) login(w http.ResponseWriter, r *http.Request) {
if !a.sameOrigin(r) {
httpx.WriteProblem(w, http.StatusForbidden, "Origem da requisição não permitida", "BAD_ORIGIN")
return
}
var body struct {
Username string `json:"username"`
Password string `json:"password"`
}
if !decode(w, r, &body) {
return
}
result, err := a.svc.Login(r.Context(), body.Username, body.Password, a.clientIP(r), r.UserAgent())
if err != nil {
a.writeError(w, err)
return
}
setSessionCookies(w, result.Token, result.CSRFToken, result.ExpiresAt, a.cfg.Web.CookieSecure)
httpx.WriteJSON(w, http.StatusOK, map[string]any{"account": toAccount(result.Account), "expires_at": result.ExpiresAt})
}
func (a *API) logout(w http.ResponseWriter, r *http.Request) {
if err := a.svc.Logout(r.Context(), sessionFrom(r)); err != nil {
a.writeError(w, err)
return
}
clearCookies(w, a.cfg.Web.CookieSecure)
w.WriteHeader(http.StatusNoContent)
}
func (a *API) me(w http.ResponseWriter, r *http.Request) {
s := sessionFrom(r)
httpx.WriteJSON(w, http.StatusOK, map[string]any{"account": toAccount(s.Account), "expires_at": s.ExpiresAt})
}
func (a *API) dashboard(w http.ResponseWriter, r *http.Request) {
o, err := a.svc.Overview(r.Context())
if err != nil {
a.writeError(w, err)
return
}
if account := sessionFrom(r).Account; isRestricted(account) {
// Início da conta restrita: sem contadores globais, só os avisos do
// sistema e os lembretes de hoje do próprio usuário.
today := a.svc.TodayDate()
items, err := a.svc.Store().ListNotifications(r.Context(), postgres.NotificationFilter{Day: &today, UserID: account.UserID, Limit: 200})
if err != nil {
a.writeError(w, err)
return
}
byStatus := map[string]int{}
for _, v := range items {
byStatus[string(v.Notification.Status)]++
}
httpx.WriteJSON(w, http.StatusOK, dashboardJSON{
UsersByStatus: map[string]int{}, TodayByStatus: byStatus,
OutageSince: o.OutageSince, HolidayFallbackDay: o.HolidayFallback, Today: today,
})
return
}
httpx.WriteJSON(w, http.StatusOK, dashboardJSON{
UsersByStatus: o.Counts.UsersByStatus, NotificationsEnabled: o.Counts.NotificationsEnabled,
PausedByAPIFailures: o.Counts.PausedByAPIFailures, TodayByStatus: o.Counts.TodayByStatus,
FailedLast7Days: o.Counts.FailedLast7Days, ExpiredLast7Days: o.Counts.ExpiredLast7Days,
CustomWindows: o.Counts.CustomWindows, OutageSince: o.OutageSince, HolidayFallbackDay: o.HolidayFallback,
Today: a.svc.TodayDate(),
})
}
// --- usuários ---
func (a *API) listUsers(w http.ResponseWriter, r *http.Request) {
custom, err := queryBool(r, "custom_window")
if err != nil {
a.writeError(w, err)
return
}
users, err := a.svc.Store().ListUsers(r.Context(), postgres.UserFilter{
Status: queryString(r, "status"), Query: queryString(r, "q"), CustomWindow: custom,
})
if err != nil {
a.writeError(w, err)
return
}
account := sessionFrom(r).Account
out := make([]userSummaryJSON, 0, len(users))
for _, u := range users {
if isRestricted(account) && u.User.ID != *account.UserID {
continue
}
out = append(out, toUserSummary(u))
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"users": out})
}
func (a *API) getUser(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
detail, err := a.svc.UserDetail(r.Context(), id)
if err != nil {
a.writeError(w, err)
return
}
httpx.WriteJSON(w, http.StatusOK, toUserDetail(detail))
}
// respondUser devolve o detalhe atualizado após uma ação.
func (a *API) respondUser(w http.ResponseWriter, r *http.Request, id int64) {
detail, err := a.svc.UserDetail(r.Context(), id)
if err != nil {
a.writeError(w, err)
return
}
httpx.WriteJSON(w, http.StatusOK, toUserDetail(detail))
}
func (a *API) activateUser(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
if _, err := a.svc.ActivateUser(r.Context(), actorOf(r), id); err != nil {
a.writeError(w, err)
return
}
a.respondUser(w, r, id)
}
func (a *API) disableUser(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
if err := a.svc.DisableUser(r.Context(), actorOf(r), id); err != nil {
a.writeError(w, err)
return
}
a.respondUser(w, r, id)
}
func (a *API) updateSchedule(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
var body struct {
scheduleJSON
Timezone string `json:"timezone"`
}
if !decode(w, r, &body) {
return
}
in := app.ScheduleInput{
LunchMinutes: body.LunchMinutes, DailyWorkMinutes: body.DailyWorkMinutes,
Entrada2LeadMinutes: body.Entrada2LeadMinutes, Saida2LeadMinutes: body.Saida2LeadMinutes,
Timezone: body.Timezone,
}
if body.Window != nil {
window, err := body.Window.parse("window.")
if err != nil {
a.writeError(w, err)
return
}
in.Window = &window
}
if _, err := a.svc.UpdateSchedule(r.Context(), actorOf(r), id, in); err != nil {
a.writeError(w, err)
return
}
a.respondUser(w, r, id)
}
func (a *API) updateCredentials(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
var body struct {
Email string `json:"email"`
Password string `json:"password"`
}
if !decode(w, r, &body) {
return
}
if err := a.svc.SetCredentials(r.Context(), actorOf(r), id, body.Email, body.Password); err != nil {
a.writeError(w, err)
return
}
a.respondUser(w, r, id)
}
func (a *API) testCredentials(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
view, err := a.svc.TestCredentials(r.Context(), actorOf(r), id)
if err != nil {
a.writeError(w, err)
return
}
httpx.WriteJSON(w, http.StatusOK, toToday(view))
}
func (a *API) enableNotifications(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
if _, err := a.svc.EnableNotifications(r.Context(), actorOf(r), id); err != nil {
a.writeError(w, err)
return
}
a.respondUser(w, r, id)
}
func (a *API) disableNotifications(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
if err := a.svc.DisableNotifications(r.Context(), actorOf(r), id, domain.PauseAdmin); err != nil {
a.writeError(w, err)
return
}
a.respondUser(w, r, id)
}
func (a *API) setBotAdmin(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
var body struct {
Enabled bool `json:"enabled"`
}
if !decode(w, r, &body) {
return
}
if err := a.svc.SetBotAdmin(r.Context(), actorOf(r), id, body.Enabled); err != nil {
a.writeError(w, err)
return
}
a.respondUser(w, r, id)
}
func (a *API) today(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
view, err := a.svc.Today(r.Context(), actorOf(r), id, nil)
if err != nil {
a.writeError(w, err)
return
}
httpx.WriteJSON(w, http.StatusOK, toToday(view))
}
func (a *API) resumeDay(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
if _, err := a.svc.ResumeDay(r.Context(), actorOf(r), id); err != nil {
a.writeError(w, err)
return
}
a.respondUser(w, r, id)
}