diff --git a/backend/blog.db b/backend/blog.db index 2accc7a..cef8449 100644 Binary files a/backend/blog.db and b/backend/blog.db differ diff --git a/backend/internal/auth/controller.go b/backend/internal/auth/controller.go index 12eb08c..fe516f9 100644 --- a/backend/internal/auth/controller.go +++ b/backend/internal/auth/controller.go @@ -6,7 +6,6 @@ import ( "fmt" "log" "net/http" - "slices" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" @@ -14,6 +13,7 @@ import ( "git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/model" ) +// Signup handles user signup by decoding request body, hashing the password, and saving user data to the database. func (s *Service) Signup(w http.ResponseWriter, r *http.Request) { var err error var login Login @@ -50,6 +50,7 @@ func (s *Service) Signup(w http.ResponseWriter, r *http.Request) { } } +// Login handles user login by decoding request body, verifying credentials, and returning a JWT token. func (s *Service) Login(w http.ResponseWriter, r *http.Request) { var login Login var user model.User @@ -86,6 +87,7 @@ func (s *Service) Login(w http.ResponseWriter, r *http.Request) { w.Write(res) } +// Logout handles user logout by invalidating the JWT and saving it to the database. func (s *Service) Logout(w http.ResponseWriter, r *http.Request) { token, err := extractToken(r) if err != nil { @@ -109,28 +111,3 @@ func (s *Service) Logout(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } - -func (s *Service) Authenticated(next http.HandlerFunc, roles ...model.Role) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Our middleware logic goes here... - token, err := extractToken(r) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - claims, err := s.validateJWT(token) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - // if roles specified check if satisfied - if len(roles) > 0 && !slices.Contains(roles, claims.Role) { - w.WriteHeader(http.StatusForbidden) - return - } - r = writeToContext(r, &claims) - next(w, r) - }) -} diff --git a/backend/internal/auth/jwt.go b/backend/internal/auth/jwt.go index 162ee8e..2b688b5 100644 --- a/backend/internal/auth/jwt.go +++ b/backend/internal/auth/jwt.go @@ -3,7 +3,6 @@ package auth import ( "errors" "fmt" - "log" "net/http" "strings" "time" @@ -28,6 +27,7 @@ func (s *Service) createJWT(user *model.User) (token string, err error) { return jwt.NewWithClaims(jwt.SigningMethodHS512, claims).SignedString([]byte(s.cfg.Secret)) } +// validateJWT returns the token Claims and if token ist invalid ErrJWTInvalid func (s *Service) validateJWT(tokenString string) (claims Claims, err error) { _, err = jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (any, error) { // Don't forget to validate the alg is what you expect: @@ -40,12 +40,13 @@ func (s *Service) validateJWT(tokenString string) (claims Claims, err error) { if err != nil { return } - log.Println(claims) + if claims.ExpiresAt.Before(time.Now()) { err = ErrJWTInvalid return } + // check if user has logged out this token var invalidated bool err = s.db.Model(&model.InvalidJWT{}). Select("count(*) > 0"). @@ -60,6 +61,7 @@ func (s *Service) validateJWT(tokenString string) (claims Claims, err error) { return } +// extractToken extracts the Bearer token from the request func extractToken(r *http.Request) (token string, err error) { tokenHeader := r.Header.Get("Authorization") // Grab the token from the header diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go new file mode 100644 index 0000000..848710f --- /dev/null +++ b/backend/internal/auth/middleware.go @@ -0,0 +1,34 @@ +package auth + +import ( + "net/http" + "slices" + + "git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/model" +) + +// Authenticated: This function is a middleware that authenticates incoming HTTP requests using JWT tokens and role-based access control. +func (s *Service) Authenticated(next http.HandlerFunc, roles ...model.Role) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Our middleware logic goes here... + token, err := extractToken(r) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + claims, err := s.validateJWT(token) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // if roles specified check if satisfied + if len(roles) > 0 && !slices.Contains(roles, claims.Role) { + w.WriteHeader(http.StatusForbidden) + return + } + r = writeToContext(r, &claims) + next(w, r) + }) +} diff --git a/backend/internal/auth/resource.go b/backend/internal/auth/resource.go index c7698f0..f35b706 100644 --- a/backend/internal/auth/resource.go +++ b/backend/internal/auth/resource.go @@ -11,6 +11,7 @@ import ( "gorm.io/gorm/clause" ) +// Config defines a struct for configuration settings, often loaded from environment variables. type Config struct { Secret string `env:"SECRET"` ValidDuration time.Duration `env:"VALID_DURATION"` @@ -19,11 +20,13 @@ type Config struct { DefaultRole model.Role `env:"DEFAULT_ROLE"` } +// Service Represents a service with configuration and database connection. type Service struct { cfg *Config db *gorm.DB } +// New creates a new Service instance, initializing a default admin user and saving it to the database. func New(cfg *Config, db *gorm.DB) *Service { user := model.NewUser() var err error @@ -42,16 +45,20 @@ func New(cfg *Config, db *gorm.DB) *Service { } } +// Claims struct represents JWT claims, including role and user ID, extending the standard jwt.RegisteredClaims. type Claims struct { Role model.Role `json:"rl"` UserID uint `json:"uid"` jwt.RegisteredClaims } + +// Login struct represents user login credentials with a name and password. type Login struct { Name string `json:"name"` Password string `json:"Password"` } +// LoginResponse Represents the response from a login endpoint, containing a JWT token. type LoginResponse struct { Token string `json:"token"` } diff --git a/backend/internal/config/resource.go b/backend/internal/config/resource.go index a305253..d89f84b 100644 --- a/backend/internal/config/resource.go +++ b/backend/internal/config/resource.go @@ -6,6 +6,7 @@ import ( "git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/auth" ) +// Config holds configuration settings for the application, loaded from environment variables. type Config struct { Port int `env:"PORT"` Host string `env:"HOST"` @@ -13,6 +14,7 @@ type Config struct { Auth auth.Config `env:"AUTH"` } +// Default returns a default configuration with pre-defined values. func Default() *Config { return &Config{ Port: 8080, diff --git a/backend/internal/initialize/inject.go b/backend/internal/initialize/inject.go index e660ffd..62b499f 100644 --- a/backend/internal/initialize/inject.go +++ b/backend/internal/initialize/inject.go @@ -11,6 +11,7 @@ import ( "github.com/gorilla/mux" ) +// CreateMux creates and configures a mux router with authentication and post-related routes. func CreateMux(cfg *config.Config) (r *mux.Router) { db := model.Init() blg := posts.New(db) @@ -22,7 +23,7 @@ func CreateMux(cfg *config.Config) (r *mux.Router) { // auth r.HandleFunc("/login", auth.Login).Methods("POST") r.HandleFunc("/signup", auth.Signup).Methods("POST") - r.Handle("logout", auth.Authenticated(auth.Logout)).Methods("POST") + r.Handle("/logout", auth.Authenticated(auth.Logout)).Methods("DELETE") // Posts r.Handle("/posts", auth.Authenticated(blg.SavePost, model.RoleUser, model.RoleAdmin)).Methods("POST") diff --git a/backend/internal/model/auth.go b/backend/internal/model/auth.go index 3d2b624..8e5b7b3 100644 --- a/backend/internal/model/auth.go +++ b/backend/internal/model/auth.go @@ -14,11 +14,13 @@ const ( RoleGuest Role = "guest" ) +// InvalidJWT Represents a JWT that has expired or is otherwise invalid. type InvalidJWT struct { JWT string `gorm:"primarykey"` ValidUntil time.Time } +// User represents a user with an ID, UUID, name, role, and password. type User struct { ID uint `gorm:"primarykey" json:"-"` UUID uuid.UUID `gorm:"type:uuid" json:"uuid"` @@ -27,6 +29,7 @@ type User struct { Password []byte `json:"-"` } +// NewUser creates a new User struct with a generated UUID. func NewUser() User { return User{ UUID: uuid.New(), diff --git a/backend/internal/model/blog.go b/backend/internal/model/blog.go index 64a6c5b..c9d034e 100644 --- a/backend/internal/model/blog.go +++ b/backend/internal/model/blog.go @@ -4,6 +4,7 @@ import ( "gorm.io/gorm" ) +// Post represents a blog post with associated comments and user ID. type Post struct { gorm.Model ID uint `gorm:"primarykey" json:"id"` @@ -13,6 +14,8 @@ type Post struct { Comments []Comment UserID uint `gorm:"->;<-:create"` } + +// Comment represents a comment on a post, including its ID, post association, content, and creator. type Comment struct { ID uint PostID uint diff --git a/backend/internal/model/init.go b/backend/internal/model/init.go index a427a22..d6b3e16 100644 --- a/backend/internal/model/init.go +++ b/backend/internal/model/init.go @@ -7,6 +7,7 @@ import ( "gorm.io/gorm" ) +// Init initializes the database connection, auto-migrates models, and seeds a default post. func Init() *gorm.DB { db, err := gorm.Open(sqlite.Open("./blog.db")) if err != nil { diff --git a/backend/internal/posts/controller.go b/backend/internal/posts/controller.go index 2b7eb9c..361d118 100644 --- a/backend/internal/posts/controller.go +++ b/backend/internal/posts/controller.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/mux" ) +// SavePost handles saving a new post to the database after extracting user claims and decoding the request body. func (s Service) SavePost(w http.ResponseWriter, r *http.Request) { claims, ok := auth.ExtractClaims(r.Context()) if !ok { @@ -45,6 +46,7 @@ func (s Service) SavePost(w http.ResponseWriter, r *http.Request) { w.Write(res) } +// GetAllPosts retrieves all posts from the database, eager-loads comments, orders them by creation time, and returns them as JSON. func (s Service) GetAllPosts(w http.ResponseWriter, r *http.Request) { var posts []model.Post @@ -63,6 +65,7 @@ func (s Service) GetAllPosts(w http.ResponseWriter, r *http.Request) { w.Write(res) } +// DeletePost handles deleting a post from the database based on its ID and user authentication. func (s Service) DeletePost(w http.ResponseWriter, r *http.Request) { idStr, ok := mux.Vars(r)["postID"] if !ok { diff --git a/backend/internal/posts/resource.go b/backend/internal/posts/resource.go index 0580309..2596a8b 100644 --- a/backend/internal/posts/resource.go +++ b/backend/internal/posts/resource.go @@ -2,10 +2,12 @@ package posts import "gorm.io/gorm" +// Service Represents a service with a database connection. type Service struct { db *gorm.DB } +// Service New creates a new Service instance, initializing it with a GORM database connection. func New(db *gorm.DB) *Service { return &Service{db: db} } diff --git a/backend/internal/users/password.go b/backend/internal/users/password.go index 94af198..3f6cf9a 100644 --- a/backend/internal/users/password.go +++ b/backend/internal/users/password.go @@ -11,6 +11,7 @@ import ( "golang.org/x/crypto/bcrypt" ) +// ChangePassword handles changing a user's password by decoding a request, validating input, hashing the password, and updating the database. func (s Service) ChangePassword(w http.ResponseWriter, r *http.Request) { var err error var req Password diff --git a/backend/internal/users/resource.go b/backend/internal/users/resource.go index 23a2d43..b1d7b77 100644 --- a/backend/internal/users/resource.go +++ b/backend/internal/users/resource.go @@ -4,16 +4,19 @@ import ( "gorm.io/gorm" ) +// Service Represents a service with a database connection. type Service struct { db *gorm.DB } +// Service - Creates a new Service instance, initializing it with a GORM database connection. func New(db *gorm.DB) *Service { return &Service{ db: db, } } +// Password struct represents a user's password with a JSON tag. type Password struct { Password string `json:"password"` } diff --git a/backend/internal/users/roles.go b/backend/internal/users/roles.go index 5f74886..c188376 100644 --- a/backend/internal/users/roles.go +++ b/backend/internal/users/roles.go @@ -14,6 +14,7 @@ import ( "gorm.io/gorm" ) +// GetUsers retrieves all users from the database and returns them as a JSON response. func (s *Service) GetUsers(w http.ResponseWriter, r *http.Request) { var users []model.User @@ -32,6 +33,7 @@ func (s *Service) GetUsers(w http.ResponseWriter, r *http.Request) { w.Write(res) } +// SetUserRole handles updating a user's role based on a UUID from the request. func (s *Service) SetUserRole(w http.ResponseWriter, r *http.Request) { var role model.Role userUUIDstr, ok := mux.Vars(r)["userUUID"] @@ -63,6 +65,7 @@ func (s *Service) SetUserRole(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +// DeleteUser handles the deletion of a user from the database, enforcing authorization checks. func (s *Service) DeleteUser(w http.ResponseWriter, r *http.Request) { claims, ok := auth.ExtractClaims(r.Context()) if !ok { diff --git a/backend/pkg/cors/cors.go b/backend/pkg/cors/cors.go index ab611a8..2cd827f 100644 --- a/backend/pkg/cors/cors.go +++ b/backend/pkg/cors/cors.go @@ -2,6 +2,7 @@ package cors import "net/http" +// HandlerForOrigin returns a CORS middleware function that sets headers based on the requested origin. func HandlerForOrigin(origin string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -22,6 +23,7 @@ func HandlerForOrigin(origin string) func(http.Handler) http.Handler { } } +// DeafaultHandler: Returns a handler that allows requests from any origin by delegating to a handler that allows all origins. func DeafaultHandler(next http.Handler) http.Handler { return HandlerForOrigin("*")(next) }