http response tooling written
This commit is contained in:
@@ -3,25 +3,24 @@ package site
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"ruben/inventory2/internal/site/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
// POST /accounts
|
// POST /accounts
|
||||||
func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) createAccount(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
email := r.FormValue("email")
|
email := r.FormValue("email")
|
||||||
if email == "" {
|
if email == "" {
|
||||||
http.Error(w, "no email provided", http.StatusBadRequest)
|
return nil, response.BadRequest().Msg("no email provided")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := getAccessTokenClaims(ctx).Subject
|
userID := getAccessTokenClaims(ctx).Subject
|
||||||
|
|
||||||
acct, err := s.accts.CreateAccount(ctx, userID, email)
|
acct, err := s.accts.CreateAccount(ctx, userID, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("failed to create account: %v", err), http.StatusInternalServerError)
|
return nil, response.Errorf("failed to create account: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//http.Redirect(w, r, fmt.Sprintf("/site/accounts/%d", acct.ID), http.StatusSeeOther)
|
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.ID)), nil
|
||||||
http.Redirect(w, r, fmt.Sprintf("/accounts/%d", acct.ID), http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-16
@@ -9,24 +9,24 @@ import (
|
|||||||
|
|
||||||
"ruben/inventory2/internal/consts"
|
"ruben/inventory2/internal/consts"
|
||||||
"ruben/inventory2/internal/domains/authentication"
|
"ruben/inventory2/internal/domains/authentication"
|
||||||
|
"ruben/inventory2/internal/site/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
// just keep this around long enough for testing auth middleware..
|
// just keep this around long enough for testing auth middleware..
|
||||||
func (s *Server) testAuthEndpoint(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) testAuthEndpoint(r *http.Request) (response.Response, error) {
|
||||||
fmt.Println("SUCCESS:", getAccessTokenClaims(r.Context()))
|
fmt.Println("AUTH TEST SUCCESS:", getAccessTokenClaims(r.Context()))
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
return response.TemporaryRedirect("/"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type customClaimsKey struct{}
|
type customClaimsKey struct{}
|
||||||
|
|
||||||
// auth middleware to verify access_token cookie and set custom claims in the request context
|
// auth middleware to verify access_token cookie and set custom claims in the request context
|
||||||
func (s *Server) authenticate(h http.Handler) http.Handler {
|
func (s *Server) authenticate(f response.HandlerFunc) response.HandlerFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return func(r *http.Request) (response.Response, error) {
|
||||||
ck, err := r.Cookie("access_token")
|
ck, err := r.Cookie("access_token")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
return response.TemporaryRedirect("/"), nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
@@ -34,22 +34,18 @@ func (s *Server) authenticate(h http.Handler) http.Handler {
|
|||||||
claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, ck.Value)
|
claims, expiration, err := s.auth.GetAccessTokenClaimsAndExpiration(ctx, ck.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, consts.ErrNotFound) {
|
if errors.Is(err, consts.ErrNotFound) {
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
return response.TemporaryRedirect("/"), nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Error(w, fmt.Sprintf("failed to authenticate: %v", err), http.StatusInternalServerError)
|
return nil, response.Errorf("failed to authenticate: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if expiration.Before(time.Now()) {
|
if expiration.Before(time.Now()) {
|
||||||
deleteCookieInResponse(w, "access_token")
|
return response.TemporaryRedirect("/").Cookie(getExpiredCookie("access_token")), nil
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h.ServeHTTP(w, r.WithContext(setAccessTokenClaims(ctx, claims)))
|
return f(r.WithContext(setAccessTokenClaims(ctx, claims)))
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getAccessTokenClaims(r *http.Request) (authentication.AccessTokenClaims, bool) {
|
func (s *Server) getAccessTokenClaims(r *http.Request) (authentication.AccessTokenClaims, bool) {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package site
|
|||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
func deleteCookieInResponse(w http.ResponseWriter, name string) {
|
func getExpiredCookie(name string) http.Cookie {
|
||||||
w.Header().Set("Set-Cookie", (&http.Cookie{
|
return http.Cookie{
|
||||||
Name: name,
|
Name: name,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: -1, // expire the cookie
|
MaxAge: -1, // expire the cookie
|
||||||
}).String())
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-28
@@ -3,44 +3,41 @@ package site
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"ruben/inventory2/internal/site/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /login
|
// GET /login
|
||||||
func (s *Server) loginPage(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) loginPage(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
state, err := s.auth.NewState(ctx)
|
state, err := s.auth.NewState(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("failed to generate random state: %v", err), http.StatusInternalServerError)
|
return nil, response.Errorf("failed to generate random state: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
base64EncodedState := fmt.Sprintf("%x", state[:])
|
base64EncodedState := fmt.Sprintf("%x", state[:])
|
||||||
|
|
||||||
http.Redirect(w, r, s.auth.AuthCodeURL(base64EncodedState), http.StatusTemporaryRedirect)
|
return response.TemporaryRedirect(s.auth.AuthCodeURL(base64EncodedState)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /login
|
// POST /login
|
||||||
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) login(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
email := r.FormValue("email")
|
email := r.FormValue("email")
|
||||||
if email == "" {
|
if email == "" {
|
||||||
http.Error(w, "no email provided", http.StatusBadRequest)
|
return nil, response.BadRequest().Msg("no email provided")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
acct, err := s.accts.GetAccountByEmail(ctx, email)
|
acct, err := s.accts.GetAccountByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("failed to create account: %v", err), http.StatusInternalServerError)
|
return nil, response.Errorf("failed to create account: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//http.Redirect(w, r, fmt.Sprintf("/site/accounts/%d", acct.ID), http.StatusSeeOther)
|
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.ID)), nil
|
||||||
http.Redirect(w, r, fmt.Sprintf("/accounts/%d", acct.ID), http.StatusSeeOther)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /login/callback
|
// GET /login/callback
|
||||||
func (s *Server) loginCallback(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) loginCallback(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
|
|
||||||
@@ -48,38 +45,37 @@ func (s *Server) loginCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
accessToken, expiration, err := s.auth.Exchange(ctx, q.Get("state"), q.Get("code"))
|
accessToken, expiration, err := s.auth.Exchange(ctx, q.Get("state"), q.Get("code"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Failed to exchange an authorization code for a token: %v", err), http.StatusUnauthorized)
|
return nil, response.Unauthorized().
|
||||||
return
|
Msg(fmt.Sprintf("Failed to exchange an authorization code for a token")).
|
||||||
|
Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set access_token cookie and redirect to a reasonable place
|
// set access_token cookie and redirect to a reasonable place
|
||||||
|
|
||||||
w.Header().Set("Set-Cookie", (&http.Cookie{
|
return response.TemporaryRedirect("/").
|
||||||
Name: "access_token",
|
Cookie(http.Cookie{
|
||||||
Value: accessToken,
|
Name: "access_token",
|
||||||
Path: "/",
|
Value: accessToken,
|
||||||
Expires: expiration,
|
Path: "/",
|
||||||
MaxAge: 0, // using Expiration instead
|
Expires: expiration,
|
||||||
Secure: true,
|
MaxAge: 0, // using Expiration instead
|
||||||
}).String())
|
Secure: true,
|
||||||
|
}), nil
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /logout
|
// GET /logout
|
||||||
func (s *Server) logoutPage(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) logoutPage(r *http.Request) (response.Response, error) {
|
||||||
host := r.Header.Get("X-Forwarded-Host")
|
host := r.Header.Get("X-Forwarded-Host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = r.Host
|
host = r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCookieInResponse(w, "access_token")
|
|
||||||
|
|
||||||
if ck, err := r.Cookie("access_token"); err == nil && ck != nil {
|
if ck, err := r.Cookie("access_token"); err == nil && ck != nil {
|
||||||
if err := s.auth.DeleteOAuthTokens(r.Context(), ck.Value); err != nil {
|
if err := s.auth.DeleteOAuthTokens(r.Context(), ck.Value); err != nil {
|
||||||
fmt.Println("[ERROR] failed to delete auth token:", err)
|
fmt.Println("[ERROR] failed to delete auth token:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, s.auth.GetLogoutURL(host).String(), http.StatusTemporaryRedirect)
|
return response.TemporaryRedirect(s.auth.GetLogoutURL(host).String()).
|
||||||
|
Cookie(getExpiredCookie("access_token")), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package redirect
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type (
|
||||||
|
Code int
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
MovedPermanently = Code(http.StatusMovedPermanently)
|
||||||
|
Found = Code(http.StatusFound)
|
||||||
|
SeeOther = Code(http.StatusSeeOther)
|
||||||
|
TemporaryRedirect = Code(http.StatusTemporaryRedirect)
|
||||||
|
PermanentRedirect = Code(http.StatusPermanentRedirect)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Code) Int() int {
|
||||||
|
return int(c)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"ruben/inventory2/internal/site/redirect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
cookieRes struct {
|
||||||
|
cookie http.Cookie
|
||||||
|
res Response
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Response = cookieRes{}
|
||||||
|
|
||||||
|
func Cookie(c http.Cookie) Response {
|
||||||
|
return cookieRes{
|
||||||
|
cookie: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) String() string {
|
||||||
|
if c.res != nil {
|
||||||
|
return fmt.Sprintf(`{"cookie": %q, "nested": %s}`, c.cookie, c.res)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`{"cookie": %q}`, c.cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) wrap(res Response) Response {
|
||||||
|
c.res = res
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) Status(code int) Response {
|
||||||
|
return Status(code).wrap(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) Redirect(code redirect.Code, to string) Response {
|
||||||
|
return Redirect(code, to).wrap(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) JSON(body any) Response {
|
||||||
|
return JSON(body).wrap(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) Cookie(ck http.Cookie) Response {
|
||||||
|
return Cookie(ck).wrap(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) getStatus() (int, bool) {
|
||||||
|
if c.res == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.res.getStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
|
if c.res == nil {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.res.getRedirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
if c.res == nil {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.res.getBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) getCookies() []http.Cookie {
|
||||||
|
if c.res != nil {
|
||||||
|
return append(c.res.getCookies(), c.cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []http.Cookie{c.cookie}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// ErrorResponse is an error
|
||||||
|
ErrorResponse struct {
|
||||||
|
err error
|
||||||
|
msg string
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
func Errorf(format string, args ...any) ErrorResponse {
|
||||||
|
return ErrorResponse{
|
||||||
|
err: fmt.Errorf(format, args...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BadRequest() ErrorResponse {
|
||||||
|
return ErrorResponse{
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotFound() ErrorResponse {
|
||||||
|
return ErrorResponse{
|
||||||
|
status: http.StatusNotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unauthorized() ErrorResponse {
|
||||||
|
return ErrorResponse{
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Forbidden() ErrorResponse {
|
||||||
|
return ErrorResponse{
|
||||||
|
status: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// builder pattern implementation
|
||||||
|
|
||||||
|
func (e ErrorResponse) Msg(msg string) ErrorResponse {
|
||||||
|
e.msg = msg
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrorResponse) Status(status int) ErrorResponse {
|
||||||
|
e.status = status
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrorResponse) Wrap(err error) ErrorResponse {
|
||||||
|
e.err = err
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// error implementation
|
||||||
|
|
||||||
|
func (e ErrorResponse) Error() string {
|
||||||
|
parts := make([]string, 0, 3)
|
||||||
|
|
||||||
|
if e.msg != "" {
|
||||||
|
parts = append(parts, e.msg)
|
||||||
|
} else if e.status != 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("status = %d", e.status))
|
||||||
|
}
|
||||||
|
if e.err != nil {
|
||||||
|
parts = append(parts, e.err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "status = 500"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, ": ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrorResponse) Unwrap() error {
|
||||||
|
return e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// nested response value resolution
|
||||||
|
|
||||||
|
func (e ErrorResponse) getStatus() (int, bool) {
|
||||||
|
if e.status != 0 {
|
||||||
|
return e.status, true
|
||||||
|
}
|
||||||
|
|
||||||
|
ce, ok := getError(e.err)
|
||||||
|
if ok {
|
||||||
|
return ce.getStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrorResponse) getMsg() (string, bool) {
|
||||||
|
if e.msg != "" {
|
||||||
|
return e.msg, true
|
||||||
|
}
|
||||||
|
|
||||||
|
ce, ok := getError(e.err)
|
||||||
|
if ok {
|
||||||
|
return ce.getMsg()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getError(err error) (e ErrorResponse, ok bool) {
|
||||||
|
ok = errors.As(err, &e)
|
||||||
|
return e, ok
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HandlerFunc = func(r *http.Request) (Response, error)
|
||||||
|
|
||||||
|
func Handler(f HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res, err := f(r)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
|
} else {
|
||||||
|
Write(w, r, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"ruben/inventory2/internal/site/redirect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
jsonRes struct {
|
||||||
|
body any
|
||||||
|
res Response
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Response = jsonRes{}
|
||||||
|
|
||||||
|
func JSON(body any) Response {
|
||||||
|
return jsonRes{
|
||||||
|
body: body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) String() string {
|
||||||
|
if j.res != nil {
|
||||||
|
return fmt.Sprintf(`{"body": %q, "nested": %s}`, j.body, j.res)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`{"body": %q}`, j.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) wrap(res Response) Response {
|
||||||
|
j.res = res
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) Status(code int) Response {
|
||||||
|
return Status(code).wrap(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) Redirect(code redirect.Code, to string) Response {
|
||||||
|
return Redirect(code, to).wrap(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) JSON(body any) Response {
|
||||||
|
j.body = body
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) Cookie(ck http.Cookie) Response {
|
||||||
|
return Cookie(ck).wrap(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) getStatus() (int, bool) {
|
||||||
|
if j.res == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return j.res.getStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
|
if j.res == nil {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return j.res.getRedirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
return io.NopCloser(buf), true, json.NewEncoder(buf).Encode(j.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) getCookies() []http.Cookie {
|
||||||
|
if j.res != nil {
|
||||||
|
return j.res.getCookies()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"ruben/inventory2/internal/site/redirect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
redirectRes struct {
|
||||||
|
code redirect.Code
|
||||||
|
to string
|
||||||
|
res Response
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Response = redirectRes{}
|
||||||
|
|
||||||
|
// convenience constructors
|
||||||
|
|
||||||
|
func MovedPermanently(to string) Response {
|
||||||
|
return redirectRes{
|
||||||
|
code: redirect.MovedPermanently,
|
||||||
|
to: to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Found(to string) Response {
|
||||||
|
return redirectRes{
|
||||||
|
code: redirect.Found,
|
||||||
|
to: to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SeeOther(to string) Response {
|
||||||
|
return redirectRes{
|
||||||
|
code: redirect.SeeOther,
|
||||||
|
to: to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TemporaryRedirect(to string) Response {
|
||||||
|
return redirectRes{
|
||||||
|
code: redirect.TemporaryRedirect,
|
||||||
|
to: to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PermanentRedirect(to string) Response {
|
||||||
|
return redirectRes{
|
||||||
|
code: redirect.PermanentRedirect,
|
||||||
|
to: to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Redirect(code redirect.Code, to string) Response {
|
||||||
|
return redirectRes{
|
||||||
|
code: code,
|
||||||
|
to: to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) String() string {
|
||||||
|
if r.res != nil {
|
||||||
|
return fmt.Sprintf(`{"redirect": {"code": %d, "to": %q}, "nested": %s}`, r.code, r.to, r.res)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`{"redirect": {"code": %d, "to": %q}}`, r.code, r.to)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) wrap(res Response) Response {
|
||||||
|
r.res = res
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) Status(code int) Response {
|
||||||
|
return Status(code).wrap(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) Redirect(code redirect.Code, to string) Response {
|
||||||
|
r.code = code
|
||||||
|
r.to = to
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) JSON(body any) Response {
|
||||||
|
return JSON(body).wrap(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) Cookie(ck http.Cookie) Response {
|
||||||
|
return Cookie(ck).wrap(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) getStatus() (int, bool) {
|
||||||
|
if r.res == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.res.getStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
|
return r.code, r.to, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
if r.res == nil {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.res.getBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) getCookies() []http.Cookie {
|
||||||
|
if r.res != nil {
|
||||||
|
return r.res.getCookies()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"ruben/inventory2/internal/site/redirect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Response interface {
|
||||||
|
Status(int) Response
|
||||||
|
Redirect(code redirect.Code, to string) Response
|
||||||
|
JSON(any) Response
|
||||||
|
Cookie(http.Cookie) Response
|
||||||
|
|
||||||
|
getStatus() (code int, ok bool)
|
||||||
|
getBody() (body io.ReadCloser, ok bool, err error)
|
||||||
|
getRedirect() (code redirect.Code, to string, ok bool)
|
||||||
|
getCookies() []http.Cookie
|
||||||
|
|
||||||
|
wrap(Response) Response
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"ruben/inventory2/internal/site/redirect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
statusRes struct {
|
||||||
|
code int
|
||||||
|
res Response
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Response = statusRes{}
|
||||||
|
|
||||||
|
func Status(code int) Response {
|
||||||
|
return statusRes{
|
||||||
|
code: code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) String() string {
|
||||||
|
if s.res != nil {
|
||||||
|
return fmt.Sprintf(`{"status": %d, "nested": %s}`, s.code, s.res)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`{"status": %d}`, s.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) wrap(res Response) Response {
|
||||||
|
s.res = res
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) Status(code int) Response {
|
||||||
|
s.code = code
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) Redirect(code redirect.Code, to string) Response {
|
||||||
|
return Redirect(code, to).wrap(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) JSON(body any) Response {
|
||||||
|
return JSON(body).wrap(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) Cookie(ck http.Cookie) Response {
|
||||||
|
return Cookie(ck).wrap(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) getStatus() (int, bool) {
|
||||||
|
return s.code, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
|
if s.res == nil {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.res.getRedirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
if s.res == nil {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.res.getBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s statusRes) getCookies() []http.Cookie {
|
||||||
|
if s.res != nil {
|
||||||
|
return s.res.getCookies()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Write(w http.ResponseWriter, r *http.Request, res Response) {
|
||||||
|
// w.Header() must be set before ResponseWriter.WriteHeader is called
|
||||||
|
// or redirect is attempted
|
||||||
|
hdrs := w.Header()
|
||||||
|
for _, ck := range res.getCookies() {
|
||||||
|
hdrs.Add("Set-Cookie", ck.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if code, to, ok := res.getRedirect(); ok {
|
||||||
|
http.Redirect(w, r, to, code.Int())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// the body is written after the status header, but it's read here
|
||||||
|
// first, because if an error is incurred, an error status header will
|
||||||
|
// need to be written.
|
||||||
|
body, bodySet, err := res.getBody()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(
|
||||||
|
w,
|
||||||
|
fmt.Sprintf("Failed to construct response body: %v", err.Error()),
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if status, ok := res.getStatus(); ok {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
}
|
||||||
|
if bodySet {
|
||||||
|
// will automatically set the status header,
|
||||||
|
// if w.WriteHeader wasn't already called
|
||||||
|
io.Copy(w, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteError(w http.ResponseWriter, err error) {
|
||||||
|
var status int
|
||||||
|
|
||||||
|
if e, ok := getError(err); ok {
|
||||||
|
if status, ok = e.getStatus(); !ok {
|
||||||
|
status = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Error(w, err.Error(), status)
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"ruben/inventory2/internal/domains/authentication"
|
"ruben/inventory2/internal/domains/authentication"
|
||||||
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
|
etsy_platform "ruben/inventory2/internal/domains/platforms/etsy"
|
||||||
"ruben/inventory2/internal/domains/raw_events"
|
"ruben/inventory2/internal/domains/raw_events"
|
||||||
|
"ruben/inventory2/internal/site/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -90,16 +91,16 @@ func NewServer(
|
|||||||
|
|
||||||
// api routes
|
// api routes
|
||||||
|
|
||||||
s.mux.HandleFunc("GET /login", s.loginPage)
|
s.mux.HandleFunc("GET /login", response.Handler(s.loginPage))
|
||||||
s.mux.HandleFunc("GET /login/callback", s.loginCallback)
|
s.mux.HandleFunc("GET /login/callback", response.Handler(s.loginCallback))
|
||||||
s.mux.HandleFunc("GET /logout", s.logoutPage)
|
s.mux.HandleFunc("GET /logout", response.Handler(s.logoutPage))
|
||||||
s.mux.Handle("POST /accounts", s.authenticate(http.HandlerFunc(s.createAccount)))
|
s.mux.Handle("POST /accounts", response.Handler(s.authenticate(s.createAccount)))
|
||||||
|
|
||||||
// TODO: eliminate once no longer used.
|
// TODO: eliminate once no longer used.
|
||||||
s.mux.HandleFunc("POST /login", s.login)
|
s.mux.HandleFunc("POST /login", response.Handler(s.login))
|
||||||
|
|
||||||
// TODO: get rid of this, once we're confident this isn't needed...
|
// TODO: get rid of this, once we're confident this isn't needed...
|
||||||
s.mux.Handle("GET /test-auth", s.authenticate(http.HandlerFunc(s.testAuthEndpoint)))
|
s.mux.Handle("GET /test-auth", response.Handler(s.authenticate(s.testAuthEndpoint)))
|
||||||
|
|
||||||
// webpage content
|
// webpage content
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user