A website inspired by Last.fm that will keep track of your listening statistics
lastfm music statistics
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

undo auth and inertia

oscar345 46d010fe 8f6351f6

+8 -1454
+1 -1
cmd/bridge/main.go
··· 36 36 return errors.New("path is required") 37 37 } 38 38 39 - router := router.New(services.ArtistService{}, services.UserService{}, services.AuthenticationService{}, nil, nil, nil, &config.Config{}) 39 + router := router.New(services.ArtistService{}, services.UserService{}, nil, &config.Config{}) 40 40 bridge.CreateRoutes(router.Router(), c.path) 41 41 42 42 return nil
-53
cmd/seeduser/main.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "database/sql" 6 - "flag" 7 - "log" 8 - 9 - "github.com/oscar345/keeptrack/internal/config" 10 - "github.com/oscar345/keeptrack/internal/models" 11 - "github.com/oscar345/keeptrack/internal/repo/db" 12 - authentication "github.com/oscar345/keeptrack/pkg/authentication" 13 - "github.com/oscar345/keeptrack/pkg/database" 14 - _ "modernc.org/sqlite" 15 - ) 16 - 17 - func main() { 18 - var ( 19 - email string 20 - password string 21 - isAdmin bool // currently not used 22 - ) 23 - 24 - cfg := config.Load() 25 - conn := database.Open("sqlite", cfg.AppDatabase.Path, func(d *sql.DB) {}) 26 - 27 - flag.StringVar(&email, "email", "", "Email address") 28 - flag.StringVar(&password, "password", "", "Password") 29 - flag.BoolVar(&isAdmin, "admin", false, "Is admin") 30 - flag.Parse() 31 - 32 - if email == "" || password == "" { 33 - log.Fatalln("Email and password are required") 34 - } 35 - 36 - password, err := authentication.HashPassword(password) 37 - if err != nil { 38 - log.Panicln(err) 39 - } 40 - 41 - user := models.User{ 42 - Email: email, 43 - Password: password, 44 - } 45 - 46 - userRepo := db.NewUserRepoDB(conn) 47 - userID, err := userRepo.Create(context.Background(), user) 48 - if err != nil { 49 - log.Panicln(err) 50 - } 51 - 52 - log.Println("User created with ID:", userID) 53 - }
-45
internal/repo/db/authentication_repo.go
··· 1 - package db 2 - 3 - import ( 4 - "database/sql" 5 - 6 - "github.com/oscar345/keeptrack/pkg/authentication" 7 - ) 8 - 9 - var _ authentication.Repo = (*AuthenticationRepoDB)(nil) 10 - 11 - type AuthenticationRepoDB struct { 12 - db *sql.DB 13 - } 14 - 15 - func NewAuthenticationRepoDB(db *sql.DB) *AuthenticationRepoDB { 16 - return &AuthenticationRepoDB{db: db} 17 - } 18 - 19 - func (repo *AuthenticationRepoDB) GetUserByEmail(email string) (authentication.User, error) { 20 - return authentication.User{}, nil 21 - } 22 - func (repo *AuthenticationRepoDB) GetUserByID(id int) (authentication.User, error) { 23 - return authentication.User{}, nil 24 - } 25 - func (repo *AuthenticationRepoDB) GetUserByTokenHash(hash string) (authentication.User, authentication.Token, error) { 26 - return authentication.User{}, authentication.Token{}, nil 27 - } 28 - func (repo *AuthenticationRepoDB) CreateUser(user authentication.User) (int, error) { 29 - return 0, nil 30 - } 31 - func (repo *AuthenticationRepoDB) UpdateUser(user authentication.User) error { 32 - return nil 33 - } 34 - func (repo *AuthenticationRepoDB) GetTokenByHash(hash string) (authentication.Token, error) { 35 - return authentication.Token{}, nil 36 - } 37 - func (repo *AuthenticationRepoDB) ListTokenByUserID(userID int) ([]authentication.Token, error) { 38 - return []authentication.Token{}, nil 39 - } 40 - func (repo *AuthenticationRepoDB) CreateToken(token authentication.Token) error { 41 - return nil 42 - } 43 - func (repo *AuthenticationRepoDB) DeleteToken(hash string) error { 44 - return nil 45 - }
-38
internal/server/inertia.go
··· 1 - package server 2 - 3 - import ( 4 - "html/template" 5 - "log" 6 - 7 - "github.com/gorilla/sessions" 8 - "github.com/oscar345/keeptrack/internal/web/sessionstore" 9 - "github.com/oscar345/keeptrack/pkg/inertia" 10 - ) 11 - 12 - func setupInertia(store sessions.Store) *inertia.Inertia { 13 - tmpl, err := template.New("root").Parse(root) 14 - if err != nil { 15 - log.Fatal(err) 16 - } 17 - return inertia.New(tmpl, sessionstore.NewInertia(store)) 18 - } 19 - 20 - const root = /*html*/ ` 21 - <!DOCTYPE html> 22 - <html lang="en"> 23 - <head> 24 - <meta charset="UTF-8"> 25 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 26 - <meta name="referrer" content="same-origin"> 27 - <title>Document</title> 28 - <script src="/assets/app.js" defer type="module"></script> 29 - <link rel="stylesheet" href="/assets/app.css"> 30 - <link rel="stylesheet" href="/assets/styles.css"> 31 - <script type="application/json" data-page="app"> 32 - {{ .Data }} 33 - </script> 34 - </head> 35 - <body id="app"> 36 - </body> 37 - </html> 38 - `
+3 -9
internal/server/server.go
··· 13 13 "github.com/oscar345/keeptrack/internal/repo/db" 14 14 "github.com/oscar345/keeptrack/internal/services" 15 15 "github.com/oscar345/keeptrack/internal/web/router" 16 - "github.com/oscar345/keeptrack/pkg/authentication" 17 16 "github.com/oscar345/keeptrack/pkg/database" 18 17 storagesvc "github.com/oscar345/keeptrack/pkg/storage" 19 18 "github.com/oscar345/keeptrack/pkg/utilities" ··· 49 48 }) 50 49 defer statisticsDB.Close() 51 50 52 - auth := authentication.New(db.NewAuthenticationRepoDB(generalDB)) 53 51 store := NewSessionStore(s.config) 54 - services := s.services(generalDB, statisticsDB, auth) 55 - 56 - inertia := setupInertia(store) 52 + services := s.services(generalDB, statisticsDB) 57 53 58 54 router := router. 59 - New(services.Artist, services.User, services.Auth, inertia, auth, store, s.config). 55 + New(services.Artist, services.User, store, s.config). 60 56 Router() 61 57 62 58 log.Printf("Listening on http://%s\n", s.address) ··· 67 63 type Services struct { 68 64 Artist services.ArtistService 69 65 User services.UserService 70 - Auth services.AuthenticationService 71 66 } 72 67 73 - func (s *Server) services(generalDB *sql.DB, statisticsDB *sql.DB, auth *authentication.Authenticator) Services { 68 + func (s *Server) services(generalDB *sql.DB, statisticsDB *sql.DB) Services { 74 69 artistRepo := db.NewArtistRepoDB(generalDB) 75 70 artistScrobbleRepo := db.NewArtistScrobbleRepoDB(statisticsDB) 76 71 artistImageFetcher := image.NewArtistImageFetcherFanArtTV(s.config.Services.FanartTV.APIKey) ··· 91 86 User: services.NewUserService( 92 87 userRepo, userFollowRepo, artistRepo, artistScrobbleRepo, mediaProvider, 93 88 ), 94 - Auth: services.NewAuthenticationService(userRepo, auth), 95 89 } 96 90 } 97 91
-41
internal/services/authentication.go
··· 1 - package services 2 - 3 - import ( 4 - "context" 5 - 6 - "github.com/oscar345/keeptrack/internal/models" 7 - "github.com/oscar345/keeptrack/internal/repo" 8 - "github.com/oscar345/keeptrack/pkg/authentication" 9 - ) 10 - 11 - type AuthenticationService struct { 12 - userRepo repo.UserRepo 13 - authenticator *authentication.Authenticator 14 - } 15 - 16 - func NewAuthenticationService(userRepo repo.UserRepo, authenticator *authentication.Authenticator) AuthenticationService { 17 - return AuthenticationService{ 18 - userRepo: userRepo, 19 - authenticator: authenticator, 20 - } 21 - } 22 - 23 - func (as *AuthenticationService) Login(ctx context.Context, email, password string) (authentication.User, error) { 24 - return as.authenticator.GetUserByEmailAndPassword(email, password) 25 - } 26 - 27 - func (as *AuthenticationService) Register(ctx context.Context, user models.User) (models.User, error) { 28 - hashedPassword, err := authentication.HashPassword(user.Password) 29 - if err != nil { 30 - return models.User{}, err 31 - } 32 - 33 - user.Password = hashedPassword 34 - userID, err := as.userRepo.Create(ctx, user) 35 - if err != nil { 36 - return models.User{}, err 37 - } 38 - 39 - user.ID = userID 40 - return user, nil 41 - }
+3 -10
internal/web/handlers/index.go
··· 2 2 3 3 import ( 4 4 "net/http" 5 - 6 - "github.com/oscar345/keeptrack/pkg/inertia" 7 5 ) 8 6 9 7 type IndexHandler struct { 10 - inertia *inertia.Inertia 11 8 } 12 9 13 - func NewIndexHandler(inertia *inertia.Inertia) IndexHandler { 14 - return IndexHandler{ 15 - inertia: inertia, 16 - } 10 + func NewIndexHandler() IndexHandler { 11 + return IndexHandler{} 17 12 } 18 13 19 14 func (h *IndexHandler) Index(w http.ResponseWriter, r *http.Request) { 20 - h.inertia.Render(w, r, "Index", inertia.Props{ 21 - "title": inertia.Always("Welcome"), 22 - }) 15 + 23 16 } 24 17 25 18 // func (handler *IndexHandler) Index(w http.ResponseWriter, r *http.Request) {
-15
internal/web/middleware/middleware.go
··· 1 1 package middleware 2 - 3 - import ( 4 - "net/http" 5 - 6 - "github.com/gorilla/csrf" 7 - "github.com/oscar345/keeptrack/pkg/inertia" 8 - ) 9 - 10 - func InertiaCSRFToken(next http.Handler) http.Handler { 11 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 - token := csrf.Token(r) 13 - inertia.SetProp(r, "csrf_token", inertia.Always(token)) 14 - next.ServeHTTP(w, r) 15 - }) 16 - }
+1 -167
internal/web/router/router.go
··· 1 1 package router 2 2 3 3 import ( 4 - "encoding/json" 5 - "fmt" 6 4 "net/http" 7 5 8 - "github.com/ggicci/httpin" 9 6 "github.com/go-chi/chi/v5" 10 7 chimiddleware "github.com/go-chi/chi/v5/middleware" 11 - "github.com/gorilla/csrf" 12 8 "github.com/gorilla/sessions" 13 9 "github.com/oscar345/keeptrack/internal/config" 14 - "github.com/oscar345/keeptrack/internal/filters" 15 - "github.com/oscar345/keeptrack/internal/models" 16 10 "github.com/oscar345/keeptrack/internal/services" 17 11 "github.com/oscar345/keeptrack/internal/web/handlers" 18 - "github.com/oscar345/keeptrack/internal/web/middleware" 19 - "github.com/oscar345/keeptrack/internal/web/requests" 20 - "github.com/oscar345/keeptrack/internal/web/responses" 21 - authentication "github.com/oscar345/keeptrack/pkg/authentication" 22 - "github.com/oscar345/keeptrack/pkg/enum" 23 - "github.com/oscar345/keeptrack/pkg/inertia" 24 - "github.com/oscar345/keeptrack/pkg/pagination" 25 - "github.com/oscar345/keeptrack/pkg/utilities" 26 - "github.com/oscar345/keeptrack/pkg/validation" 27 12 "github.com/oscar345/keeptrack/private" 28 13 ) 29 14 30 15 type Server struct { 31 16 artistService services.ArtistService 32 17 userService services.UserService 33 - authService services.AuthenticationService 34 - inertia *inertia.Inertia 35 - authProvider *authentication.Provider 36 18 config *config.Config 37 19 store sessions.Store 38 20 } ··· 40 22 func New( 41 23 artistService services.ArtistService, 42 24 userService services.UserService, 43 - authService services.AuthenticationService, 44 - inertia *inertia.Inertia, 45 - authProvider *authentication.Provider, 46 25 store sessions.Store, 47 26 config *config.Config, 48 27 ) *Server { 49 28 return &Server{ 50 29 artistService: artistService, 51 30 userService: userService, 52 - authService: authService, 53 - inertia: inertia, 54 - authProvider: authProvider, 55 31 store: store, 56 32 config: config, 57 33 } ··· 71 47 chimiddleware.Logger, 72 48 chimiddleware.RequestID, 73 49 chimiddleware.CleanPath, 74 - csrf.Protect( 75 - utilities.DeriveKey(s.config.Server.SecretKey, "csrf_token"), 76 - csrf.Secure(s.config.Environment == config.Production), 77 - csrf.TrustedOrigins([]string{"127.0.0.1:3000", "localhost:3000"}), 78 - ), 79 - s.inertia.Middleware, 80 - middleware.InertiaCSRFToken, 81 50 ) 82 51 83 52 r.Handle("/assets*", assetsFileSystemHandler(s.config)) ··· 89 58 } 90 59 91 60 func (s *Server) index() func(chi.Router) { 92 - handler := handlers.NewIndexHandler(s.inertia) 61 + _ = handlers.NewIndexHandler() 93 62 94 63 return func(r chi.Router) { 95 64 r.Get("/", func(w http.ResponseWriter, r *http.Request) { 96 - s.inertia.Render(w, r, "Index", inertia.Props{ 97 - "title": inertia.Always("Welcome"), 98 - }) 99 - }) 100 65 101 - r.With(s.authProvider.Middleware).Get("/test", func(w http.ResponseWriter, r *http.Request) { 102 - w.Write([]byte("Hello World")) 103 66 }) 104 67 105 - r.Get("/authentication/login", func(w http.ResponseWriter, r *http.Request) { 106 - s.inertia.Render(w, r, "authentication/Login", inertia.Props{}) 107 - }) 108 - 109 - r.Get("/authentication/register", func(w http.ResponseWriter, r *http.Request) { 110 - s.inertia.Render(w, r, "authentication/Register", inertia.Props{}) 111 - }) 112 - 113 - r.Get("/settings", func(w http.ResponseWriter, r *http.Request) { 114 - s.inertia.Render(w, r, "settings/Index", inertia.Props{}) 115 - }) 116 - 117 - r.Get("/settings/services", func(w http.ResponseWriter, r *http.Request) { 118 - s.inertia.Render(w, r, "settings/Services", inertia.Props{}) 119 - }) 120 - 121 - r.Post("/authentication/register", func(w http.ResponseWriter, r *http.Request) { 122 - var form requests.RegisterForm 123 - if err := json.NewDecoder(r.Body).Decode(&form); err != nil { 124 - http.Error(w, err.Error(), http.StatusBadRequest) 125 - return 126 - } 127 - 128 - if err := form.Validate(); err != nil { 129 - s.inertia.SetErrors(w, r, err.(validation.Errors)) 130 - http.Redirect(w, r, "/authentication/register", 302) 131 - return 132 - } 133 - 134 - user, err := s.authService.Register(r.Context(), models.User{ 135 - Email: form.Email, 136 - Password: form.Password, 137 - }) 138 - 139 - if err != nil { 140 - http.Error(w, err.Error(), http.StatusBadRequest) 141 - return 142 - } 143 - 144 - if err := s.authProvider.CreateSession(w, r, user.ID); err != nil { 145 - http.Error(w, err.Error(), http.StatusInternalServerError) 146 - return 147 - } 148 - 149 - http.Redirect(w, r, "/", 302) 150 - }) 151 - 152 - r.Delete("/authentication/logout", func(w http.ResponseWriter, r *http.Request) { 153 - err := s.authProvider.DeleteSession(w, r) 154 - if err != nil { 155 - fmt.Println("Error deleting session: ", err) 156 - http.Error(w, err.Error(), http.StatusInternalServerError) 157 - return 158 - } 159 - http.Redirect(w, r, "/authentication/login", http.StatusSeeOther) 160 - }) 161 - 162 - r.Post("/authentication/login", func(w http.ResponseWriter, r *http.Request) { 163 - fmt.Println("login") 164 - var form requests.LoginForm 165 - if err := json.NewDecoder(r.Body).Decode(&form); err != nil { 166 - http.Error(w, err.Error(), http.StatusBadRequest) 167 - return 168 - } 169 - 170 - if err := form.Validate(); err != nil { 171 - s.inertia.SetErrors(w, r, err.(validation.Errors)) 172 - http.Redirect(w, r, "/authentication/login", 302) 173 - return 174 - } 175 - 176 - user, err := s.authService.Login(r.Context(), form.Email, form.Password) 177 - if err != nil { 178 - s.inertia.SetErrors(w, r, map[string][]string{ 179 - "email": []string{"Invalid credentials"}, 180 - }) 181 - http.Redirect(w, r, "/authentication/login", 302) 182 - return 183 - } 184 - 185 - if err := s.authProvider.CreateSession(w, r, user.ID); err != nil { 186 - http.Error(w, err.Error(), http.StatusInternalServerError) 187 - return 188 - } 189 - 190 - http.Redirect(w, r, "/", 302) 191 - }) 192 - 193 - r.Get("/library", func(w http.ResponseWriter, r *http.Request) { 194 - s.inertia.Render(w, r, "library/Index", inertia.Props{}) 195 - }) 196 - 197 - r.Get("/library/artists", func(w http.ResponseWriter, r *http.Request) { 198 - artists, page, err := s.userService.ListArtistsByCount(r.Context(), 1, filters.ArtistCount{}) 199 - 200 - if err != nil { 201 - http.Error(w, err.Error(), http.StatusInternalServerError) 202 - return 203 - } 204 - 205 - s.inertia.Render(w, r, "library/artists/Index", inertia.Props{ 206 - "artists": inertia.Always(responses.Paginate(page, enum.Map(artists, responses.NewArtistFromModel))), 207 - }) 208 - }) 209 - 210 - r.Get("/friends", func(w http.ResponseWriter, r *http.Request) { 211 - s.inertia.Render(w, r, "friends/Index", inertia.Props{ 212 - "following": inertia.Lazy(func() (any, error) { 213 - following, page, err := s.userService.ListFollowing(r.Context(), 1, pagination.Filter{}) 214 - if err != nil { 215 - return nil, err 216 - } 217 - return responses.Paginate(page, enum.Map(following, responses.NewUserFromModel)), nil 218 - }), 219 - "followers": inertia.Lazy(func() (any, error) { 220 - followers, page, err := s.userService.ListFollowers(r.Context(), 1, pagination.Filter{}) 221 - if err != nil { 222 - return nil, err 223 - } 224 - return responses.Paginate(page, enum.Map(followers, responses.NewUserFromModel)), nil 225 - }), 226 - }) 227 - }) 228 - 229 - r.Get("/mixtapes", func(w http.ResponseWriter, r *http.Request) { 230 - s.inertia.Render(w, r, "mixtapes/Index", inertia.Props{}) 231 - }) 232 - 233 - r.With(httpin.NewInput(requests.Count{})).Get("/artists/{id}", handler.Index) 234 68 } 235 69 }
-65
internal/web/sessionstore/authentication.go
··· 1 - package sessionstore 2 - 3 - import ( 4 - "encoding/gob" 5 - "errors" 6 - "fmt" 7 - "net/http" 8 - 9 - "github.com/gorilla/sessions" 10 - authentication "github.com/oscar345/keeptrack/pkg/authentication" 11 - ) 12 - 13 - type Authentication struct { 14 - store sessions.Store 15 - } 16 - 17 - var _ authentication.SessionStore = (*Authentication)(nil) 18 - 19 - func NewAuthentication(store sessions.Store) *Authentication { 20 - gob.Register(authentication.Session{}) 21 - 22 - return &Authentication{ 23 - store: store, 24 - } 25 - } 26 - 27 - func (s *Authentication) Set(w http.ResponseWriter, r *http.Request, key string, authSession authentication.Session) error { 28 - session, err := s.store.Get(r, key) 29 - if err != nil { 30 - session.Options.MaxAge = -1 31 - session.Save(r, w) 32 - 33 - if session, err = s.store.Get(r, key); err != nil { 34 - fmt.Println("Error getting session in setting:", err) 35 - return err 36 - } 37 - } 38 - session.Values["data"] = authSession 39 - return session.Save(r, w) 40 - } 41 - 42 - func (s *Authentication) Get(r *http.Request, key string) (authentication.Session, error) { 43 - session, err := s.store.Get(r, key) 44 - if err != nil { 45 - return authentication.Session{}, err 46 - } 47 - 48 - result, ok := session.Values["data"].(authentication.Session) 49 - if !ok { 50 - return authentication.Session{}, errors.New("invalid session data") 51 - } 52 - 53 - return result, nil 54 - } 55 - 56 - func (s *Authentication) Delete(w http.ResponseWriter, r *http.Request, key string) error { 57 - session, err := s.store.Get(r, key) 58 - if err != nil { 59 - return err 60 - } 61 - 62 - session.Options.MaxAge = -1 63 - session.Values["data"] = nil 64 - return session.Save(r, w) 65 - }
-56
internal/web/sessionstore/inertia.go
··· 1 - package sessionstore 2 - 3 - import ( 4 - "encoding/gob" 5 - "errors" 6 - "fmt" 7 - "net/http" 8 - 9 - "github.com/gorilla/sessions" 10 - "github.com/oscar345/keeptrack/pkg/inertia" 11 - ) 12 - 13 - var _ inertia.SessionStore = (*Inertia)(nil) 14 - 15 - type Inertia struct { 16 - store sessions.Store 17 - } 18 - 19 - func NewInertia(store sessions.Store) *Inertia { 20 - gob.Register(inertia.SessionData{}) 21 - 22 - return &Inertia{ 23 - store: store, 24 - } 25 - } 26 - 27 - func (is *Inertia) Set(w http.ResponseWriter, r *http.Request, key string, value inertia.SessionData) error { 28 - session, err := is.store.Get(r, key) 29 - if err != nil { 30 - session.Options.MaxAge = -1 31 - session.Save(r, w) 32 - 33 - if session, err = is.store.Get(r, key); err != nil { 34 - fmt.Println("Error getting session in setting:", err) 35 - return err 36 - } 37 - } 38 - session.Values["data"] = value 39 - return session.Save(r, w) 40 - } 41 - 42 - func (is *Inertia) Get(r *http.Request, key string) (inertia.SessionData, error) { 43 - session, err := is.store.Get(r, key) 44 - if err != nil { 45 - if session, err = is.store.New(r, key); err != nil { 46 - fmt.Println("Error getting session:", err) 47 - return inertia.SessionData{}, err 48 - } 49 - } 50 - data, ok := session.Values["data"].(inertia.SessionData) 51 - if !ok { 52 - return inertia.SessionData{}, errors.New("invalid session data type") 53 - } 54 - 55 - return data, nil 56 - }
-1
pkg/auth/authentication.go
··· 1 - package authentication
-49
pkg/auth/config/kratos.toml
··· 1 - #:schema https://raw.githubusercontent.com/ory/kratos/v1.3.0/.schemastore/config.schema.json 2 - 3 - dsn = "sqlite:///var/lib/sqlite/users.db?_fk=true&mode=rwc" 4 - 5 - [identity] 6 - default_schema_id = "user" 7 - 8 - [[identity.schemas]] 9 - id = "user" 10 - url = "file://path/to/identity.traits.schema.json" 11 - 12 - [selfservice] 13 - default_browser_return_url = "http://localhost:3000" 14 - 15 - [selfservice.methods] 16 - password.enabled = true 17 - 18 - [selfservice.flows] 19 - verification.enabled = false 20 - recovery.enabled = false 21 - 22 - [selfservice.flows.settings] 23 - ui_url = "http://localhost:3000/settings" 24 - lifespan = "15m" 25 - privileged_session_max_age = "15m" 26 - required_aal = "highest_available" 27 - 28 - [selfservice.flows.login] 29 - ui_url = "http://localhost:3000/login" 30 - lifespan = "10m" 31 - 32 - [selfservice.flows.registration] 33 - ui_url = "http://localhost:3000/registration" 34 - lifespan = "10m" 35 - after.password.hooks = [{ hook = "session" }] 36 - 37 - [selfservice.flows.logout] 38 - after.default_browser_return_url = "http://localhost:3000" 39 - 40 - [courier.smtp] 41 - connection_uri = "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true" 42 - 43 - [secrets] 44 - cookie = ["very-long-very-secret-value"] 45 - cipher = ["even-longer-even-more-secret-val"] 46 - 47 - [hashers] 48 - algorithm = "bcrypt" 49 - bcrypt.cost = 12
-25
pkg/auth/config/user.schema.json
··· 1 - { 2 - "$id": "https://example.com/example.json", 3 - "$schema": "http://json-schema.org/draft-07/schema#", 4 - "title": "Person", 5 - "type": "object", 6 - "properties": { 7 - "traits": { 8 - "type": "object", 9 - "properties": { 10 - "username": { 11 - "type": "string", 12 - "ory.sh/kratos": { 13 - "credentials": { 14 - "password": { 15 - "identifier": true 16 - } 17 - } 18 - } 19 - } 20 - }, 21 - "required": ["username"], 22 - "additionalProperties": false 23 - } 24 - } 25 - }
-3
pkg/auth/docker-compose.yml
··· 1 - services: 2 - kratos: 3 - image:
-222
pkg/authentication/authentication.go
··· 1 - package authentication 2 - 3 - import ( 4 - "log" 5 - "net/http" 6 - "time" 7 - 8 - "golang.org/x/crypto/bcrypt" 9 - ) 10 - 11 - type User struct { 12 - ID int 13 - Email string 14 - HashedPassword string 15 - ConfirmedAt time.Time 16 - } 17 - 18 - type Repo interface { 19 - GetUserByEmail(email string) (User, error) 20 - GetUserByID(id int) (User, error) 21 - GetUserByTokenHash(hash string) (User, Token, error) 22 - CreateUser(user User) (int, error) 23 - UpdateUser(user User) error 24 - 25 - GetTokenByHash(hash string) (Token, error) 26 - ListTokenByUserID(userID int) ([]Token, error) 27 - CreateToken(token Token) error 28 - DeleteToken(hash string) error 29 - } 30 - 31 - type Authenticator struct { 32 - repo Repo 33 - invalidHashPassword []byte 34 - store SessionStore 35 - } 36 - 37 - func GenerateHashPassword(password string) (string, error) { 38 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 39 - if err != nil { 40 - return "", err 41 - } 42 - return string(hashedPassword), nil 43 - } 44 - 45 - func New(repo Repo) *Authenticator { 46 - invalidHashPassword, err := GenerateHashPassword("invalid") 47 - if err != nil { 48 - log.Panicln(err) 49 - } 50 - 51 - return &Authenticator{ 52 - repo: repo, 53 - invalidHashPassword: []byte(invalidHashPassword), 54 - } 55 - } 56 - 57 - func (a *Authenticator) GetUserByEmailAndPassword(email, password string) (User, error) { 58 - user, err := a.repo.GetUserByEmail(email) 59 - 60 - if ok := a.IsPasswordCorrect(user.HashedPassword, password, err == nil); !ok { 61 - return User{}, ErrInvalidCredentials 62 - } 63 - 64 - return user, nil 65 - } 66 - 67 - func (a *Authenticator) IsPasswordCorrect(current string, input string, isValidUser bool) bool { 68 - if !isValidUser { 69 - _ = bcrypt.CompareHashAndPassword(a.invalidHashPassword, []byte(input)) 70 - return false 71 - } 72 - err := bcrypt.CompareHashAndPassword([]byte(current), []byte(input)) 73 - return err == nil 74 - } 75 - 76 - func (a *Authenticator) UpdateUserEmail(userID int, hash string) error { 77 - user, err := a.repo.GetUserByID(userID) 78 - if err != nil { 79 - return err 80 - } 81 - 82 - raw, err := GenerateHashFromToken(hash) 83 - if err != nil { 84 - return err 85 - } 86 - 87 - token, err := a.repo.GetTokenByHash(string(raw)) 88 - if err != nil { 89 - return err 90 - } 91 - 92 - if err := token.VerifyByPurpose(CreateChangeTokenPurpose(user.Email)); err != nil { 93 - return err 94 - } 95 - 96 - user.Email = token.SentTo 97 - if err := a.repo.UpdateUser(user); err != nil { 98 - return err 99 - } 100 - 101 - return a.repo.DeleteToken(token.Hash) 102 - } 103 - 104 - func (a *Authenticator) UpdateUserPassword(userID int, current string, replacement string) error { 105 - user, err := a.repo.GetUserByID(userID) 106 - if err != nil { 107 - return err 108 - } 109 - 110 - if !a.IsPasswordCorrect(user.HashedPassword, current, true) { 111 - return ErrInvalidCredentials 112 - } 113 - 114 - hashed, err := bcrypt.GenerateFromPassword([]byte(replacement), bcrypt.DefaultCost) 115 - if err != nil { 116 - return err 117 - } 118 - 119 - user.HashedPassword = string(hashed) 120 - return a.repo.UpdateUser(user) 121 - } 122 - 123 - func (a *Authenticator) ConfirmUser(hash string) error { 124 - user, token, err := a.repo.GetUserByTokenHash(hash) 125 - if err != nil { 126 - return err 127 - } 128 - 129 - if err := token.VerifyByPurpose(TokenPurposeConfirm); err != nil { 130 - return err 131 - } 132 - 133 - user.ConfirmedAt = time.Now() 134 - if err := a.repo.UpdateUser(user); err != nil { 135 - return err 136 - } 137 - 138 - return a.repo.DeleteToken(token.Hash) 139 - } 140 - 141 - func (a *Authenticator) ResetUserPassword(hash string, replacement string) error { 142 - user, token, err := a.repo.GetUserByTokenHash(hash) 143 - if err != nil { 144 - return err 145 - } 146 - 147 - if err := token.VerifyByPurpose(TokenPurposePasswordReset); err != nil { 148 - return err 149 - } 150 - 151 - hashed, err := bcrypt.GenerateFromPassword([]byte(replacement), bcrypt.DefaultCost) 152 - if err != nil { 153 - return err 154 - } 155 - 156 - user.HashedPassword = string(hashed) 157 - if err := a.repo.UpdateUser(user); err != nil { 158 - return err 159 - } 160 - 161 - return a.repo.DeleteToken(token.Hash) 162 - } 163 - 164 - func (a *Authenticator) LogIn(w http.ResponseWriter, r *http.Request, user User) error { 165 - data, err := a.store.Get(r) 166 - if err != nil { 167 - return err 168 - } 169 - 170 - if err := RenewSession(w, r, a.store); err != nil { 171 - return err 172 - } 173 - 174 - token, raw, err := CreateSessionToken(user.ID) 175 - if err != nil { 176 - return err 177 - } 178 - 179 - if err := a.repo.CreateToken(token); err != nil { 180 - return err 181 - } 182 - 183 - if err := a.store.Set(w, r, SessionData{Token: raw}); err != nil { 184 - return err 185 - } 186 - 187 - returnTo := PathLogin 188 - if data.ReturnTo != "" { 189 - returnTo = data.ReturnTo 190 - } 191 - 192 - http.Redirect(w, r, returnTo, http.StatusFound) 193 - return nil 194 - } 195 - 196 - func (a *Authenticator) LogOut(w http.ResponseWriter, r *http.Request) error { 197 - data, err := a.store.Get(r) 198 - if err != nil { 199 - return err 200 - } 201 - 202 - if data.Token == "" { 203 - return nil 204 - } 205 - 206 - hash, err := GenerateHashFromToken(data.Token) 207 - if err != nil { 208 - return err 209 - } 210 - 211 - if err := a.repo.DeleteToken(string(hash)); err != nil { 212 - return err 213 - } 214 - 215 - if err := RenewSession(w, r, a.store); err != nil { 216 - _ = a.store.Delete(w, r) 217 - return err 218 - } 219 - 220 - http.Redirect(w, r, PathSignedOut, http.StatusFound) 221 - return nil 222 - }
-10
pkg/authentication/errors.go
··· 1 - package authentication 2 - 3 - import "errors" 4 - 5 - var ( 6 - ErrSessionExpired = errors.New("session expired") 7 - ErrWrongPurpose = errors.New("wrong purpose") 8 - ErrInvalidCredentials = errors.New("invalid credentials") 9 - ErrNoUserInContext = errors.New("no user in context") 10 - )
-172
pkg/authentication/middleware.go
··· 1 - package authentication 2 - 3 - import ( 4 - "context" 5 - "net/http" 6 - 7 - "github.com/gorilla/sessions" 8 - ) 9 - 10 - type SessionData struct { 11 - Token string 12 - ReturnTo string 13 - } 14 - 15 - const CookieNameSessionData = "_app_session_data" 16 - 17 - type SessionStore interface { 18 - Get(r *http.Request) (SessionData, error) 19 - Set(w http.ResponseWriter, r *http.Request, data SessionData) error 20 - Delete(w http.ResponseWriter, r *http.Request) error 21 - } 22 - 23 - type GorillaSessionStore struct { 24 - store sessions.Store 25 - options sessions.Options 26 - } 27 - 28 - func NewGorillaSessionStore(store sessions.Store, options sessions.Options) *GorillaSessionStore { 29 - return &GorillaSessionStore{ 30 - store: store, 31 - options: options, 32 - } 33 - } 34 - 35 - func (s *GorillaSessionStore) Get(r *http.Request) (SessionData, error) { 36 - session, err := s.store.Get(r, CookieNameSessionData) 37 - if err != nil { 38 - return SessionData{}, err 39 - } 40 - 41 - token, ok := session.Values["data"].(SessionData) 42 - if !ok { 43 - return SessionData{}, nil 44 - } 45 - 46 - return token, nil 47 - } 48 - 49 - func (s *GorillaSessionStore) Set(w http.ResponseWriter, r *http.Request, data SessionData) error { 50 - session, err := s.store.New(r, CookieNameSessionData) 51 - if err != nil { 52 - return err 53 - } 54 - 55 - session.Values["data"] = data 56 - session.Options = &s.options 57 - 58 - return session.Save(r, w) 59 - } 60 - 61 - func (s *GorillaSessionStore) Delete(w http.ResponseWriter, r *http.Request) error { 62 - session, err := s.store.Get(r, CookieNameSessionData) 63 - if err != nil { 64 - return err 65 - } 66 - 67 - session.Options.MaxAge = -1 68 - 69 - return session.Save(r, w) 70 - } 71 - 72 - func RenewSession(w http.ResponseWriter, r *http.Request, store SessionStore) error { 73 - if err := store.Delete(w, r); err != nil { 74 - return err 75 - } 76 - 77 - if err := store.Set(w, r, SessionData{}); err != nil { 78 - _ = store.Delete(w, r) 79 - return err 80 - } 81 - 82 - return nil 83 - } 84 - 85 - type Middleware struct { 86 - repo Repo 87 - store SessionStore 88 - } 89 - 90 - func NewMiddleware(repo Repo, store SessionStore) *Middleware { 91 - return &Middleware{ 92 - repo: repo, 93 - store: store, 94 - } 95 - } 96 - 97 - func (m *Middleware) FetchUser(next http.Handler) http.Handler { 98 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 99 - data, err := m.store.Get(r) 100 - if err != nil { 101 - next.ServeHTTP(w, r) 102 - return 103 - } 104 - 105 - if data.Token == "" { 106 - next.ServeHTTP(w, r) 107 - return 108 - } 109 - 110 - hash, err := GenerateHashFromToken(data.Token) 111 - if err != nil { 112 - next.ServeHTTP(w, r) 113 - return 114 - } 115 - 116 - user, token, err := m.repo.GetUserByTokenHash(string(hash)) 117 - if err != nil { 118 - next.ServeHTTP(w, r) 119 - return 120 - } 121 - 122 - if err := token.VerifyByPurpose(TokenPurposeSession); err != nil { 123 - next.ServeHTTP(w, r) 124 - return 125 - } 126 - 127 - r = r.WithContext(context.WithValue(r.Context(), ContextKeyUser, user)) 128 - 129 - next.ServeHTTP(w, r) 130 - }) 131 - } 132 - 133 - func (m *Middleware) RequireUser(next http.Handler) http.Handler { 134 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 135 - _, err := GetUserFromRequest(r) 136 - if err != nil { 137 - _ = m.store.Set(w, r, SessionData{ReturnTo: r.URL.Path}) 138 - http.Redirect(w, r, PathLogin, http.StatusFound) 139 - return 140 - } 141 - 142 - next.ServeHTTP(w, r) 143 - }) 144 - } 145 - 146 - func (m *Middleware) RequireNoUser(next http.Handler) http.Handler { 147 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 148 - _, err := GetUserFromRequest(r) 149 - if err == nil { 150 - http.Redirect(w, r, PathSignedIn, http.StatusFound) 151 - return 152 - } 153 - 154 - next.ServeHTTP(w, r) 155 - }) 156 - } 157 - 158 - const ContextKeyUser = "authentication-user" 159 - 160 - func GetUserFromRequest(r *http.Request) (User, error) { 161 - val := r.Context().Value(ContextKeyUser) 162 - if val == nil { 163 - return User{}, ErrNoUserInContext 164 - } 165 - 166 - user, ok := val.(User) 167 - if !ok { 168 - return User{}, ErrNoUserInContext 169 - } 170 - 171 - return user, nil 172 - }
-13
pkg/authentication/settings.go
··· 1 - package authentication 2 - 3 - import "time" 4 - 5 - const ( 6 - SessionValidityDuration = 7 * 24 * time.Hour 7 - ) 8 - 9 - const ( 10 - PathLogin = "/login" 11 - PathSignedIn = "/" 12 - PathSignedOut = "/" 13 - )
-84
pkg/authentication/token.go
··· 1 - package authentication 2 - 3 - import ( 4 - "crypto/rand" 5 - "crypto/sha256" 6 - "crypto/subtle" 7 - "encoding/base64" 8 - "time" 9 - ) 10 - 11 - type Token struct { 12 - Hash string 13 - UserID int 14 - Purpose TokenPurpose 15 - ExpiresAt time.Time 16 - SentTo string 17 - } 18 - 19 - func (t *Token) VerifyExpired() error { 20 - if time.Now().After(t.ExpiresAt) { 21 - return ErrSessionExpired 22 - } 23 - return nil 24 - } 25 - 26 - // VerifyByPurpose verifies if the session is valid for the given purpose and not expired. 27 - func (t *Token) VerifyByPurpose(purpose TokenPurpose) error { 28 - if subtle.ConstantTimeCompare([]byte(t.Purpose), []byte(purpose)) != 1 { 29 - return ErrWrongPurpose 30 - } 31 - return t.VerifyExpired() 32 - } 33 - 34 - type TokenPurpose string 35 - 36 - const ( 37 - TokenPurposeSession TokenPurpose = "session" 38 - TokenPurposePasswordReset TokenPurpose = "password-reset" 39 - TokenPurposeConfirm TokenPurpose = "confirm" 40 - ) 41 - 42 - func CreateChangeTokenPurpose(value string) TokenPurpose { 43 - return TokenPurpose("change:" + value) 44 - } 45 - 46 - // Create a token model. The function will return that model, its raw token value and an error if 47 - // any. 48 - func CreateSessionToken(userID int) (Token, string, error) { 49 - raw, hash, err := CreateTokenHash() 50 - if err != nil { 51 - return Token{}, "", err 52 - } 53 - 54 - token := Token{ 55 - UserID: userID, 56 - Purpose: TokenPurposeSession, 57 - ExpiresAt: time.Now().Add(SessionValidityDuration), 58 - Hash: string(hash), 59 - } 60 - 61 - return token, raw, nil 62 - } 63 - 64 - // Generate a random token, and return its raw value (for the user), its hash and a possible error 65 - func CreateTokenHash() (string, []byte, error) { 66 - token := make([]byte, 32) 67 - _, err := rand.Read(token) 68 - if err != nil { 69 - return "", nil, err 70 - } 71 - hash := sha256.Sum256(token) 72 - return base64.StdEncoding.EncodeToString(token), hash[:], nil 73 - } 74 - 75 - func GenerateHashFromToken(token string) ([]byte, error) { 76 - // Always perform the hash operation, even if decode fails 77 - raw, err := base64.StdEncoding.DecodeString(token) 78 - if err != nil { 79 - // Continue with dummy data to maintain constant time 80 - raw = make([]byte, 32) 81 - } 82 - hash := sha256.Sum256(raw) 83 - return hash[:], nil 84 - }
-49
pkg/inertia/context.go
··· 1 - package inertia 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "net/http" 7 - ) 8 - 9 - type Context struct { 10 - props Props 11 - isInertiaRequest bool 12 - isPartialRequest bool 13 - version string 14 - url string 15 - location string 16 - partialOnlyProps []string 17 - partialExceptProps []string 18 - } 19 - 20 - func ContextFromRequest(r *http.Request) (Context, bool) { 21 - ctx, ok := r.Context().Value(ContextKey).(Context) 22 - fmt.Println(ctx) 23 - return ctx, ok 24 - } 25 - 26 - const SessionKey = "inertia-session-key" 27 - 28 - func (in *Inertia) Middleware(next http.Handler) http.Handler { 29 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 - data, err := in.Store.Get(r, SessionKey) 31 - 32 - if err != nil { 33 - fmt.Println(err) 34 - } 35 - 36 - ctx := Context{ 37 - props: Props{"errors": Default(data.Errors)}, 38 - isInertiaRequest: isInertiaRequest(r), 39 - isPartialRequest: isPartialRequest(r), 40 - version: getVersion(r), 41 - location: getLocation(r), 42 - url: r.URL.Path, 43 - partialOnlyProps: getPartialOnlyProps(r), 44 - partialExceptProps: getPartialExceptProps(r), 45 - } 46 - 47 - next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ContextKey, ctx))) 48 - }) 49 - }
-43
pkg/inertia/inertia.go
··· 1 - package inertia 2 - 3 - import ( 4 - "html/template" 5 - "net/http" 6 - ) 7 - 8 - type SessionData struct { 9 - Errors map[string][]string 10 - } 11 - 12 - type SessionStore interface { 13 - Set(w http.ResponseWriter, r *http.Request, key string, value SessionData) error 14 - Get(r *http.Request, key string) (SessionData, error) 15 - } 16 - 17 - type Inertia struct { 18 - Version string 19 - Template *template.Template 20 - Store SessionStore 21 - } 22 - 23 - func New(tmpl *template.Template, store SessionStore, options ...NewInertiaOption) *Inertia { 24 - i := &Inertia{ 25 - Template: tmpl, 26 - Version: "1", 27 - Store: store, 28 - } 29 - 30 - for _, option := range options { 31 - option(i) 32 - } 33 - 34 - return i 35 - } 36 - 37 - type NewInertiaOption func(*Inertia) 38 - 39 - func WithVersion(version string) NewInertiaOption { 40 - return func(i *Inertia) { 41 - i.Version = version 42 - } 43 - }
-9
pkg/inertia/page.go
··· 1 - package inertia 2 - 3 - type Page struct { 4 - Component string `json:"component"` 5 - Props map[string]any `json:"props"` 6 - URL string `json:"url"` 7 - Version string `json:"version"` 8 - DeferredProps map[string][]string `json:"deferredProps"` 9 - }
-150
pkg/inertia/prop.go
··· 1 - package inertia 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "log" 7 - "net/http" 8 - "slices" 9 - ) 10 - 11 - type Props map[string]Prop 12 - 13 - type Prop struct { 14 - Value func() (any, error) 15 - Lazy bool 16 - Optional bool 17 - Always bool 18 - Deferred bool 19 - DeferredKey string 20 - Default bool 21 - isError bool 22 - Scroll ScrollMetadata 23 - } 24 - 25 - type ScrollMetadata struct { 26 - Name string 27 - PreviousPage int 28 - NextPage int 29 - CurrentPage int 30 - } 31 - 32 - func (p *Prop) ShouldDefer() bool { 33 - return true 34 - } 35 - 36 - func (p *Prop) IsReturned(ctx Context, key string, only []string, except []string) bool { 37 - isFirstLoad := !ctx.isPartialRequest 38 - 39 - if isFirstLoad && p.Deferred { 40 - return false 41 - } 42 - 43 - if slices.Contains(except, key) { 44 - return false 45 - } 46 - 47 - if slices.Contains(only, key) { 48 - return true 49 - } 50 - 51 - if p.Always || p.Default { 52 - return true 53 - } 54 - 55 - if isFirstLoad && p.Lazy { 56 - return true 57 - } 58 - 59 - return false 60 - } 61 - 62 - const ContextKey = "INERTIA_KEY" 63 - 64 - type PropOption func(*Prop) 65 - 66 - // The value is always evaluated during a request, but can be left out of the response when 67 - // the request is a partial request. In other Inertia adapters this would be the same as just 68 - // passing the value. 69 - func Default(value any, opts ...PropOption) Prop { 70 - return Prop{Value: func() (any, error) { return value, nil }, Default: true} 71 - } 72 - 73 - // The value is evaluated and returned for the first request to a page. For all partial request 74 - // to that same page, the value is not evaluated and returned unless the property is specified 75 - // in the `X-Inertia-Partial-Data` header. In other Inertia adapters this would be the same as 76 - // passing a closure. 77 - func Lazy(valuefn func() (any, error), opts ...PropOption) Prop { 78 - return Prop{Value: valuefn, Lazy: true} 79 - } 80 - 81 - // The value is only evaluated during a request when the request is a partial request. The value 82 - // is never returned during the first request from a page. 83 - func Optional(valuefn func() (any, error), opts ...PropOption) Prop { 84 - return Prop{Value: valuefn, Optional: true} 85 - } 86 - 87 - // The value is always returned during a partial request, and the first request from a page. 88 - func Always(value any) Prop { 89 - return Prop{Value: func() (any, error) { return value, nil }, Always: true} 90 - } 91 - 92 - func Scroll(value any, current, previous, next int, opts ...PropOption) Prop { 93 - prop := Prop{ 94 - Value: func() (any, error) { return value, nil }, 95 - Default: true, 96 - Scroll: ScrollMetadata{ 97 - CurrentPage: current, 98 - NextPage: next, 99 - PreviousPage: previous, 100 - }} 101 - 102 - for _, opt := range opts { 103 - opt(&prop) 104 - } 105 - 106 - return prop 107 - } 108 - 109 - func WithDeferredKey(key string) PropOption { 110 - return func(p *Prop) { 111 - p.DeferredKey = key 112 - } 113 - } 114 - 115 - func WithPageName(name string) PropOption { 116 - return func(p *Prop) { 117 - p.Scroll.Name = name 118 - } 119 - } 120 - 121 - // Besides setting the props in the handler when rendering the page, you can also set props in the 122 - // middleware. This is useful when you want to set props that are available to all pages. One can 123 - // use the SetProp function to set props. The value will be stored in the request context and will 124 - // be retrieved when rendering the page. 125 - func SetProp(r *http.Request, key string, value Prop) { 126 - ctx, ok := r.Context().Value(ContextKey).(Context) 127 - 128 - fmt.Println(ctx.props) 129 - 130 - if !ok { 131 - log.Panicln("context not found") 132 - } 133 - 134 - ctx.props[key] = value 135 - 136 - r.WithContext(context.WithValue(r.Context(), ContextKey, ctx)) 137 - } 138 - 139 - func (in *Inertia) SetErrors(w http.ResponseWriter, r *http.Request, errors map[string][]string) { 140 - data, err := in.Store.Get(r, SessionKey) 141 - if err != nil { 142 - log.Println(err) 143 - } 144 - data.Errors = errors 145 - fmt.Println(data) 146 - if err := in.Store.Set(w, r, SessionKey, data); err != nil { 147 - log.Println(err) 148 - } 149 - SetProp(r, "errors", Default(errors)) 150 - }
-41
pkg/inertia/request.go
··· 1 - package inertia 2 - 3 - import ( 4 - "net/http" 5 - "strings" 6 - ) 7 - 8 - type InertiaHeaderKey string 9 - 10 - const ( 11 - InertiaPartialDataHeader InertiaHeaderKey = "X-Inertia-Partial-Data" 12 - InertiaPartialComponentHeader InertiaHeaderKey = "X-Inertia-Partial-Component" 13 - InertiaPartialExceptHeader InertiaHeaderKey = "X-Inertia-Partial-Except" 14 - InertiaVersionHeader InertiaHeaderKey = "X-Inertia-Version" 15 - InertiaLocationHeader InertiaHeaderKey = "X-Inertia-Location" 16 - InertiaHeader InertiaHeaderKey = "X-Inertia" 17 - ) 18 - 19 - func isInertiaRequest(r *http.Request) bool { 20 - return r.Header.Get(string(InertiaHeader)) == "true" 21 - } 22 - 23 - func getPartialOnlyProps(r *http.Request) []string { 24 - return strings.Split(r.Header.Get(string(InertiaPartialDataHeader)), ",") 25 - } 26 - 27 - func getPartialExceptProps(r *http.Request) []string { 28 - return strings.Split(r.Header.Get(string(InertiaPartialExceptHeader)), ",") 29 - } 30 - 31 - func isPartialRequest(r *http.Request) bool { 32 - return r.Header.Get(string(InertiaPartialComponentHeader)) != "" 33 - } 34 - 35 - func getVersion(r *http.Request) string { 36 - return r.Header.Get(string(InertiaVersionHeader)) 37 - } 38 - 39 - func getLocation(r *http.Request) string { 40 - return r.Header.Get(string(InertiaLocationHeader)) 41 - }
-82
pkg/inertia/response.go
··· 1 - package inertia 2 - 3 - import ( 4 - "encoding/json" 5 - "html/template" 6 - "log" 7 - "maps" 8 - "net/http" 9 - ) 10 - 11 - func (in *Inertia) Render(w http.ResponseWriter, r *http.Request, view string, props Props) { 12 - var ( 13 - properties = make(map[string]any) 14 - deferred = make(map[string][]string) 15 - errors = make(map[string][]string) 16 - ) 17 - 18 - // here 19 - ctx, ok := r.Context().Value(ContextKey).(Context) 20 - if !ok { 21 - log.Fatalln("Failed to get inertia context, did you forget to use inertia middleware?") 22 - } 23 - 24 - maps.Copy(ctx.props, props) 25 - props = ctx.props 26 - 27 - for key, prop := range props { 28 - if !prop.IsReturned(ctx, key, ctx.partialOnlyProps, ctx.partialExceptProps) { 29 - if prop.ShouldDefer() { 30 - deferredKey := "default" 31 - if prop.DeferredKey != "" { 32 - deferredKey = prop.DeferredKey 33 - } 34 - deferred[deferredKey] = append(deferred[deferredKey], key) 35 - } 36 - continue 37 - } 38 - 39 - value, err := prop.Value() 40 - if err != nil { 41 - errors[key] = append(errors[key], err.Error()) 42 - continue 43 - } 44 - properties[key] = value 45 - } 46 - 47 - page := Page{ 48 - Component: view, 49 - Props: properties, 50 - URL: ctx.url, 51 - Version: in.Version, 52 - DeferredProps: deferred, 53 - } 54 - 55 - if ctx.isInertiaRequest { 56 - renderJSON(w, page) 57 - } else { 58 - renderHTML(in, w, page) 59 - } 60 - } 61 - 62 - func renderHTML(in *Inertia, w http.ResponseWriter, page Page) error { 63 - data, err := json.Marshal(page) 64 - 65 - if err != nil { 66 - return err 67 - } 68 - 69 - w.Header().Set("Vary", "Accept") 70 - w.WriteHeader(http.StatusOK) 71 - 72 - return in.Template.Execute(w, map[string]any{"Data": template.JS(data)}) 73 - } 74 - 75 - func renderJSON(w http.ResponseWriter, page Page) error { 76 - w.Header().Set(string(InertiaHeader), "true") 77 - w.Header().Set("Vary", "Accept") 78 - w.Header().Set("Content-Type", "application/json") 79 - w.WriteHeader(http.StatusOK) 80 - 81 - return json.NewEncoder(w).Encode(page) 82 - }
-1
pkg/inertia/scroll.go
··· 1 - package inertia