This commit is contained in:
@@ -116,12 +116,14 @@ func (s *Service) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.db.First(&user).Error; err != nil {
|
if err := s.db.First(&user).Error; err != nil {
|
||||||
fmt.Fprint(w, "user not found")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprint(w, "user not found")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(login.Password)); err != nil {
|
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(login.Password)); err != nil {
|
||||||
fmt.Fprint(w, "Invalid Password")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprint(w, "Invalid Password")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := s.createJWT(&user)
|
token, err := s.createJWT(&user)
|
||||||
@@ -134,8 +136,8 @@ func (s *Service) Login(w http.ResponseWriter, r *http.Request) {
|
|||||||
Token: token,
|
Token: token,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error: ", err)
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Println("Error: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Authenticated: This function is a middleware that authenticates incoming HTTP requests using JWT tokens and role-based access control.
|
// 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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Our middleware logic goes here...
|
// Our middleware logic goes here...
|
||||||
token, err := extractToken(r)
|
token, err := extractToken(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if !mustauth {
|
||||||
|
r = writeToContext(r, nil)
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func CreateMux(cfg *config.Config) (r *mux.Router) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func frontend(r *mux.Route) {
|
func frontend(r *mux.Route) {
|
||||||
@@ -51,12 +51,13 @@ func app(r *mux.Router, cfg *config.Config) {
|
|||||||
// auth
|
// auth
|
||||||
r.HandleFunc("/auth/login", auth.Login).Methods("POST")
|
r.HandleFunc("/auth/login", auth.Login).Methods("POST")
|
||||||
r.HandleFunc("/auth/signup", auth.Signup).Methods("POST")
|
r.HandleFunc("/auth/signup", auth.Signup).Methods("POST")
|
||||||
r.Handle("/auth/password", auth.Authenticated(auth.ChangePassword)).Methods("PUT")
|
r.Handle("/auth/password", auth.Authenticated(auth.ChangePassword, true)).Methods("PUT")
|
||||||
r.Handle("/auth/logout", auth.Authenticated(auth.Logout)).Methods("DELETE")
|
r.Handle("/auth/logout", auth.Authenticated(auth.Logout, true)).Methods("DELETE")
|
||||||
|
|
||||||
// Posts
|
// Posts
|
||||||
r.Handle("/posts", auth.Authenticated(blg.SavePost, model.RoleUser, model.RoleAdmin)).Methods("POST")
|
r.Handle("/posts", auth.Authenticated(blg.SavePost, true, model.RoleUser, model.RoleAdmin)).Methods("POST")
|
||||||
r.Handle("/posts", auth.Authenticated(blg.SavePost, model.RoleUser, model.RoleAdmin)).Methods("PUT")
|
r.Handle("/posts", auth.Authenticated(blg.SavePost, true, model.RoleUser, model.RoleAdmin)).Methods("PUT")
|
||||||
r.Handle("/posts/{postID}", auth.Authenticated(blg.DeletePost, model.RoleUser, model.RoleAdmin)).Methods("DELETE")
|
r.Handle("/posts/{postID}", auth.Authenticated(blg.DeletePost, true, model.RoleUser, model.RoleAdmin)).Methods("DELETE")
|
||||||
r.Handle("/posts", http.HandlerFunc(blg.GetAllPosts)).Methods("GET")
|
r.Handle("/posts", auth.Authenticated(blg.GetAllPosts, false)).Methods("GET")
|
||||||
|
r.Handle("/posts/secret/{secret}", http.HandlerFunc(blg.GetPostBySecret)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ type Post struct {
|
|||||||
TLDR string `json:"tldr"`
|
TLDR string `json:"tldr"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Comments []Comment
|
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.
|
// Comment represents a comment on a post, including its ID, post association, content, and creator.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"git.schreifuchs.ch/schreifuchs/ng-blog/internal/auth"
|
"git.schreifuchs.ch/schreifuchs/ng-blog/internal/auth"
|
||||||
"git.schreifuchs.ch/schreifuchs/ng-blog/internal/model"
|
"git.schreifuchs.ch/schreifuchs/ng-blog/internal/model"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +29,13 @@ func (s Service) SavePost(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if post.Private {
|
||||||
|
secret := uuid.NewString()
|
||||||
|
post.Secret = &secret
|
||||||
|
} else {
|
||||||
|
post.Secret = nil
|
||||||
|
}
|
||||||
|
|
||||||
post.UserID = claims.UserID
|
post.UserID = claims.UserID
|
||||||
|
|
||||||
if err := s.db.Save(&post).Error; err != nil {
|
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) {
|
func (s Service) GetAllPosts(w http.ResponseWriter, r *http.Request) {
|
||||||
var posts []model.Post
|
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 {
|
if err := s.db.Preload("Comments").Order("created_at DESC").Find(&posts).Error; err != nil {
|
||||||
fmt.Fprint(w, err.Error())
|
fmt.Fprint(w, err.Error())
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
fmt.Fprint(w, err.Error())
|
fmt.Fprint(w, err.Error())
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { LoggedInGuard } from './shared/guards/logged-in.guard';
|
|||||||
import { CreatePostComponent } from './routes/post/create-post/create-post.component';
|
import { CreatePostComponent } from './routes/post/create-post/create-post.component';
|
||||||
import { UpdatePostComponent } from './routes/post/update-post/update-post.component';
|
import { UpdatePostComponent } from './routes/post/update-post/update-post.component';
|
||||||
import { AccountComponent } from './routes/dashboard/account/account.component';
|
import { AccountComponent } from './routes/dashboard/account/account.component';
|
||||||
|
import { SecretPostComponent } from './routes/post/secret-post/secret-post.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: HomeComponent },
|
{ path: '', component: HomeComponent },
|
||||||
@@ -13,6 +14,7 @@ export const routes: Routes = [
|
|||||||
path: 'post',
|
path: 'post',
|
||||||
children: [
|
children: [
|
||||||
{ path: 'new', component: CreatePostComponent },
|
{ path: 'new', component: CreatePostComponent },
|
||||||
|
{ path: 'secret/:secret', component: SecretPostComponent },
|
||||||
{ path: ':id/edit', component: UpdatePostComponent },
|
{ path: ':id/edit', component: UpdatePostComponent },
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<label>Private:</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
[ngModel]="data().private"
|
||||||
|
(ngModelChange)="private = $event"
|
||||||
|
class="p-1 border-2 rounded-md col-span-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ data().private ? "this post is private" : "this post is public" }}
|
||||||
|
</p>
|
||||||
<div class="grid grid-cols-3">
|
<div class="grid grid-cols-3">
|
||||||
<label>Title:</label>
|
<label>Title:</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -22,9 +22,18 @@ export class PostEditorComponent {
|
|||||||
title: '',
|
title: '',
|
||||||
tldr: '',
|
tldr: '',
|
||||||
content: '',
|
content: '',
|
||||||
|
private: false,
|
||||||
});
|
});
|
||||||
@Output('postChange') dataChange = new EventEmitter<Post>();
|
@Output('postChange') dataChange = new EventEmitter<Post>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
if (this.data().private === null) {
|
||||||
|
this.private = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
set title(val: string) {
|
set title(val: string) {
|
||||||
this.data.update((d) => ({ ...d, title: val }));
|
this.data.update((d) => ({ ...d, title: val }));
|
||||||
}
|
}
|
||||||
@@ -36,4 +45,7 @@ export class PostEditorComponent {
|
|||||||
set content(val: string) {
|
set content(val: string) {
|
||||||
this.data.update((d) => ({ ...d, content: val }));
|
this.data.update((d) => ({ ...d, content: val }));
|
||||||
}
|
}
|
||||||
|
set private(val: boolean) {
|
||||||
|
this.data.update((d) => ({ ...d, private: val }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<section class="grid grid-cols-1 gap-5 m-5">
|
<section class="grid grid-cols-1 gap-5 m-5">
|
||||||
<article
|
<article
|
||||||
*ngFor="let post of posts()"
|
*ngFor="let post of posts()"
|
||||||
class="p-5 grid grid-cols-5 grid-rows-3 items-start rounded-s bg-white drop-shadow-md hover:drop-shadow-lg"
|
class="p-5 grid grid-cols-5 grid-rows-3 items-start rounded-s bg-white drop-shadow-md hover:drop-shadow-lg gap-5"
|
||||||
>
|
>
|
||||||
<h3 class="text-xl col-span-4">{{ post.title }}</h3>
|
<h3 class="text-xl col-span-4">{{ post.title }}</h3>
|
||||||
<p class="col-start-1 col-span-4 row-start-2 row-span-2">
|
<p class="col-start-1 col-span-4 row-start-2 row-span-2">
|
||||||
@@ -31,5 +31,14 @@
|
|||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</app-button>
|
</app-button>
|
||||||
|
@if (post.secret !== undefined) {
|
||||||
|
<app-button
|
||||||
|
(click)="getPath(post.secret)"
|
||||||
|
class="col-start-5 row-start-2"
|
||||||
|
color="yellow"
|
||||||
|
>
|
||||||
|
Copy Link
|
||||||
|
</app-button>
|
||||||
|
}
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -17,4 +17,8 @@ export class DashboardComponent {
|
|||||||
delete(id: number) {
|
delete(id: number) {
|
||||||
this.postsService.deletePost(id);
|
this.postsService.deletePost(id);
|
||||||
}
|
}
|
||||||
|
getPath(secret: string) {
|
||||||
|
const url = document.URL.replaceAll('/dashboard', '');
|
||||||
|
navigator.clipboard.writeText(`${url}/post/secret/${secret}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Component, inject, Input } from '@angular/core';
|
import { Component, inject, Input } from '@angular/core';
|
||||||
import { PostsService } from '../../shared/services/posts.service';
|
import { PostsService } from '../../shared/services/posts.service';
|
||||||
import { JsonPipe, NgIf } from '@angular/common';
|
|
||||||
import { MarkdownComponent } from '../../components/markdown/markdown.component';
|
import { MarkdownComponent } from '../../components/markdown/markdown.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<h2 class="text-3xl">{{ post()?.title }}</h2>
|
||||||
|
<p class="mb-5 italic text-sm">TL;DR; {{ post()?.tldr }}</p>
|
||||||
|
|
||||||
|
<app-markdown
|
||||||
|
class="todo"
|
||||||
|
[markdown]="post()?.content || 'this post is empty'"
|
||||||
|
/>
|
||||||
28
web/src/app/routes/post/secret-post/secret-post.component.ts
Normal file
28
web/src/app/routes/post/secret-post/secret-post.component.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
signal,
|
||||||
|
WritableSignal,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { PostsService } from '../../../shared/services/posts.service';
|
||||||
|
import { Post } from '../../../shared/interfaces/post';
|
||||||
|
import { MarkdownComponent } from '../../../components/markdown/markdown.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-secret-post',
|
||||||
|
imports: [MarkdownComponent],
|
||||||
|
templateUrl: './secret-post.component.html',
|
||||||
|
})
|
||||||
|
export class SecretPostComponent implements OnInit {
|
||||||
|
@Input() secret!: string;
|
||||||
|
private postsService = inject(PostsService);
|
||||||
|
post: WritableSignal<Post | undefined> = signal(undefined);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.postsService.getPostBySecret(this.secret).subscribe((post) => {
|
||||||
|
this.post.set(post);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ export interface Post {
|
|||||||
title: string;
|
title: string;
|
||||||
tldr: string;
|
tldr: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
private?: boolean;
|
||||||
|
secret?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Comment {
|
export interface Comment {
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ export class PostsService {
|
|||||||
getPost(id: number): Signal<Post | undefined> {
|
getPost(id: number): Signal<Post | undefined> {
|
||||||
return computed(() => this.posts().get(id));
|
return computed(() => this.posts().get(id));
|
||||||
}
|
}
|
||||||
|
getPostBySecret(secret: string) {
|
||||||
|
return this.http.get<Post>(`${environment.apiRoot}/posts/secret/${secret}`);
|
||||||
|
}
|
||||||
|
|
||||||
deletePost(id: number) {
|
deletePost(id: number) {
|
||||||
this.http.delete(`${environment.apiRoot}/posts/${id}`).subscribe(() => {
|
this.http.delete(`${environment.apiRoot}/posts/${id}`).subscribe(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user