diff --git a/server/api/api.go b/server/api/api.go index f83f565..bd050fb 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -25,6 +25,17 @@ func Start() { r.Get("/", Whoami) }) + r.Route("/channels", func(r chi.Router) { + r.Use(SessionAuthMiddleware) + + r.Get("/", ListChannels) + r.Post("/", NewChannel) + r.Route("/{channelID}", func(r chi.Router) { + r.Get("/", GetChannel) + r.Delete("/", DeleteChannel) + }) + }) + r.Route("/users", func(r chi.Router) { r.Use(SessionAuthMiddleware) diff --git a/server/api/channel.go b/server/api/channel.go new file mode 100644 index 0000000..dccae62 --- /dev/null +++ b/server/api/channel.go @@ -0,0 +1,146 @@ +package api + +import ( + "errors" + "log/slog" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/google/uuid" +) + +func GetChannel(w http.ResponseWriter, r *http.Request) { + slog.Debug("channel: entering GetChannel handler") + + channelID := chi.URLParam(r, "channelID") + parsed, err := uuid.Parse(channelID) + if err != nil { + render.Render(w, r, ErrInvalidRequest(err)) + return + } + + channel, err := dbGetChannel(parsed.String()) + if err != nil { + if errors.Is(err, ErrChannelNotFound) { + render.Render(w, r, ErrNotFound) + } else { + slog.Error("channel: failed to fetch channel", "channelid", parsed.String(), "error", err) + render.Render(w, r, ErrInternal(err)) + } + return + } + + slog.Debug("channel: rendering channel", "channelid", channel.ID, "channelname", channel.Name) + if err := render.Render(w, r, NewChannelPayloadResponse(channel)); err != nil { + slog.Error("channel: failed to render channel", "channelid", parsed.String(), "error", err) + render.Render(w, r, ErrInternal(err)) + } +} + +func ListChannels(w http.ResponseWriter, r *http.Request) { + slog.Debug("channel: entering ListChannels handler") + dbChannels, err := dbGetAllChannels() + if err != nil { + if errors.Is(err, ErrChannelNotFound) { + render.Render(w, r, ErrNotFound) + } else { + slog.Error("channel: failed to fetch channels", "error", err) + render.Render(w, r, ErrInternal(err)) + } + return + } + + slog.Debug("channel: successfully fetched channels", "count", len(dbChannels)) + if err := render.RenderList(w, r, NewChannelListResponse(dbChannels)); err != nil { + slog.Error("channel: failed to render channel list response", "error", err) + render.Render(w, r, ErrInternal(err)) + return + } +} + +func newChannelID() uuid.UUID { + return uuid.New() +} + +func NewChannel(w http.ResponseWriter, r *http.Request) { + slog.Debug("channel: entering NewChannel handler") + err := r.ParseMultipartForm(64 << 10) + if err != nil { + slog.Error("user: failed to parse multipartform", "error", err) + http.Error(w, "Unable to parse form", http.StatusBadRequest) + return + } + + newChannelName := r.FormValue("name") + newChannelType := r.FormValue("type") + newChannelLocation := r.FormValue("location") + newChannelNotes := r.FormValue("notes") + if newChannelName == "" { + slog.Error("channel: channelname is empty") + http.Error(w, "Channel name cannot be empty", http.StatusBadRequest) + return + } + + newChannel := Channel{ + ID: newChannelID(), + Name: newChannelName, + Created: time.Now(), + Type: newChannelType, + Location: newChannelLocation, + Notes: newChannelNotes, + } + + slog.Debug("channel: adding new channel to database", "channelid", newChannel.ID, "channelname", newChannel.Name) + err = dbAddChannel(&newChannel) + if err != nil { + slog.Error("channel: failed to add new channel", "channelid", newChannel.ID, "channelname", newChannel.Name) + render.Render(w, r, ErrInternal(err)) + return + } + + slog.Debug("channel: successfully added new channel", "channelid", newChannel.ID, "channelname", newChannel.Name) + render.Render(w, r, NewChannelPayloadResponse(&newChannel)) + +} + +func DeleteChannel(w http.ResponseWriter, r *http.Request) { + slog.Debug("channel: entering DeleteChannel handler") + + channelID := chi.URLParam(r, "channelID") + parsed, err := uuid.Parse(channelID) + if err != nil { + render.Render(w, r, ErrInvalidRequest(err)) + return + } + + err = dbDeleteChannel(parsed.String()) + if err != nil { + if errors.Is(err, ErrChannelNotFound) { + render.Render(w, r, ErrNotFound) + } else { + slog.Error("channel: failed to delete channel", "channelid", parsed.String(), "error", err) + render.Render(w, r, ErrInternal(err)) + } + return + } + + slog.Debug("channel: deleted channel", "channelid", parsed.String()) + w.Write([]byte("Channel deleted successfully")) +} + +type Channel struct { + ID uuid.UUID + Name string + Created time.Time + Type string + Location string + Notes string +} + +type channelKey struct{} + +type ChannelPayload struct { + *Channel +} diff --git a/server/api/db.go b/server/api/db.go index a483cc4..cf305a6 100644 --- a/server/api/db.go +++ b/server/api/db.go @@ -12,6 +12,7 @@ import ( var ErrUserNotFound = errors.New("db: user not found") var ErrSessionNotFound = errors.New("db: session not found") +var ErrChannelNotFound = errors.New("db: channel not found") func dbGetUser(id string) (*User, error) { query := `SELECT id, name, password FROM users WHERE id = $1` @@ -131,3 +132,77 @@ func dbDeleteSession(jwtToken string) error { slog.Debug("db: session deleted") return nil } + +func dbAddChannel(channel *Channel) error { + query := `INSERT INTO channels (id, name, created, type, location, notes) VALUES ($1, $2, $3, $4, $5, $6)` + _, err := db.Pool.Exec(context.Background(), query, channel.ID, channel.Name, channel.Created, channel.Type, channel.Location, channel.Notes) + if err != nil { + slog.Error("db: failed to add channel", "channel", err, "channelid", channel.ID, "channelname", channel.Name) + return fmt.Errorf("failed to add channel") + } + + slog.Debug("db: channel added", "channelid", channel.ID, "channelname", channel.Name) + return nil +} + +func dbGetChannel(id string) (*Channel, error) { + query := `SELECT id, name, created, type, location, notes FROM channels WHERE id = $1` + var channel Channel + err := db.Pool.QueryRow(context.Background(), query, id).Scan(&channel.ID, &channel.Name, &channel.Created, &channel.Type, &channel.Location, &channel.Notes) + if errors.Is(err, pgx.ErrNoRows) { + slog.Debug("db: channel not found", "channelid", id) + return nil, ErrChannelNotFound + } else if err != nil { + slog.Error("db: failed to query channel", "error", err) + return nil, fmt.Errorf("failed to query channel") + } + + slog.Debug("db: channel found", "channelid", channel.ID, "channelname", channel.Name) + return &channel, nil +} + +func dbGetAllChannels() ([]*Channel, error) { + query := `SELECT id, name, created, type, location, notes FROM channels` + rows, err := db.Pool.Query(context.Background(), query) + if err != nil { + slog.Error("db: failed to query channels", "error", err) + return nil, fmt.Errorf("failed to query channels") + } + defer rows.Close() + + var channels []*Channel + for rows.Next() { + channel := &Channel{} + if err := rows.Scan(&channel.ID, &channel.Name, &channel.Created, &channel.Type, &channel.Location, &channel.Notes); err != nil { + slog.Error("db: failed to scan channel", "error", err) + return nil, fmt.Errorf("failed to scan channel") + } + channels = append(channels, channel) + } + if err := rows.Err(); err != nil { + slog.Error("db: row iteration error", "error", err) + return nil, fmt.Errorf("failed to iterate channels") + } + if len(channels) == 0 { + slog.Debug("db: no channels found") + return nil, ErrChannelNotFound + } + + slog.Debug("db: channel list returned") + return channels, nil +} + +func dbDeleteChannel(id string) error { + query := `DELETE FROM channels WHERE id = $1` + tag, err := db.Pool.Exec(context.Background(), query, id) + if err != nil { + slog.Error("db: failed to delete channel", "error", err) + return fmt.Errorf("failed to delete channel") + } + if tag.RowsAffected() == 0 { + return ErrChannelNotFound + } + + slog.Debug("db: channel deleted") + return nil +} diff --git a/server/api/response.go b/server/api/response.go index b7b04e2..59890d6 100644 --- a/server/api/response.go +++ b/server/api/response.go @@ -21,3 +21,19 @@ func NewUserListResponse(users []*User) []render.Renderer { func (u *UserPayload) Render(w http.ResponseWriter, r *http.Request) error { return nil } + +func NewChannelPayloadResponse(channel *Channel) *ChannelPayload { + return &ChannelPayload{Channel: channel} +} + +func NewChannelListResponse(channels []*Channel) []render.Renderer { + list := []render.Renderer{} + for _, channel := range channels { + list = append(list, NewChannelPayloadResponse(channel)) + } + return list +} + +func (c *ChannelPayload) Render(w http.ResponseWriter, r *http.Request) error { + return nil +}