{{ post.title }}
@@ -31,5 +31,14 @@
>
Delete
+ @if (post.secret !== undefined) {
+
diff --git a/internal/auth/controller.go b/internal/auth/controller.go index 4bfd9b4..6c3dba4 100644 --- a/internal/auth/controller.go +++ b/internal/auth/controller.go @@ -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 } diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go index 13e3026..9226e6e 100644 --- a/internal/auth/middleware.go +++ b/internal/auth/middleware.go @@ -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 } diff --git a/internal/initialize/inject.go b/internal/initialize/inject.go index ce53282..05b9e30 100644 --- a/internal/initialize/inject.go +++ b/internal/initialize/inject.go @@ -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") } diff --git a/internal/model/blog.go b/internal/model/blog.go index c9d034e..a750a32 100644 --- a/internal/model/blog.go +++ b/internal/model/blog.go @@ -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. diff --git a/internal/posts/controller.go b/internal/posts/controller.go index 1a5b73e..b49d0a8 100644 --- a/internal/posts/controller.go +++ b/internal/posts/controller.go @@ -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) diff --git a/web/src/app/app.routes.ts b/web/src/app/app.routes.ts index 38c7fc3..af5167d 100644 --- a/web/src/app/app.routes.ts +++ b/web/src/app/app.routes.ts @@ -6,6 +6,7 @@ import { LoggedInGuard } from './shared/guards/logged-in.guard'; import { CreatePostComponent } from './routes/post/create-post/create-post.component'; import { UpdatePostComponent } from './routes/post/update-post/update-post.component'; import { AccountComponent } from './routes/dashboard/account/account.component'; +import { SecretPostComponent } from './routes/post/secret-post/secret-post.component'; export const routes: Routes = [ { path: '', component: HomeComponent }, @@ -13,6 +14,7 @@ export const routes: Routes = [ path: 'post', children: [ { path: 'new', component: CreatePostComponent }, + { path: 'secret/:secret', component: SecretPostComponent }, { path: ':id/edit', component: UpdatePostComponent }, { path: ':id', diff --git a/web/src/app/components/post-editor/post-editor.component.html b/web/src/app/components/post-editor/post-editor.component.html index 3fd9509..d28756a 100644 --- a/web/src/app/components/post-editor/post-editor.component.html +++ b/web/src/app/components/post-editor/post-editor.component.html @@ -1,4 +1,16 @@
+ {{ data().private ? "this post is private" : "this post is public" }} +
@@ -31,5 +31,14 @@
>
Delete
+ @if (post.secret !== undefined) {
+
TL;DR; {{ post()?.tldr }}
+ +