diff --git a/cmd/main.go b/cmd/api/main.go similarity index 96% rename from cmd/main.go rename to cmd/api/main.go index bad9fe7..88bbb55 100644 --- a/cmd/main.go +++ b/cmd/api/main.go @@ -10,15 +10,15 @@ import ( "syscall" "time" - "nfeeder/internal/db" - "nfeeder/internal/web" + "nfeeder/db" + "nfeeder/web" "github.com/jackc/pgx/v5/pgxpool" ) func main() { - // @Logger + // INFO: Logger // ------------------------------------------------------------ var handler slog.Handler if os.Getenv("ENV") == "production" { @@ -31,7 +31,7 @@ func main() { ctx, ctxCancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer ctxCancel() - // @DB + // INFO:DB // ------------------------------------------------------------ connStr := os.Getenv("DATABASE_URL") if connStr == "" { @@ -67,7 +67,7 @@ func main() { // Create Store sqlc wrapper store := db.NewStore(pool) - // @Server + // INFO: Server // ------------------------------------------------------------ jwtSecret := os.Getenv("JWT_SECRET") if jwtSecret == "" { @@ -87,7 +87,7 @@ func main() { } }() - // @GracefulShutdown + // INFO: GracefulShutdown // ------------------------------------------------------------ <-ctx.Done() logger.Warn("shutdown signal received", "signal", "SIGTERM/Interrupt") diff --git a/cmd/app/main.go b/cmd/app/main.go new file mode 100644 index 0000000..c89bb93 --- /dev/null +++ b/cmd/app/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "os" + + tea "charm.land/bubbletea/v2" +) + +type model struct { + name string + sayHello bool +} + +func initialModel() model { + return model{ + name: "jason", + sayHello: false, + } +} + +func (m model) Init() tea.Cmd { + // any init commands like fetching data etc + // to populate model if need be + return nil +} + +// Update loop like a game +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + // Handle key presses + case tea.KeyMsg: + switch msg.String() { + // Quit the app + case "ctrl+c", "q": + return m, tea.Quit + + // Toggle the greeting + case "enter", " ": + m.sayHello = !m.sayHello + } + } + + return m, nil +} + +// View: Returns a string representing the UI. like a render method +func (m model) View() tea.View { + s := "nfeeder Dev Console\n" + s += "-------------------\n\n" + + if m.sayHello { + s += fmt.Sprintf("Hello, %s! Nice to see you.\n\n", m.name) + } else { + s += "Press ENTER to say hello.\n\n" + } + + s += "Press 'q' to quit.\n" + + return tea.NewView(s) +} + +func main() { + p := tea.NewProgram(initialModel()) + if _, err := p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } +} diff --git a/internal/db/db.go b/db/db.go similarity index 100% rename from internal/db/db.go rename to db/db.go diff --git a/internal/db/models.go b/db/models.go similarity index 100% rename from internal/db/models.go rename to db/models.go diff --git a/internal/db/querier.go b/db/querier.go similarity index 100% rename from internal/db/querier.go rename to db/querier.go diff --git a/internal/db/refresh_tokens.sql.go b/db/refresh_tokens.sql.go similarity index 100% rename from internal/db/refresh_tokens.sql.go rename to db/refresh_tokens.sql.go diff --git a/internal/db/store.go b/db/store.go similarity index 100% rename from internal/db/store.go rename to db/store.go diff --git a/internal/db/users.sql.go b/db/users.sql.go similarity index 100% rename from internal/db/users.sql.go rename to db/users.sql.go diff --git a/go.mod b/go.mod index d7eddb3..ee48487 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module nfeeder go 1.26.1 require ( + charm.land/bubbletea/v2 v2.0.6 github.com/go-chi/chi/v5 v5.2.5 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/jackc/pgx/v5 v5.9.1 @@ -10,9 +11,23 @@ require ( ) require ( + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect ) diff --git a/go.sum b/go.sum index 0d2e24a..ac41580 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,25 @@ +charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo= +charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac= +github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f h1:UytXHv0UxnsDFmL/7Z9Q5SBYPwSuRLXHbwx+6LycZ2w= +github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,17 +35,31 @@ github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/tests/hurl/auth_lifecycle.hurl b/tests/hurl/auth_lifecycle.hurl deleted file mode 100644 index 7c2b6ff..0000000 --- a/tests/hurl/auth_lifecycle.hurl +++ /dev/null @@ -1,56 +0,0 @@ -# 1. Register a new unique user -POST http://localhost:3333/register -[FormParams] -email: test_user@debian.org -password: supersecretpassword - -HTTP 200 -[Captures] -access_token: jsonpath "$.access_token" -refresh_token: jsonpath "$.refresh_token" - - -# 2. Access a protected route with the first token -GET http://localhost:3333/home -Authorization: Bearer {{access_token}} - -HTTP 200 -[Asserts] -jsonpath "$.status" == "authenticated" - - -# 3. Refresh the tokens -POST http://localhost:3333/refresh -Content-Type: application/json -{ - "refresh_token": "{{refresh_token}}" -} - -HTTP 200 -[Captures] -# Overwrite with the fresh tokens -next_access_token: jsonpath "$.access_token" -next_refresh_token: jsonpath "$.refresh_token" - -[Asserts] -# Now compare the two distinct variable names -variable "next_refresh_token" != "{{refresh_token}}" - -# 4. Access the protected route again with the NEW access token -GET http://localhost:3333/home -Authorization: Bearer {{next_access_token}} - -HTTP 200 -[Asserts] -jsonpath "$.status" == "authenticated" - -# Log out user to clean table of tokens etc -POST http://localhost:3333/logout -Content-Type: application/json -{ - "refresh_token": "{{next_refresh_token}}" -} - -HTTP 200 -[Asserts] -jsonpath "$.message" == "logout success" diff --git a/tests/hurl/bad_token.hurl b/tests/hurl/bad_token.hurl deleted file mode 100644 index b00eafd..0000000 --- a/tests/hurl/bad_token.hurl +++ /dev/null @@ -1,7 +0,0 @@ -# Check accessing protected route with an invalid token gives a 401 -GET http://localhost:3333/home -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30 - -HTTP 401 -[Asserts] -jsonpath "$.error" == "unauthorized" diff --git a/tests/hurl/login_logout.hurl b/tests/hurl/login_logout.hurl deleted file mode 100644 index d2d32d9..0000000 --- a/tests/hurl/login_logout.hurl +++ /dev/null @@ -1,28 +0,0 @@ -POST http://localhost:3333/login -[FormParams] -email: test_user@debian.org -password: supersecretpassword - -HTTP 200 -[Captures] -access_token: jsonpath "$.access_token" -refresh_token: jsonpath "$.refresh_token" - -# Check the logged in use can access the protected route -GET http://localhost:3333/home -Authorization: Bearer {{access_token}} - -HTTP 200 -[Asserts] -jsonpath "$.status" == "authenticated" - -# Log out user to clean table of tokens etc -POST http://localhost:3333/logout -Content-Type: application/json -{ - "refresh_token": "{{refresh_token}}" -} - -HTTP 200 -[Asserts] -jsonpath "$.message" == "logout success" diff --git a/tui/ui.go b/tui/ui.go new file mode 100644 index 0000000..ad72273 --- /dev/null +++ b/tui/ui.go @@ -0,0 +1,23 @@ +package tui + +import ( + // "nfeeder/tui/pages" +) + +type sessionState int + +const ( + LoginView sessionState = iota + FeedView + HelpView +) + +type MainModel struct { + state sessionState + width int + height int + + // Sub-models + loginPage pages.LoginModel + feedPage pages.FeedModel +} diff --git a/web/handlers_api.go b/web/handlers_api.go new file mode 100644 index 0000000..12b5c9d --- /dev/null +++ b/web/handlers_api.go @@ -0,0 +1,29 @@ +package web + +import ( + "encoding/json" + "log" + "net/http" +) + +// TODO CLEANUP: Test route for now +func (s *Server) handleHome() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 1. Grab the userID from the context (placed there by s.hasAuth) + userID, ok := userIDFromContext(r.Context()) + if !ok { + // Technically never happen if the middleware is working... + log.Printf("UNAUTHORIZED!") + http.Error(w, "User not found in context", http.StatusUnauthorized) + return + } + + // 2. Respond with something that proves it works + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "message": "Welcome to the nfeeder API!", + "user_id": userID, + "status": "authenticated", + }) + } +} diff --git a/internal/web/handlers_auth.go b/web/handlers_auth.go similarity index 99% rename from internal/web/handlers_auth.go rename to web/handlers_auth.go index d1ae4b3..ba0e7af 100644 --- a/internal/web/handlers_auth.go +++ b/web/handlers_auth.go @@ -8,7 +8,7 @@ import ( "encoding/json" "log" "net/http" - "nfeeder/internal/db" + "nfeeder/db" "strconv" "time" diff --git a/internal/web/middleware.go b/web/middleware.go similarity index 100% rename from internal/web/middleware.go rename to web/middleware.go diff --git a/internal/web/responses.go b/web/responses.go similarity index 100% rename from internal/web/responses.go rename to web/responses.go diff --git a/internal/web/routes.go b/web/routes.go similarity index 98% rename from internal/web/routes.go rename to web/routes.go index 2edf044..b21dbb1 100644 --- a/internal/web/routes.go +++ b/web/routes.go @@ -1,8 +1,6 @@ package web import ( - "net/http" - "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) diff --git a/internal/web/server.go b/web/server.go similarity index 90% rename from internal/web/server.go rename to web/server.go index 6efb17d..7bb71e0 100644 --- a/internal/web/server.go +++ b/web/server.go @@ -5,11 +5,13 @@ import ( "log/slog" "net/http" - "nfeeder/internal/db" + "nfeeder/db" ) -var ISSUER = "nfeeder-app" -var MAX_USER_SESSIONS = 3 +const ( + MAX_USER_SESSIONS = 3 + ISSUER = "nfeeder-app" +) type Server struct { httpServer *http.Server diff --git a/internal/web/utils.go b/web/utils.go similarity index 100% rename from internal/web/utils.go rename to web/utils.go