Update structure to domain driven architecture.
This commit is contained in:
@@ -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")
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user