backend barebone done, big chunk of work done
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"messenger/internal/auth"
|
||||
"messenger/internal/platform/config"
|
||||
"messenger/internal/realtime"
|
||||
"messenger/internal/store"
|
||||
)
|
||||
|
||||
type apiHandler struct {
|
||||
store *store.Store
|
||||
tokens *auth.TokenManager
|
||||
hub *realtime.Hub
|
||||
log *slog.Logger
|
||||
secure bool
|
||||
}
|
||||
|
||||
// NewHandler builds the /api/v1 route tree with all middleware applied.
|
||||
func NewHandler(st *store.Store, tm *auth.TokenManager, hub *realtime.Hub, log *slog.Logger, cfg *config.Config) http.Handler {
|
||||
h := &apiHandler{
|
||||
store: st,
|
||||
tokens: tm,
|
||||
hub: hub,
|
||||
log: log,
|
||||
secure: cfg.Environment == "production",
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
authHandler := requireAuth(tm, http.HandlerFunc(h.websocket))
|
||||
|
||||
mux.HandleFunc("GET /api/v1/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "pong"})
|
||||
})
|
||||
|
||||
mux.HandleFunc("POST /api/v1/auth/register", h.register)
|
||||
mux.HandleFunc("POST /api/v1/auth/login", h.login)
|
||||
mux.HandleFunc("POST /api/v1/auth/refresh", h.refresh)
|
||||
mux.HandleFunc("POST /api/v1/auth/logout", h.logout)
|
||||
|
||||
mux.Handle("GET /api/v1/users/me", requireAuth(tm, h.me))
|
||||
mux.Handle("GET /api/v1/users", requireAuth(tm, h.searchUsers))
|
||||
|
||||
mux.Handle("GET /api/v1/contacts", requireAuth(tm, h.listContacts))
|
||||
mux.Handle("POST /api/v1/contacts", requireAuth(tm, h.addContact))
|
||||
mux.Handle("DELETE /api/v1/contacts/{user_id}", requireAuth(tm, h.removeContact))
|
||||
|
||||
mux.Handle("GET /api/v1/conversations", requireAuth(tm, h.listConversations))
|
||||
mux.Handle("POST /api/v1/conversations", requireAuth(tm, h.createDM))
|
||||
mux.Handle("GET /api/v1/conversations/{id}/messages", requireAuth(tm, h.listMessages))
|
||||
mux.Handle("POST /api/v1/conversations/{id}/messages", requireAuth(tm, h.sendMessage))
|
||||
|
||||
mux.Handle("GET /ws", authHandler)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func (h *apiHandler) websocket(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := userIDFrom(r.Context())
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "unauthenticated")
|
||||
return
|
||||
}
|
||||
h.hub.HandleWS(w, r, userID)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"messenger/internal/auth"
|
||||
"messenger/internal/store"
|
||||
)
|
||||
|
||||
var usernameRe = regexp.MustCompile(`^[a-z0-9_]{3,32}$`)
|
||||
|
||||
type userJSON struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func toUserJSON(u *store.User) userJSON {
|
||||
return userJSON{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
DisplayName: u.DisplayName,
|
||||
Avatar: u.Avatar,
|
||||
CreatedAt: u.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
type credentialsRequest struct {
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (h *apiHandler) register(w http.ResponseWriter, r *http.Request) {
|
||||
var req credentialsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
if !usernameRe.MatchString(req.Username) {
|
||||
writeError(w, http.StatusBadRequest, "username must be 3-32 chars of [a-z0-9_]")
|
||||
return
|
||||
}
|
||||
if req.DisplayName == "" || len(req.DisplayName) > 64 {
|
||||
writeError(w, http.StatusBadRequest, "display_name must be 1-64 chars")
|
||||
return
|
||||
}
|
||||
if len(req.Password) < 8 || len(req.Password) > 128 {
|
||||
writeError(w, http.StatusBadRequest, "password must be 8-128 chars")
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := auth.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
user, err := h.store.CreateUser(r.Context(), req.Username, req.DisplayName, hash)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrDuplicate) {
|
||||
writeError(w, http.StatusConflict, "username already taken")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
h.issueSession(w, r, user.ID)
|
||||
writeJSON(w, http.StatusCreated, toUserJSON(user))
|
||||
}
|
||||
|
||||
func (h *apiHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
var req credentialsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
user, err := h.store.GetUserByUsername(r.Context(), req.Username)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
writeError(w, http.StatusUnauthorized, "invalid credentials")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
ok, err := auth.VerifyPassword(user.PasswordHash, req.Password)
|
||||
if err != nil || !ok {
|
||||
writeError(w, http.StatusUnauthorized, "invalid credentials")
|
||||
return
|
||||
}
|
||||
h.issueSession(w, r, user.ID)
|
||||
writeJSON(w, http.StatusOK, toUserJSON(user))
|
||||
}
|
||||
|
||||
func (h *apiHandler) refresh(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(refreshCookieName)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusUnauthorized, "missing refresh token")
|
||||
return
|
||||
}
|
||||
session, err := h.store.GetSessionByRefreshHash(r.Context(), auth.HashRefreshToken(cookie.Value))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusUnauthorized, "invalid refresh token")
|
||||
return
|
||||
}
|
||||
if session.RevokedAt != nil {
|
||||
writeError(w, http.StatusUnauthorized, "session revoked")
|
||||
return
|
||||
}
|
||||
if time.Now().After(session.ExpiresAt) {
|
||||
writeError(w, http.StatusUnauthorized, "session expired")
|
||||
return
|
||||
}
|
||||
|
||||
// Rotation: revoke the used session and issue a fresh pair.
|
||||
if err := h.store.RevokeSession(r.Context(), session.ID); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
h.issueSession(w, r, session.UserID)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *apiHandler) logout(w http.ResponseWriter, r *http.Request) {
|
||||
if cookie, err := r.Cookie(refreshCookieName); err == nil {
|
||||
session, err := h.store.GetSessionByRefreshHash(r.Context(), auth.HashRefreshToken(cookie.Value))
|
||||
if err == nil {
|
||||
_ = h.store.RevokeSession(r.Context(), session.ID)
|
||||
}
|
||||
}
|
||||
clearAuthCookies(w)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *apiHandler) me(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := userIDFrom(r.Context())
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "unauthenticated")
|
||||
return
|
||||
}
|
||||
user, err := h.store.GetUserByID(r.Context(), userID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, toUserJSON(user))
|
||||
}
|
||||
|
||||
func (h *apiHandler) issueSession(w http.ResponseWriter, r *http.Request, userID int64) {
|
||||
access, err := h.tokens.IssueAccessToken(userID, accessTTL)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
rawRefresh, refreshHash, err := auth.GenerateRefreshToken()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
_, err = h.store.CreateSession(r.Context(), userID, refreshHash, r.UserAgent(), r.RemoteAddr, time.Now().Add(refreshTTL))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
setAuthCookies(w, access, rawRefresh, h.secure)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"messenger/internal/store"
|
||||
)
|
||||
|
||||
func (h *apiHandler) searchUsers(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
users, err := h.store.SearchUsers(r.Context(), query, 20)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
out := make([]userJSON, 0, len(users))
|
||||
for i := range users {
|
||||
out = append(out, toUserJSON(&users[i]))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
type contactJSON struct {
|
||||
User userJSON `json:"user"`
|
||||
AddedAt string `json:"added_at"`
|
||||
}
|
||||
|
||||
func (h *apiHandler) listContacts(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := userIDFrom(r.Context())
|
||||
contacts, err := h.store.ListContacts(r.Context(), userID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
out := make([]contactJSON, 0, len(contacts))
|
||||
for _, c := range contacts {
|
||||
u, err := h.store.GetUserByID(r.Context(), c.ContactID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, contactJSON{User: toUserJSON(u), AddedAt: c.CreatedAt.Format("2006-01-02T15:04:05Z07:00")})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *apiHandler) addContact(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := userIDFrom(r.Context())
|
||||
var req struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid user_id")
|
||||
return
|
||||
}
|
||||
if req.UserID == userID {
|
||||
writeError(w, http.StatusBadRequest, "cannot add yourself")
|
||||
return
|
||||
}
|
||||
if _, err := h.store.GetUserByID(r.Context(), req.UserID); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
writeError(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
if err := h.store.AddContact(r.Context(), userID, req.UserID); err != nil {
|
||||
if errors.Is(err, store.ErrDuplicate) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "already a contact"})
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *apiHandler) removeContact(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := userIDFrom(r.Context())
|
||||
contactID, err := strconv.ParseInt(r.PathValue("user_id"), 10, 64)
|
||||
if err != nil || contactID <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid user_id")
|
||||
return
|
||||
}
|
||||
if err := h.store.RemoveContact(r.Context(), userID, contactID); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"messenger/internal/realtime"
|
||||
"messenger/internal/store"
|
||||
)
|
||||
|
||||
type conversationJSON struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastMessage *messageJSON `json:"last_message,omitempty"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
}
|
||||
|
||||
type messageJSON struct {
|
||||
ID int64 `json:"id"`
|
||||
ConversationID int64 `json:"conversation_id"`
|
||||
SenderID int64 `json:"sender_id"`
|
||||
Body string `json:"body"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
func toMessageJSON(m *store.Message) messageJSON {
|
||||
return messageJSON{
|
||||
ID: m.ID,
|
||||
ConversationID: m.ConversationID,
|
||||
SenderID: m.SenderID,
|
||||
Body: m.Body,
|
||||
CreatedAt: m.CreatedAt,
|
||||
EditedAt: m.EditedAt,
|
||||
DeletedAt: m.DeletedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *apiHandler) listConversations(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := userIDFrom(r.Context())
|
||||
summaries, err := h.store.ListConversationsForUser(r.Context(), userID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
out := make([]conversationJSON, 0, len(summaries))
|
||||
for _, s := range summaries {
|
||||
item := conversationJSON{
|
||||
ID: s.ID,
|
||||
Type: s.Type,
|
||||
CreatedAt: s.CreatedAt,
|
||||
UnreadCount: s.UnreadCount,
|
||||
}
|
||||
if s.LastMessage != nil {
|
||||
msg := toMessageJSON(s.LastMessage)
|
||||
item.LastMessage = &msg
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *apiHandler) createDM(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := userIDFrom(r.Context())
|
||||
var req struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid user_id")
|
||||
return
|
||||
}
|
||||
if req.UserID == userID {
|
||||
writeError(w, http.StatusBadRequest, "cannot chat with yourself")
|
||||
return
|
||||
}
|
||||
conv, err := h.store.CreateDM(r.Context(), userID, req.UserID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, conversationJSON{ID: conv.ID, Type: conv.Type, CreatedAt: conv.CreatedAt})
|
||||
}
|
||||
|
||||
func (h *apiHandler) listMessages(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := userIDFrom(r.Context())
|
||||
convID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil || convID <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid conversation id")
|
||||
return
|
||||
}
|
||||
member, err := h.store.IsMember(r.Context(), convID, userID)
|
||||
if err != nil || !member {
|
||||
writeError(w, http.StatusForbidden, "not a member")
|
||||
return
|
||||
}
|
||||
|
||||
limit := 50
|
||||
if v := r.URL.Query().Get("limit"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 100 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
var before int64
|
||||
if v := r.URL.Query().Get("before"); v != "" {
|
||||
before, _ = strconv.ParseInt(v, 10, 64)
|
||||
}
|
||||
|
||||
messages, err := h.store.ListMessages(r.Context(), convID, before, limit)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
out := make([]messageJSON, 0, len(messages))
|
||||
for i := range messages {
|
||||
out = append(out, toMessageJSON(&messages[i]))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
type sendMessageRequest struct {
|
||||
Body string `json:"body"`
|
||||
TempID string `json:"temp_id"`
|
||||
}
|
||||
|
||||
func (h *apiHandler) sendMessage(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := userIDFrom(r.Context())
|
||||
convID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil || convID <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "invalid conversation id")
|
||||
return
|
||||
}
|
||||
member, err := h.store.IsMember(r.Context(), convID, userID)
|
||||
if err != nil || !member {
|
||||
writeError(w, http.StatusForbidden, "not a member")
|
||||
return
|
||||
}
|
||||
|
||||
var req sendMessageRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
if req.Body == "" {
|
||||
writeError(w, http.StatusBadRequest, "body is required")
|
||||
return
|
||||
}
|
||||
if len(req.Body) > 4000 {
|
||||
writeError(w, http.StatusBadRequest, "body too long (max 4000)")
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := h.store.InsertMessage(r.Context(), convID, userID, req.Body)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
return
|
||||
}
|
||||
|
||||
h.hub.PublishMessageCreated(convID, realtime.MessageCreated{
|
||||
TempID: req.TempID,
|
||||
ConversationID: convID,
|
||||
MessageID: msg.ID,
|
||||
SenderID: userID,
|
||||
Body: msg.Body,
|
||||
CreatedAt: msg.CreatedAt,
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusCreated, toMessageJSON(msg))
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"messenger/internal/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
accessCookieName = "messenger_access"
|
||||
refreshCookieName = "messenger_refresh"
|
||||
accessTTL = 15 * time.Minute
|
||||
refreshTTL = 30 * 24 * time.Hour
|
||||
)
|
||||
|
||||
type ctxKey string
|
||||
|
||||
// ctxKeyUserID is the context key carrying the authenticated user ID.
|
||||
const ctxKeyUserID ctxKey = "messenger.userID"
|
||||
|
||||
func userIDFrom(ctx context.Context) (int64, bool) {
|
||||
id, ok := ctx.Value(ctxKeyUserID).(int64)
|
||||
return id, ok
|
||||
}
|
||||
|
||||
func withUserID(ctx context.Context, userID int64) context.Context {
|
||||
return context.WithValue(ctx, ctxKeyUserID, userID)
|
||||
}
|
||||
|
||||
// requireAuth parses the access-token cookie and injects the user ID
|
||||
// into the request context.
|
||||
func requireAuth(tm *auth.TokenManager, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(accessCookieName)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusUnauthorized, "missing access token")
|
||||
return
|
||||
}
|
||||
userID, err := tm.ParseAccessToken(cookie.Value)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusUnauthorized, "invalid access token")
|
||||
return
|
||||
}
|
||||
next(w, r.WithContext(withUserID(r.Context(), userID)))
|
||||
}
|
||||
}
|
||||
|
||||
func setAuthCookies(w http.ResponseWriter, accessToken, refreshToken string, secure bool) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: accessCookieName,
|
||||
Value: accessToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: int(accessTTL.Seconds()),
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: refreshCookieName,
|
||||
Value: refreshToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: int(refreshTTL.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
func clearAuthCookies(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{Name: accessCookieName, Path: "/", MaxAge: -1})
|
||||
http.SetCookie(w, &http.Cookie{Name: refreshCookieName, Path: "/", MaxAge: -1})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Package api implements the HTTP handlers for the messenger REST API.
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"messenger/internal/store"
|
||||
)
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(body); err != nil {
|
||||
http.Error(w, `{"error":"failed to encode response"}`, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, message string) {
|
||||
writeJSON(w, status, map[string]string{"error": message})
|
||||
}
|
||||
|
||||
func writeStoreError(w http.ResponseWriter, err error) {
|
||||
switch {
|
||||
case errors.Is(err, store.ErrNotFound):
|
||||
writeError(w, http.StatusNotFound, "not found")
|
||||
case errors.Is(err, store.ErrDuplicate):
|
||||
writeError(w, http.StatusConflict, "already exists")
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, "internal server error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user