basic account creation flow created

This commit is contained in:
2026-01-24 16:45:38 -07:00
parent 7d37fae332
commit 1c305cd494
14 changed files with 649 additions and 37 deletions
+11 -8
View File
@@ -2,10 +2,10 @@ package accounts
import (
"errors"
"fmt"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"ruben/inventory2/internal/consts"
"ruben/inventory2/internal/domains/accounts"
@@ -31,6 +31,8 @@ func Routes(
accts: accts,
}
r.POST("", response.Handler(as.createAccount))
syncGroups := r.Group("/:acctID/inventory/sync-groups")
syncGroups.POST("", response.Handler(as.saveNewSyncGroup))
@@ -41,19 +43,17 @@ func Routes(
draftListings.DELETE("/:orderIndex", response.Handler(as.deleteSyncGroupListingDraft))
}
// TODO: test this endpoint again, to make it work.
// POST /
func (s *accountSubrouter) createAccount(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
email := r.FormValue("email")
if email == "" {
return nil, response.BadRequest().Msg("no email provided")
}
// TODO: get email from the user's account profile (how?) - will need to flesh out the account creation process later on
email := uuid.NewString() + "@example.com"
userID := auth.GetIdentity(ctx).User.UserID
acct, err := s.accts.CreateAccount(ctx, userID, email)
_, err := s.accts.CreateAccount(ctx, userID, email)
if err != nil {
if errors.Is(err, consts.ErrConflict) {
return nil, response.Conflict().
@@ -62,7 +62,10 @@ func (s *accountSubrouter) createAccount(c *gin.Context) (response.Response, err
return nil, response.Errorf("failed to create account: %w", err)
}
return response.SeeOther(fmt.Sprintf("/ui/accounts/%d", acct.AccountID)), nil
return response.StatusCreated().
// force a reload to ensure an sse connection is made.
// user should automatically be redirected to the appropriate page based on their current location and/or login/account statuses.
HXRefresh("true"), nil
}
// POST /:acctID/inventory/sync-groups/draft/listings
+55
View File
@@ -59,6 +59,54 @@ func (b bodyRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(b)
}
func (b bodyRes) HXPushURL(v string) Response {
return HXPushURL(v).wrap(b)
}
func (b bodyRes) HXLocation(v string) Response {
return HXLocation(v).wrap(b)
}
func (b bodyRes) HXPushUrl(v string) Response {
return HXPushUrl(v).wrap(b)
}
func (b bodyRes) HXRedirect(v string) Response {
return HXRedirect(v).wrap(b)
}
func (b bodyRes) HXRefresh(v string) Response {
return HXRefresh(v).wrap(b)
}
func (b bodyRes) HXReplaceUrl(v string) Response {
return HXReplaceUrl(v).wrap(b)
}
func (b bodyRes) HXReswap(v string) Response {
return HXReswap(v).wrap(b)
}
func (b bodyRes) HXRetarget(v string) Response {
return HXRetarget(v).wrap(b)
}
func (b bodyRes) HXReselect(v string) Response {
return HXReselect(v).wrap(b)
}
func (b bodyRes) HXTrigger(v string) Response {
return HXTrigger(v).wrap(b)
}
func (b bodyRes) HXTriggerAfterSettle(v string) Response {
return HXTriggerAfterSettle(v).wrap(b)
}
func (b bodyRes) HXTriggerAfterSwap(v string) Response {
return HXTriggerAfterSwap(v).wrap(b)
}
func (b bodyRes) GetStatus() (int, bool) {
if b.res == nil {
return 0, false
@@ -67,6 +115,13 @@ func (b bodyRes) GetStatus() (int, bool) {
return b.res.GetStatus()
}
func (b bodyRes) getHeaders() [][2]string {
if b.res != nil {
return b.res.getHeaders()
}
return nil
}
func (b bodyRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if b.res == nil {
return 0, "", false
+55
View File
@@ -58,6 +58,54 @@ func (c cookieRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(c)
}
func (c cookieRes) HXPushURL(v string) Response {
return HXPushURL(v).wrap(c)
}
func (c cookieRes) HXLocation(v string) Response {
return HXLocation(v).wrap(c)
}
func (c cookieRes) HXPushUrl(v string) Response {
return HXPushUrl(v).wrap(c)
}
func (c cookieRes) HXRedirect(v string) Response {
return HXRedirect(v).wrap(c)
}
func (c cookieRes) HXRefresh(v string) Response {
return HXRefresh(v).wrap(c)
}
func (c cookieRes) HXReplaceUrl(v string) Response {
return HXReplaceUrl(v).wrap(c)
}
func (c cookieRes) HXReswap(v string) Response {
return HXReswap(v).wrap(c)
}
func (c cookieRes) HXRetarget(v string) Response {
return HXRetarget(v).wrap(c)
}
func (c cookieRes) HXReselect(v string) Response {
return HXReselect(v).wrap(c)
}
func (c cookieRes) HXTrigger(v string) Response {
return HXTrigger(v).wrap(c)
}
func (c cookieRes) HXTriggerAfterSettle(v string) Response {
return HXTriggerAfterSettle(v).wrap(c)
}
func (c cookieRes) HXTriggerAfterSwap(v string) Response {
return HXTriggerAfterSwap(v).wrap(c)
}
func (c cookieRes) GetStatus() (int, bool) {
if c.res == nil {
return 0, false
@@ -66,6 +114,13 @@ func (c cookieRes) GetStatus() (int, bool) {
return c.res.GetStatus()
}
func (c cookieRes) getHeaders() [][2]string {
if c.res != nil {
return c.res.getHeaders()
}
return nil
}
func (c cookieRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if c.res == nil {
return 0, "", false
+212
View File
@@ -0,0 +1,212 @@
package response
import (
"fmt"
"io"
"net/http"
"ruben/inventory2/internal/server/redirect"
)
type (
headerRes struct {
name string
value string
res Response
}
)
var _ Response = headerRes{}
func header(name, value string) Response {
return headerRes{
name: name,
value: value,
}
}
func HXPushURL(v string) Response {
return header("HX-Push-Url", v)
}
func HXLocation(v string) Response {
return header("HX-Location", v)
}
func HXPushUrl(v string) Response {
return header("HX-Push-Url", v)
}
func HXRedirect(v string) Response {
return header("HX-Redirect", v)
}
func HXRefresh(v string) Response {
return header("HX-Refresh", v)
}
func HXReplaceUrl(v string) Response {
return header("HX-Replace-Url", v)
}
func HXReswap(v string) Response {
return header("HX-Reswap", v)
}
func HXRetarget(v string) Response {
return header("HX-Retarget", v)
}
func HXReselect(v string) Response {
return header("HX-Reselect", v)
}
func HXTrigger(v string) Response {
return header("HX-Trigger", v)
}
func HXTriggerAfterSettle(v string) Response {
return header("HX-Trigger-After-Settle", v)
}
func HXTriggerAfterSwap(v string) Response {
return header("HX-Trigger-After-Swap", v)
}
func (h headerRes) String() string {
if h.res != nil {
return fmt.Sprintf(`{"header": {"name": %q, "value": %q}, "nested": %s}`, h.name, h.value, h.res)
}
return fmt.Sprintf(`{"header": {"name": %q, "value": %q}}`, h.name, h.value)
}
func (h headerRes) wrap(res Response) Response {
h.res = res
return h
}
func (h headerRes) Status(code int) Response {
return Status(code).wrap(h)
}
func (h headerRes) Header(name, value string) Response {
h.name = name
h.value = value
return h
}
func (h headerRes) Redirect(code redirect.Code, to string) Response {
return Redirect(code, to).wrap(h)
}
func (h headerRes) HTML(body []byte) Response {
return HTML(body).wrap(h)
}
func (h headerRes) JSON(body any) Response {
return JSON(body).wrap(h)
}
func (h headerRes) Body(body io.ReadCloser) Response {
return Body(body).wrap(h)
}
func (h headerRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(h)
}
func (h headerRes) HXPushURL(v string) Response {
return HXPushURL(v).wrap(h)
}
func (h headerRes) HXLocation(v string) Response {
return HXLocation(v).wrap(h)
}
func (h headerRes) HXPushUrl(v string) Response {
return HXPushUrl(v).wrap(h)
}
func (h headerRes) HXRedirect(v string) Response {
return HXRedirect(v).wrap(h)
}
func (h headerRes) HXRefresh(v string) Response {
return HXRefresh(v).wrap(h)
}
func (h headerRes) HXReplaceUrl(v string) Response {
return HXReplaceUrl(v).wrap(h)
}
func (h headerRes) HXReswap(v string) Response {
return HXReswap(v).wrap(h)
}
func (h headerRes) HXRetarget(v string) Response {
return HXRetarget(v).wrap(h)
}
func (h headerRes) HXReselect(v string) Response {
return HXReselect(v).wrap(h)
}
func (h headerRes) HXTrigger(v string) Response {
return HXTrigger(v).wrap(h)
}
func (h headerRes) HXTriggerAfterSettle(v string) Response {
return HXTriggerAfterSettle(v).wrap(h)
}
func (h headerRes) HXTriggerAfterSwap(v string) Response {
return HXTriggerAfterSwap(v).wrap(h)
}
func (h headerRes) GetStatus() (int, bool) {
if h.res == nil {
return 0, false
}
return h.res.GetStatus()
}
func (h headerRes) getHeaders() [][2]string {
hdr := [2]string{h.name, h.value}
if h.res == nil {
return [][2]string{hdr}
}
return append(h.res.getHeaders(), hdr)
}
func (h headerRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if h.res == nil {
return 0, "", false
}
return h.res.GetRedirect()
}
func (h headerRes) getBody() (body io.ReadCloser, ok bool, err error) {
if h.res == nil {
return nil, false, nil
}
return h.res.getBody()
}
func (h headerRes) getCookies() []http.Cookie {
if h.res != nil {
return h.res.getCookies()
}
return nil
}
func (h headerRes) getContentType() (contentType string, ok bool) {
if h.res != nil {
return h.res.getContentType()
}
return "", false
}
+55
View File
@@ -60,6 +60,54 @@ func (h htmlRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(h)
}
func (h htmlRes) HXPushURL(v string) Response {
return HXPushURL(v).wrap(h)
}
func (h htmlRes) HXLocation(v string) Response {
return HXLocation(v).wrap(h)
}
func (h htmlRes) HXPushUrl(v string) Response {
return HXPushUrl(v).wrap(h)
}
func (h htmlRes) HXRedirect(v string) Response {
return HXRedirect(v).wrap(h)
}
func (h htmlRes) HXRefresh(v string) Response {
return HXRefresh(v).wrap(h)
}
func (h htmlRes) HXReplaceUrl(v string) Response {
return HXReplaceUrl(v).wrap(h)
}
func (h htmlRes) HXReswap(v string) Response {
return HXReswap(v).wrap(h)
}
func (h htmlRes) HXRetarget(v string) Response {
return HXRetarget(v).wrap(h)
}
func (h htmlRes) HXReselect(v string) Response {
return HXReselect(v).wrap(h)
}
func (h htmlRes) HXTrigger(v string) Response {
return HXTrigger(v).wrap(h)
}
func (h htmlRes) HXTriggerAfterSettle(v string) Response {
return HXTriggerAfterSettle(v).wrap(h)
}
func (h htmlRes) HXTriggerAfterSwap(v string) Response {
return HXTriggerAfterSwap(v).wrap(h)
}
func (h htmlRes) GetStatus() (int, bool) {
if h.res == nil {
return 0, false
@@ -68,6 +116,13 @@ func (h htmlRes) GetStatus() (int, bool) {
return h.res.GetStatus()
}
func (h htmlRes) getHeaders() [][2]string {
if h.res != nil {
return h.res.getHeaders()
}
return nil
}
func (h htmlRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if h.res == nil {
return 0, "", false
+55
View File
@@ -61,6 +61,54 @@ func (j jsonRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(j)
}
func (j jsonRes) HXPushURL(v string) Response {
return HXPushURL(v).wrap(j)
}
func (j jsonRes) HXLocation(v string) Response {
return HXLocation(v).wrap(j)
}
func (j jsonRes) HXPushUrl(v string) Response {
return HXPushUrl(v).wrap(j)
}
func (j jsonRes) HXRedirect(v string) Response {
return HXRedirect(v).wrap(j)
}
func (j jsonRes) HXRefresh(v string) Response {
return HXRefresh(v).wrap(j)
}
func (j jsonRes) HXReplaceUrl(v string) Response {
return HXReplaceUrl(v).wrap(j)
}
func (j jsonRes) HXReswap(v string) Response {
return HXReswap(v).wrap(j)
}
func (j jsonRes) HXRetarget(v string) Response {
return HXRetarget(v).wrap(j)
}
func (j jsonRes) HXReselect(v string) Response {
return HXReselect(v).wrap(j)
}
func (j jsonRes) HXTrigger(v string) Response {
return HXTrigger(v).wrap(j)
}
func (j jsonRes) HXTriggerAfterSettle(v string) Response {
return HXTriggerAfterSettle(v).wrap(j)
}
func (j jsonRes) HXTriggerAfterSwap(v string) Response {
return HXTriggerAfterSwap(v).wrap(j)
}
func (j jsonRes) GetStatus() (int, bool) {
if j.res == nil {
return 0, false
@@ -69,6 +117,13 @@ func (j jsonRes) GetStatus() (int, bool) {
return j.res.GetStatus()
}
func (j jsonRes) getHeaders() [][2]string {
if j.res != nil {
return j.res.getHeaders()
}
return nil
}
func (j jsonRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if j.res == nil {
return 0, "", false
+55
View File
@@ -99,6 +99,54 @@ func (r redirectRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(r)
}
func (r redirectRes) HXPushURL(v string) Response {
return HXPushURL(v).wrap(r)
}
func (r redirectRes) HXLocation(v string) Response {
return HXLocation(v).wrap(r)
}
func (r redirectRes) HXPushUrl(v string) Response {
return HXPushUrl(v).wrap(r)
}
func (r redirectRes) HXRedirect(v string) Response {
return HXRedirect(v).wrap(r)
}
func (r redirectRes) HXRefresh(v string) Response {
return HXRefresh(v).wrap(r)
}
func (r redirectRes) HXReplaceUrl(v string) Response {
return HXReplaceUrl(v).wrap(r)
}
func (r redirectRes) HXReswap(v string) Response {
return HXReswap(v).wrap(r)
}
func (r redirectRes) HXRetarget(v string) Response {
return HXRetarget(v).wrap(r)
}
func (r redirectRes) HXReselect(v string) Response {
return HXReselect(v).wrap(r)
}
func (r redirectRes) HXTrigger(v string) Response {
return HXTrigger(v).wrap(r)
}
func (r redirectRes) HXTriggerAfterSettle(v string) Response {
return HXTriggerAfterSettle(v).wrap(r)
}
func (r redirectRes) HXTriggerAfterSwap(v string) Response {
return HXTriggerAfterSwap(v).wrap(r)
}
func (r redirectRes) GetStatus() (int, bool) {
if r.res == nil {
return 0, false
@@ -107,6 +155,13 @@ func (r redirectRes) GetStatus() (int, bool) {
return r.res.GetStatus()
}
func (r redirectRes) getHeaders() [][2]string {
if r.res != nil {
return r.res.getHeaders()
}
return nil
}
func (r redirectRes) GetRedirect() (code redirect.Code, to string, ok bool) {
return r.code, r.to, true
}
+13
View File
@@ -10,6 +10,18 @@ import (
type (
Response interface {
Status(int) Response
HXPushURL(string) Response
HXLocation(string) Response
HXPushUrl(string) Response
HXRedirect(string) Response
HXRefresh(string) Response
HXReplaceUrl(string) Response
HXReswap(string) Response
HXRetarget(string) Response
HXReselect(string) Response
HXTrigger(string) Response
HXTriggerAfterSettle(string) Response
HXTriggerAfterSwap(string) Response
Redirect(code redirect.Code, to string) Response
Body(io.ReadCloser) Response
HTML([]byte) Response
@@ -21,6 +33,7 @@ type (
GetRedirect() (code redirect.Code, to string, ok bool)
getCookies() []http.Cookie
getContentType() (contentType string, ok bool)
getHeaders() [][2]string
wrap(Response) Response
}
+55
View File
@@ -76,10 +76,65 @@ func (s statusRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(s)
}
func (s statusRes) HXPushURL(v string) Response {
return HXPushURL(v).wrap(s)
}
func (s statusRes) HXLocation(v string) Response {
return HXLocation(v).wrap(s)
}
func (s statusRes) HXPushUrl(v string) Response {
return HXPushUrl(v).wrap(s)
}
func (s statusRes) HXRedirect(v string) Response {
return HXRedirect(v).wrap(s)
}
func (s statusRes) HXRefresh(v string) Response {
return HXRefresh(v).wrap(s)
}
func (s statusRes) HXReplaceUrl(v string) Response {
return HXReplaceUrl(v).wrap(s)
}
func (s statusRes) HXReswap(v string) Response {
return HXReswap(v).wrap(s)
}
func (s statusRes) HXRetarget(v string) Response {
return HXRetarget(v).wrap(s)
}
func (s statusRes) HXReselect(v string) Response {
return HXReselect(v).wrap(s)
}
func (s statusRes) HXTrigger(v string) Response {
return HXTrigger(v).wrap(s)
}
func (s statusRes) HXTriggerAfterSettle(v string) Response {
return HXTriggerAfterSettle(v).wrap(s)
}
func (s statusRes) HXTriggerAfterSwap(v string) Response {
return HXTriggerAfterSwap(v).wrap(s)
}
func (s statusRes) GetStatus() (int, bool) {
return s.code, true
}
func (s statusRes) getHeaders() [][2]string {
if s.res != nil {
return s.res.getHeaders()
}
return nil
}
func (s statusRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if s.res == nil {
return 0, "", false
+5 -1
View File
@@ -72,12 +72,16 @@ func writeResponse(c *gin.Context, res Response) {
// w.Header() must be set before ResponseWriter.WriteHeader is called
// or redirect is attempted
hdrs := w.Header()
for _, hdr := range res.getHeaders() {
hdrs.Add(hdr[0], hdr[1])
}
for _, ck := range res.getCookies() {
hdrs.Add("Set-Cookie", ck.String())
}
if ct, ok := res.getContentType(); ok {
hdrs.Add("Content-Type", ct)
hdrs.Set("Content-Type", ct)
}
if code, to, ok := res.GetRedirect(); ok {
+37 -2
View File
@@ -93,10 +93,19 @@ func Routes(
etsy: etsy,
}
r.GET("", response.Handler(s.serveTemplate))
r.GET("", response.Handler(s.redirectToAccountsIfLoggedInWithAnAccount), response.Handler(s.serveTemplate))
r.GET("/*rest", authenticate, response.Handler(s.serveTemplate))
}
func (s *webpageRouter) redirectToAccountsIfLoggedInWithAnAccount(c *gin.Context) (response.Response, error) {
id := auth.GetIdentity(c)
if id.Account == nil || id.Account.AccountID == 0 {
return nil, nil
}
return response.TemporaryRedirect(fmt.Sprintf("%s/accounts/%d", s.uiPath, id.Account.AccountID)), nil
}
// GET /
// compiles the page template or component template matching the url
func (s *webpageRouter) serveTemplate(c *gin.Context) (response.Response, error) {
@@ -221,7 +230,33 @@ func authorizeByMatchingAccountID(r *http.Request, acctIDPathPosition int) error
id := auth.GetIdentity(r.Context())
if id.Account == nil || id.Account.AccountID != acctID {
return response.Unauthorized().
Msgf("user does not have access to account %d", acctID)
HTML([]byte(fmt.Sprintf(`
<section class="text-center">
<header class="m-[1em]">
<h2>
Access Not Granted
</h2>
<a href="/ui%s" class="block m-[1em]">
<code>
/ui%s
</code>
</a>
</header>
<div class="m-[2em]">
<p class="italic font-bold">
Sorry, you don't have access to the given page.
</p>
</div>
</section>
<section class="text-center">
<a href="/">
Return to app
</a>
</section>
`, r.URL, r.URL)))
}
return nil