Update structure to domain driven architecture.

This commit is contained in:
2026-05-08 08:14:25 +02:00
parent 4f3692abd9
commit 2993cc04ef
21 changed files with 185 additions and 103 deletions
+6 -6
View File
@@ -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")
+70
View File
@@ -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)
}
}
View File
+15
View File
@@ -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
)
+36
View File
@@ -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=
-56
View File
@@ -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"
-7
View File
@@ -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"
-28
View File
@@ -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"
+23
View File
@@ -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
}
+29
View File
@@ -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",
})
}
}
@@ -8,7 +8,7 @@ import (
"encoding/json"
"log"
"net/http"
"nfeeder/internal/db"
"nfeeder/db"
"strconv"
"time"
-2
View File
@@ -1,8 +1,6 @@
package web
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
+5 -3
View File
@@ -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