diff --git a/backend/auth/login.go b/backend/auth/login.go index 244ebfc..46f641c 100644 --- a/backend/auth/login.go +++ b/backend/auth/login.go @@ -14,8 +14,8 @@ import ( func Login(username, password string, secret []byte) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var login model.Login - if err := json.NewDecoder(r.Response.Request.Body).Decode(&login); err != nil { + login := model.Login{} + if err := json.NewDecoder(r.Body).Decode(&login); err != nil { w.WriteHeader(http.StatusUnauthorized) return } diff --git a/backend/blog.db b/backend/blog.db index 0222620..0a59c2a 100644 Binary files a/backend/blog.db and b/backend/blog.db differ diff --git a/backend/cors/cors.go b/backend/cors/cors.go index 9a8e027..ab611a8 100644 --- a/backend/cors/cors.go +++ b/backend/cors/cors.go @@ -5,7 +5,7 @@ import "net/http" 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) { - if origin := r.Header.Get("Origin"); origin != "" { + if o := r.Header.Get("Origin"); o != "" { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") @@ -13,6 +13,8 @@ func HandlerForOrigin(origin string) func(http.Handler) http.Handler { w.Header().Set("Access-Control-Allow-Credentials", "true") } if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusNoContent) + return } next.ServeHTTP(w, r) diff --git a/backend/main.go b/backend/main.go index 68fa234..44b89b8 100644 --- a/backend/main.go +++ b/backend/main.go @@ -28,11 +28,13 @@ func main() { db := model.Init() blg := blog.New(db) r := mux.NewRouter() + r.Use(cors.HandlerForOrigin("*")) r.Handle("/login", auth.Login(user, password, []byte(secret))).Methods("POST") r.Handle("/posts", auth.Authenticated([]byte(secret))(blg.CreatePost)).Methods("POST") r.Handle("/posts", http.HandlerFunc(blg.GetAllPosts)).Methods("GET") - - r.Use(cors.HandlerForOrigin("*")) - + r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // The CORS middleware should set up the headers for you + w.WriteHeader(http.StatusNoContent) + }) http.ListenAndServe(":8080", r) } diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 360b314..b009cd4 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -4,8 +4,19 @@

My Blog

- + -
+ + + + + +
diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 57992ad..b10338b 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,11 +1,39 @@ -import { Component } from '@angular/core'; +import { + Component, + computed, + effect, + inject, + OnChanges, + SimpleChanges, +} from '@angular/core'; import { RouterLink, RouterOutlet } from '@angular/router'; +import { ModalComponent } from './components/modal/modal.component'; +import { LoginComponent } from './components/login/login.component'; +import { AuthService } from './shared/services/auth.service'; +import { NgIf } from '@angular/common'; @Component({ selector: 'app-root', - imports: [RouterOutlet, RouterLink], + imports: [RouterOutlet, RouterLink, ModalComponent, LoginComponent, NgIf], templateUrl: './app.component.html', }) export class AppComponent { - title = 'frontend'; + private auth = inject(AuthService); + loginOpen: boolean = false; + loggedIn = computed(() => this.auth.jwt() !== null); + + constructor() { + effect(() => { + if (this.auth.jwt()) { + this.loginOpen = false; + } + }); + } + + toggleLogin() { + this.loginOpen = !this.loginOpen; + } + logOut() { + this.auth.jwt.set(null); + } } diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 143ce72..0148fc8 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,8 +1,13 @@ import { Routes } from '@angular/router'; import { HomeComponent } from './routes/home/home.component'; import { PostComponent } from './routes/post/post.component'; +import { AdminComponent } from './routes/admin/admin.component'; +import { LoggedInGuard } from './shared/guards/logged-in.guard'; +import { PostEditorComponent } from './components/post-editor/post-editor.component'; export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'post', children: [{ path: ':id', component: PostComponent }] }, + { path: 'admin', component: AdminComponent, canActivate: [LoggedInGuard] }, + { path: 'tst', component: PostEditorComponent }, ]; diff --git a/frontend/src/app/components/login/login.component.html b/frontend/src/app/components/login/login.component.html new file mode 100644 index 0000000..9563b2c --- /dev/null +++ b/frontend/src/app/components/login/login.component.html @@ -0,0 +1,22 @@ +
+ + + + + +
diff --git a/frontend/src/app/components/login/login.component.spec.ts b/frontend/src/app/components/login/login.component.spec.ts new file mode 100644 index 0000000..18f3685 --- /dev/null +++ b/frontend/src/app/components/login/login.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/login/login.component.ts b/frontend/src/app/components/login/login.component.ts new file mode 100644 index 0000000..df985c9 --- /dev/null +++ b/frontend/src/app/components/login/login.component.ts @@ -0,0 +1,39 @@ +import { Component, inject } from '@angular/core'; +import { + FormGroup, + Validators, + ReactiveFormsModule, + FormControl, +} from '@angular/forms'; +import { AuthService } from '../../shared/services/auth.service'; +import { User } from '../../shared/interfaces/auth'; + +@Component({ + selector: 'app-login', + imports: [ReactiveFormsModule], + templateUrl: './login.component.html', +}) +export class LoginComponent { + private auth = inject(AuthService); + form = new FormGroup({ + name: new FormControl('', [Validators.required]), + password: new FormControl('', [Validators.required]), + }); + + get name() { + return this.form.controls.name; + } + get password() { + return this.form.controls.password; + } + submit() { + if (this.form.invalid) { + return; + } + const user: User = { + name: this.form.controls.name.value!, + password: this.form.controls.password.value!, + }; + this.auth.login(user); + } +} diff --git a/frontend/src/app/components/modal/modal.component.html b/frontend/src/app/components/modal/modal.component.html new file mode 100644 index 0000000..8d4756f --- /dev/null +++ b/frontend/src/app/components/modal/modal.component.html @@ -0,0 +1,9 @@ +
+
+ + +
+
diff --git a/frontend/src/app/components/modal/modal.component.spec.ts b/frontend/src/app/components/modal/modal.component.spec.ts new file mode 100644 index 0000000..f9bab0f --- /dev/null +++ b/frontend/src/app/components/modal/modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ModalComponent } from './modal.component'; + +describe('ModalComponent', () => { + let component: ModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ModalComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/modal/modal.component.ts b/frontend/src/app/components/modal/modal.component.ts new file mode 100644 index 0000000..b002642 --- /dev/null +++ b/frontend/src/app/components/modal/modal.component.ts @@ -0,0 +1,18 @@ +import { NgIf } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-modal', + imports: [NgIf], + standalone: true, + templateUrl: './modal.component.html', +}) +export class ModalComponent { + @Input() open: boolean = false; + @Output() openChange = new EventEmitter(); + + toggleOpen() { + this.open = !this.open; + this.openChange.emit(this.open); + } +} diff --git a/frontend/src/app/components/post-editor/post-editor.component.css b/frontend/src/app/components/post-editor/post-editor.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/components/post-editor/post-editor.component.html b/frontend/src/app/components/post-editor/post-editor.component.html new file mode 100644 index 0000000..a465289 --- /dev/null +++ b/frontend/src/app/components/post-editor/post-editor.component.html @@ -0,0 +1,27 @@ +
+
+ + +
+

{{ data().title }}

+
+ + +
+

TL;DR; {{ data().tldr }}

+ + + +
diff --git a/frontend/src/app/components/post-editor/post-editor.component.spec.ts b/frontend/src/app/components/post-editor/post-editor.component.spec.ts new file mode 100644 index 0000000..5acaa27 --- /dev/null +++ b/frontend/src/app/components/post-editor/post-editor.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PostEditorComponent } from './post-editor.component'; + +describe('PostEditorComponent', () => { + let component: PostEditorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PostEditorComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PostEditorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/post-editor/post-editor.component.ts b/frontend/src/app/components/post-editor/post-editor.component.ts new file mode 100644 index 0000000..967618a --- /dev/null +++ b/frontend/src/app/components/post-editor/post-editor.component.ts @@ -0,0 +1,26 @@ +import { Component, effect, signal } from '@angular/core'; +import { MarkdownComponent } from '../markdown/markdown.component'; +import { FormsModule } from '@angular/forms'; +import { Post } from '../../shared/interfaces/post'; + +@Component({ + selector: 'app-post-editor', + imports: [MarkdownComponent, FormsModule], + standalone: true, + templateUrl: './post-editor.component.html', + styleUrl: './post-editor.component.css', +}) +export class PostEditorComponent { + data = signal({ + id: 0, + title: '', + tldr: '', + content: '', + }); + + constructor() { + effect(() => { + console.log(this.data()); + }); + } +} diff --git a/frontend/src/app/routes/admin/admin.component.html b/frontend/src/app/routes/admin/admin.component.html new file mode 100644 index 0000000..95c91bd --- /dev/null +++ b/frontend/src/app/routes/admin/admin.component.html @@ -0,0 +1 @@ + diff --git a/frontend/src/app/routes/admin/admin.component.spec.ts b/frontend/src/app/routes/admin/admin.component.spec.ts new file mode 100644 index 0000000..617b55b --- /dev/null +++ b/frontend/src/app/routes/admin/admin.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminComponent } from './admin.component'; + +describe('AdminComponent', () => { + let component: AdminComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/routes/admin/admin.component.ts b/frontend/src/app/routes/admin/admin.component.ts new file mode 100644 index 0000000..2610029 --- /dev/null +++ b/frontend/src/app/routes/admin/admin.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { PostEditorComponent } from '../../components/post-editor/post-editor.component'; + +@Component({ + selector: 'app-admin', + imports: [PostEditorComponent], + standalone: true, + templateUrl: './admin.component.html', +}) +export class AdminComponent {} diff --git a/frontend/src/app/routes/home/home.component.ts b/frontend/src/app/routes/home/home.component.ts index bb87e20..37f2357 100644 --- a/frontend/src/app/routes/home/home.component.ts +++ b/frontend/src/app/routes/home/home.component.ts @@ -2,7 +2,6 @@ import { Component, inject, OnInit, Signal } from '@angular/core'; import { PostsService } from '../../shared/services/posts.service'; import { NgForOf } from '@angular/common'; import { RouterLink } from '@angular/router'; -import { Post } from '../../shared/services/interfaces/post'; @Component({ selector: 'app-home', diff --git a/frontend/src/app/shared/guards/logged-in.guard.spec.ts b/frontend/src/app/shared/guards/logged-in.guard.spec.ts new file mode 100644 index 0000000..1850959 --- /dev/null +++ b/frontend/src/app/shared/guards/logged-in.guard.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LoggedInGuard } from './logged-in.guard'; + +describe('LoggedInGuard', () => { + let guard: LoggedInGuard; + + beforeEach(() => { + TestBed.configureTestingModule({}); + guard = TestBed.inject(LoggedInGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/shared/guards/logged-in.guard.ts b/frontend/src/app/shared/guards/logged-in.guard.ts new file mode 100644 index 0000000..f38a679 --- /dev/null +++ b/frontend/src/app/shared/guards/logged-in.guard.ts @@ -0,0 +1,13 @@ +import { inject, Injectable } from '@angular/core'; +import { CanActivate } from '@angular/router'; +import { AuthService } from '../services/auth.service'; + +@Injectable({ + providedIn: 'root', +}) +export class LoggedInGuard implements CanActivate { + private auth = inject(AuthService); + canActivate(): boolean { + return this.auth.jwt() !== null; + } +} diff --git a/frontend/src/app/shared/interfaces/auth.ts b/frontend/src/app/shared/interfaces/auth.ts new file mode 100644 index 0000000..3676e97 --- /dev/null +++ b/frontend/src/app/shared/interfaces/auth.ts @@ -0,0 +1,7 @@ +export interface User { + name: string; + password: string; +} +export interface LoginResponse { + token: string; +} diff --git a/frontend/src/app/shared/services/interfaces/post.ts b/frontend/src/app/shared/interfaces/post.ts similarity index 100% rename from frontend/src/app/shared/services/interfaces/post.ts rename to frontend/src/app/shared/interfaces/post.ts diff --git a/frontend/src/app/shared/services/auth.service.spec.ts b/frontend/src/app/shared/services/auth.service.spec.ts new file mode 100644 index 0000000..f1251ca --- /dev/null +++ b/frontend/src/app/shared/services/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/shared/services/auth.service.ts b/frontend/src/app/shared/services/auth.service.ts new file mode 100644 index 0000000..ec30409 --- /dev/null +++ b/frontend/src/app/shared/services/auth.service.ts @@ -0,0 +1,20 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable, signal, WritableSignal } from '@angular/core'; +import { LoginResponse, User } from '../interfaces/auth'; +import { environment } from '../../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + private http = inject(HttpClient); + jwt: WritableSignal = signal(null); + + login(user: User) { + console.log(user); + + this.http + .post(`${environment.apiRoot}/login`, user) + .subscribe((res) => this.jwt.set(res.token)); + } +} diff --git a/frontend/src/app/shared/services/posts.service.ts b/frontend/src/app/shared/services/posts.service.ts index bca3318..4ef2e6e 100644 --- a/frontend/src/app/shared/services/posts.service.ts +++ b/frontend/src/app/shared/services/posts.service.ts @@ -7,7 +7,7 @@ import { signal, WritableSignal, } from '@angular/core'; -import { Post } from './interfaces/post'; +import { Post } from '../interfaces/post'; import { environment } from '../../../environments/environment'; @Injectable({ diff --git a/frontend/src/styles.css b/frontend/src/styles.css index a7b5fc6..7ca046a 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2,7 +2,7 @@ @import "tailwindcss"; /* apply todo css */ -.todo { +app-markdown { @layer base { body { @apply bg-gray-50 text-gray-800 font-sans leading-relaxed text-base; @@ -30,9 +30,11 @@ @apply text-blue-600 hover:text-blue-800 transition-colors underline; } - ul, + ul { + @apply list-disc pl-6 mb-4; + } ol { - @apply pl-6 mb-4; + @apply list-decimal pl-6 mb-4; } li { @@ -55,5 +57,11 @@ tr:nth-child(even) { @apply bg-gray-50; } + blockquote { + @apply italic bg-gray-200 rounded-md backdrop-blur-md p-3; + } + p { + @apply mb-2; + } } }