feat: private posts
All checks were successful
Release / publish (push) Successful in 2m41s

This commit is contained in:
2025-10-17 23:51:19 +02:00
parent 68574ad289
commit 893c49ec88
15 changed files with 154 additions and 15 deletions

View File

@@ -116,12 +116,14 @@ func (s *Service) Login(w http.ResponseWriter, r *http.Request) {
}
if err := s.db.First(&user).Error; err != nil {
fmt.Fprint(w, "user not found")
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "user not found")
return
}
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(login.Password)); err != nil {
fmt.Fprint(w, "Invalid Password")
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "Invalid Password")
return
}
token, err := s.createJWT(&user)
@@ -134,8 +136,8 @@ func (s *Service) Login(w http.ResponseWriter, r *http.Request) {
Token: token,
})
if err != nil {
log.Println("Error: ", err)
w.WriteHeader(http.StatusInternalServerError)
log.Println("Error: ", err)
return
}

View File

@@ -8,11 +8,16 @@ import (
)
// 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 {
func (s *Service) Authenticated(next http.HandlerFunc, mustauth bool, 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 {
if !mustauth {
r = writeToContext(r, nil)
next(w, r)
return
}
w.WriteHeader(http.StatusUnauthorized)
return
}

View File

@@ -26,7 +26,7 @@ func CreateMux(cfg *config.Config) (r *mux.Router) {
w.WriteHeader(http.StatusNoContent)
})
return
return r
}
func frontend(r *mux.Route) {
@@ -51,12 +51,13 @@ func app(r *mux.Router, cfg *config.Config) {
// auth
r.HandleFunc("/auth/login", auth.Login).Methods("POST")
r.HandleFunc("/auth/signup", auth.Signup).Methods("POST")
r.Handle("/auth/password", auth.Authenticated(auth.ChangePassword)).Methods("PUT")
r.Handle("/auth/logout", auth.Authenticated(auth.Logout)).Methods("DELETE")
r.Handle("/auth/password", auth.Authenticated(auth.ChangePassword, true)).Methods("PUT")
r.Handle("/auth/logout", auth.Authenticated(auth.Logout, true)).Methods("DELETE")
// 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("PUT")
r.Handle("/posts/{postID}", auth.Authenticated(blg.DeletePost, model.RoleUser, model.RoleAdmin)).Methods("DELETE")
r.Handle("/posts", http.HandlerFunc(blg.GetAllPosts)).Methods("GET")
r.Handle("/posts", auth.Authenticated(blg.SavePost, true, model.RoleUser, model.RoleAdmin)).Methods("POST")
r.Handle("/posts", auth.Authenticated(blg.SavePost, true, model.RoleUser, model.RoleAdmin)).Methods("PUT")
r.Handle("/posts/{postID}", auth.Authenticated(blg.DeletePost, true, model.RoleUser, model.RoleAdmin)).Methods("DELETE")
r.Handle("/posts", auth.Authenticated(blg.GetAllPosts, false)).Methods("GET")
r.Handle("/posts/secret/{secret}", http.HandlerFunc(blg.GetPostBySecret)).Methods("GET")
}

View File

@@ -12,7 +12,9 @@ type Post struct {
TLDR string `json:"tldr"`
Content string `json:"content"`
Comments []Comment
UserID uint `gorm:"->;<-:create"`
UserID uint `gorm:"->;<-:create"`
Private bool `json:"private" gorm:"-"`
Secret *string `json:"secret,omitempty" gorm:"->;<-:create;unique"`
}
// Comment represents a comment on a post, including its ID, post association, content, and creator.

View File

@@ -9,6 +9,7 @@ import (
"git.schreifuchs.ch/schreifuchs/ng-blog/internal/auth"
"git.schreifuchs.ch/schreifuchs/ng-blog/internal/model"
"github.com/google/uuid"
"github.com/gorilla/mux"
)
@@ -28,6 +29,13 @@ func (s Service) SavePost(w http.ResponseWriter, r *http.Request) {
return
}
if post.Private {
secret := uuid.NewString()
post.Secret = &secret
} else {
post.Secret = nil
}
post.UserID = claims.UserID
if err := s.db.Save(&post).Error; err != nil {
@@ -50,13 +58,56 @@ func (s Service) SavePost(w http.ResponseWriter, r *http.Request) {
func (s Service) GetAllPosts(w http.ResponseWriter, r *http.Request) {
var posts []model.Post
claims, ok := auth.ExtractClaims(r.Context())
if !ok {
claims = nil
}
if err := s.db.Preload("Comments").Order("created_at DESC").Find(&posts).Error; err != nil {
fmt.Fprint(w, err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
res, err := json.Marshal(&posts)
newPosts := make([]model.Post, 0, len(posts))
for _, p := range posts {
if p.Secret == nil {
newPosts = append(newPosts, p)
continue
}
if claims == nil {
continue
}
if claims.UserID == p.UserID {
newPosts = append(newPosts, p)
}
}
res, err := json.Marshal(&newPosts)
if err != nil {
fmt.Fprint(w, err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(res)
}
// GetPostBySecret retrieves a post by its secret from the database, eager-loads comments, and returns it as JSON.
func (s Service) GetPostBySecret(w http.ResponseWriter, r *http.Request) {
secret, ok := mux.Vars(r)["secret"]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
var post model.Post
if err := s.db.Preload("Comments").Where("secret = ?", secret).First(&post).Error; err != nil {
fmt.Fprint(w, err.Error())
w.WriteHeader(http.StatusNotFound)
return
}
res, err := json.Marshal(&post)
if err != nil {
fmt.Fprint(w, err.Error())
w.WriteHeader(http.StatusInternalServerError)