added comments
This commit is contained in:
BIN
backend/blog.db
BIN
backend/blog.db
Binary file not shown.
@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -14,6 +13,7 @@ import (
|
|||||||
"git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/model"
|
"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) {
|
func (s *Service) Signup(w http.ResponseWriter, r *http.Request) {
|
||||||
var err error
|
var err error
|
||||||
var login Login
|
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) {
|
func (s *Service) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
var login Login
|
var login Login
|
||||||
var user model.User
|
var user model.User
|
||||||
@ -86,6 +87,7 @@ func (s *Service) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(res)
|
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) {
|
func (s *Service) Logout(w http.ResponseWriter, r *http.Request) {
|
||||||
token, err := extractToken(r)
|
token, err := extractToken(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -109,28 +111,3 @@ func (s *Service) Logout(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -3,7 +3,6 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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))
|
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) {
|
func (s *Service) validateJWT(tokenString string) (claims Claims, err error) {
|
||||||
_, err = jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (any, error) {
|
_, err = jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (any, error) {
|
||||||
// Don't forget to validate the alg is what you expect:
|
// 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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Println(claims)
|
|
||||||
if claims.ExpiresAt.Before(time.Now()) {
|
if claims.ExpiresAt.Before(time.Now()) {
|
||||||
err = ErrJWTInvalid
|
err = ErrJWTInvalid
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if user has logged out this token
|
||||||
var invalidated bool
|
var invalidated bool
|
||||||
err = s.db.Model(&model.InvalidJWT{}).
|
err = s.db.Model(&model.InvalidJWT{}).
|
||||||
Select("count(*) > 0").
|
Select("count(*) > 0").
|
||||||
@ -60,6 +61,7 @@ func (s *Service) validateJWT(tokenString string) (claims Claims, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractToken extracts the Bearer token from the request
|
||||||
func extractToken(r *http.Request) (token string, err error) {
|
func extractToken(r *http.Request) (token string, err error) {
|
||||||
tokenHeader := r.Header.Get("Authorization") // Grab the token from the header
|
tokenHeader := r.Header.Get("Authorization") // Grab the token from the header
|
||||||
|
|
||||||
|
34
backend/internal/auth/middleware.go
Normal file
34
backend/internal/auth/middleware.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
@ -11,6 +11,7 @@ import (
|
|||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config defines a struct for configuration settings, often loaded from environment variables.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Secret string `env:"SECRET"`
|
Secret string `env:"SECRET"`
|
||||||
ValidDuration time.Duration `env:"VALID_DURATION"`
|
ValidDuration time.Duration `env:"VALID_DURATION"`
|
||||||
@ -19,11 +20,13 @@ type Config struct {
|
|||||||
DefaultRole model.Role `env:"DEFAULT_ROLE"`
|
DefaultRole model.Role `env:"DEFAULT_ROLE"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service Represents a service with configuration and database connection.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
cfg *Config
|
cfg *Config
|
||||||
db *gorm.DB
|
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 {
|
func New(cfg *Config, db *gorm.DB) *Service {
|
||||||
user := model.NewUser()
|
user := model.NewUser()
|
||||||
var err error
|
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 {
|
type Claims struct {
|
||||||
Role model.Role `json:"rl"`
|
Role model.Role `json:"rl"`
|
||||||
UserID uint `json:"uid"`
|
UserID uint `json:"uid"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Login struct represents user login credentials with a name and password.
|
||||||
type Login struct {
|
type Login struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Password string `json:"Password"`
|
Password string `json:"Password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginResponse Represents the response from a login endpoint, containing a JWT token.
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/auth"
|
"git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config holds configuration settings for the application, loaded from environment variables.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port int `env:"PORT"`
|
Port int `env:"PORT"`
|
||||||
Host string `env:"HOST"`
|
Host string `env:"HOST"`
|
||||||
@ -13,6 +14,7 @@ type Config struct {
|
|||||||
Auth auth.Config `env:"AUTH"`
|
Auth auth.Config `env:"AUTH"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default returns a default configuration with pre-defined values.
|
||||||
func Default() *Config {
|
func Default() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"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) {
|
func CreateMux(cfg *config.Config) (r *mux.Router) {
|
||||||
db := model.Init()
|
db := model.Init()
|
||||||
blg := posts.New(db)
|
blg := posts.New(db)
|
||||||
@ -22,7 +23,7 @@ func CreateMux(cfg *config.Config) (r *mux.Router) {
|
|||||||
// auth
|
// auth
|
||||||
r.HandleFunc("/login", auth.Login).Methods("POST")
|
r.HandleFunc("/login", auth.Login).Methods("POST")
|
||||||
r.HandleFunc("/signup", auth.Signup).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
|
// Posts
|
||||||
r.Handle("/posts", auth.Authenticated(blg.SavePost, model.RoleUser, model.RoleAdmin)).Methods("POST")
|
r.Handle("/posts", auth.Authenticated(blg.SavePost, model.RoleUser, model.RoleAdmin)).Methods("POST")
|
||||||
|
@ -14,11 +14,13 @@ const (
|
|||||||
RoleGuest Role = "guest"
|
RoleGuest Role = "guest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// InvalidJWT Represents a JWT that has expired or is otherwise invalid.
|
||||||
type InvalidJWT struct {
|
type InvalidJWT struct {
|
||||||
JWT string `gorm:"primarykey"`
|
JWT string `gorm:"primarykey"`
|
||||||
ValidUntil time.Time
|
ValidUntil time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User represents a user with an ID, UUID, name, role, and password.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint `gorm:"primarykey" json:"-"`
|
ID uint `gorm:"primarykey" json:"-"`
|
||||||
UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
|
UUID uuid.UUID `gorm:"type:uuid" json:"uuid"`
|
||||||
@ -27,6 +29,7 @@ type User struct {
|
|||||||
Password []byte `json:"-"`
|
Password []byte `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewUser creates a new User struct with a generated UUID.
|
||||||
func NewUser() User {
|
func NewUser() User {
|
||||||
return User{
|
return User{
|
||||||
UUID: uuid.New(),
|
UUID: uuid.New(),
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Post represents a blog post with associated comments and user ID.
|
||||||
type Post struct {
|
type Post struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
ID uint `gorm:"primarykey" json:"id"`
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
@ -13,6 +14,8 @@ type Post struct {
|
|||||||
Comments []Comment
|
Comments []Comment
|
||||||
UserID uint `gorm:"->;<-:create"`
|
UserID uint `gorm:"->;<-:create"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Comment represents a comment on a post, including its ID, post association, content, and creator.
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
ID uint
|
ID uint
|
||||||
PostID uint
|
PostID uint
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Init initializes the database connection, auto-migrates models, and seeds a default post.
|
||||||
func Init() *gorm.DB {
|
func Init() *gorm.DB {
|
||||||
db, err := gorm.Open(sqlite.Open("./blog.db"))
|
db, err := gorm.Open(sqlite.Open("./blog.db"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"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) {
|
func (s Service) SavePost(w http.ResponseWriter, r *http.Request) {
|
||||||
claims, ok := auth.ExtractClaims(r.Context())
|
claims, ok := auth.ExtractClaims(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -45,6 +46,7 @@ func (s Service) SavePost(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(res)
|
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) {
|
func (s Service) GetAllPosts(w http.ResponseWriter, r *http.Request) {
|
||||||
var posts []model.Post
|
var posts []model.Post
|
||||||
|
|
||||||
@ -63,6 +65,7 @@ func (s Service) GetAllPosts(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(res)
|
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) {
|
func (s Service) DeletePost(w http.ResponseWriter, r *http.Request) {
|
||||||
idStr, ok := mux.Vars(r)["postID"]
|
idStr, ok := mux.Vars(r)["postID"]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -2,10 +2,12 @@ package posts
|
|||||||
|
|
||||||
import "gorm.io/gorm"
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
// Service Represents a service with a database connection.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service New creates a new Service instance, initializing it with a GORM database connection.
|
||||||
func New(db *gorm.DB) *Service {
|
func New(db *gorm.DB) *Service {
|
||||||
return &Service{db: db}
|
return &Service{db: db}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"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) {
|
func (s Service) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||||
var err error
|
var err error
|
||||||
var req Password
|
var req Password
|
||||||
|
@ -4,16 +4,19 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Service Represents a service with a database connection.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service - Creates a new Service instance, initializing it with a GORM database connection.
|
||||||
func New(db *gorm.DB) *Service {
|
func New(db *gorm.DB) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Password struct represents a user's password with a JSON tag.
|
||||||
type Password struct {
|
type Password struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"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) {
|
func (s *Service) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
var users []model.User
|
var users []model.User
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ func (s *Service) GetUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write(res)
|
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) {
|
func (s *Service) SetUserRole(w http.ResponseWriter, r *http.Request) {
|
||||||
var role model.Role
|
var role model.Role
|
||||||
userUUIDstr, ok := mux.Vars(r)["userUUID"]
|
userUUIDstr, ok := mux.Vars(r)["userUUID"]
|
||||||
@ -63,6 +65,7 @@ func (s *Service) SetUserRole(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
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) {
|
func (s *Service) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
claims, ok := auth.ExtractClaims(r.Context())
|
claims, ok := auth.ExtractClaims(r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -2,6 +2,7 @@ package cors
|
|||||||
|
|
||||||
import "net/http"
|
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 {
|
func HandlerForOrigin(origin string) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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 {
|
func DeafaultHandler(next http.Handler) http.Handler {
|
||||||
return HandlerForOrigin("*")(next)
|
return HandlerForOrigin("*")(next)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user