diff --git a/main.go b/main.go index be2e77c..f6ae07d 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ func main() { panic(err) } r := chi.NewRouter() + r.Post("/user/login", s.Login) r.With(serve.WithPlayerID(sessions)).Get("/queue", s.Queue) http.ListenAndServe(":8080", r) } diff --git a/serve/consent.go b/serve/consent.go deleted file mode 100644 index ae83030..0000000 --- a/serve/consent.go +++ /dev/null @@ -1,38 +0,0 @@ -package serve - -import ( - "net/http" - "time" -) - -const cookieName = "__Host-consent-v1" - -// SetConsent registers a consent cookie on the response. -func SetConsent(w http.ResponseWriter) { - http.SetCookie(w, &http.Cookie{ - Name: cookieName, - Value: "given", - Expires: time.Now().Add(20 * 365 * 24 * time.Hour), - Path: "/", - Secure: true, - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }) -} - -// NeedsConsent is a middleware that immediately responds with a 403 if the -// request does not bear a consent cookie. -func NeedsConsent(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if _, err := r.Cookie(cookieName); err != nil { - http.Error(w, cookieFailed, http.StatusForbidden) - return - } - next.ServeHTTP(w, r) - }) -} - -const cookieFailed = ` -The requested resource requires consent to processing identifying information and storing necessary cookies. -I'm just a lil guy. The information is used solely for providing the service's functionality. -` diff --git a/serve/session.go b/serve/session.go index 3fa807f..e18fd70 100644 --- a/serve/session.go +++ b/serve/session.go @@ -21,6 +21,19 @@ func with[T any](ctx context.Context, v T) context.Context { const sessionCookie = "__Host-id-v1" +// SetSession sets a cookie carrying a session token on a response. +func SetSession(w http.ResponseWriter, s player.Session) { + http.SetCookie(w, &http.Cookie{ + Name: sessionCookie, + Value: s.String(), + Path: "/", + Expires: time.Now().Add(365 * 24 * time.Hour), + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) +} + // WithPlayerID is a middleware that adds a player ID to the request context // based on the session cookie content. If there is no such cookie, or its // value is invalid, the request fails with a 403 error. diff --git a/server.go b/server.go index 67145ab..5353c3b 100644 --- a/server.go +++ b/server.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "io" "log/slog" "net/http" "sync" @@ -20,6 +21,9 @@ import ( type Server struct { l *lobby.Lobby + creds player.RowQuerier + sessions player.Execer + mu sync.Mutex pp map[player.ID]*websocket.Conn } @@ -62,6 +66,40 @@ func (s *Server) left(p player.ID) { } } +func (s *Server) Login(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + slog := slog.With( + slog.String("remote", r.RemoteAddr), + slog.String("forwarded-for", r.Header.Get("X-Forwarded-For")), + slog.String("user-agent", r.UserAgent()), + ) + if err := r.ParseForm(); err != nil { + slog.WarnContext(ctx, "error parsing form on login", "err", err.Error()) + http.Error(w, "what", http.StatusBadRequest) + return + } + user, pass := r.PostFormValue("user"), r.PostFormValue("pass") + if user == "" || pass == "" { + slog.WarnContext(ctx, "missing user or pass on login") + http.Error(w, "missing credentials", http.StatusBadRequest) + return + } + p, err := player.Login(ctx, s.creds, user, pass) + if err != nil { + slog.ErrorContext(ctx, "login failed", "err", err.Error()) + http.Error(w, "no", http.StatusUnauthorized) + return + } + id, err := player.StartSession(ctx, s.sessions, p, time.Now()) + if err != nil { + slog.ErrorContext(ctx, "failed to create session", "player", p, "err", err.Error()) + http.Error(w, "something went wrong", http.StatusInternalServerError) + return + } + serve.SetSession(w, id) + io.WriteString(w, id.String()) +} + // Queue connects players to games. The connection immediately upgrades. // This handler MUST be wrapped in [serve.WithPlayerID]. func (s *Server) Queue(w http.ResponseWriter, r *http.Request) {