Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
30dac9f12f | |||
73ff28347a |
42
.gitignore
vendored
42
.gitignore
vendored
@ -1,2 +1,44 @@
|
||||
blog.db
|
||||
backend
|
||||
**/.DS_Store
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
web/dist
|
||||
web/tmp
|
||||
web/out-tsc
|
||||
web/bazel-out
|
||||
|
||||
# Node
|
||||
web/node_modules
|
||||
web/npm-debug.log
|
||||
web/yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
web/.idea/
|
||||
web/.project
|
||||
web/.classpath
|
||||
web/.c9/
|
||||
web/*.launch
|
||||
web/.settings/
|
||||
web/*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
web/.vscode/*
|
||||
web/!.vscode/settings.json
|
||||
web/!.vscode/tasks.json
|
||||
web/!.vscode/launch.json
|
||||
web/!.vscode/extensions.json
|
||||
web/.history/*
|
||||
|
||||
# Miscellaneous
|
||||
web/.angular/cache
|
||||
web/.sass-cache/
|
||||
web/connect.lock
|
||||
web/coverage
|
||||
web/libpeerconnection.log
|
||||
testem.log
|
||||
web/typings
|
||||
|
||||
# System files
|
||||
web/Thumbs.db
|
||||
|
2
go.mod
2
go.mod
@ -13,6 +13,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/go-swiss/compress v0.0.0-20231015173048-c7b565746931 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -1,3 +1,7 @@
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/go-swiss/compress v0.0.0-20231015173048-c7b565746931 h1:4GONJghYPtbCcPDZXWhbgKgbK8tfmv/C7su6O72AZWw=
|
||||
github.com/go-swiss/compress v0.0.0-20231015173048-c7b565746931/go.mod h1:atoBfZRTinNQQlYfu42MCp8E1yoKWhmohXj71lgRtfU=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
@ -48,6 +48,61 @@ func (s *Service) Signup(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Error: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
token, err := s.createJWT(&user)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := json.Marshal(&LoginResponse{
|
||||
Token: token,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Error: ", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(res)
|
||||
}
|
||||
|
||||
// Signup handles user signup by decoding request body, hashing the password, and saving user data to the database.
|
||||
func (s *Service) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var login Login
|
||||
var password []byte
|
||||
|
||||
claims, ok := ExtractClaims(r.Context())
|
||||
if !ok {
|
||||
log.Println("Error while extracting claims")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(r.Body).Decode(&login); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len([]byte(login.Password)) > 72 {
|
||||
fmt.Fprint(w, "Password to long, max 72 bytes")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if password, err = bcrypt.GenerateFromPassword([]byte(login.Password), 6); err != nil {
|
||||
log.Println("Error: ", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.db.Model(&model.User{}).Where("id = ?", claims.UserID).Update("password", password).Error
|
||||
if err != nil {
|
||||
|
||||
log.Printf("Error: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Login handles user login by decoding request body, verifying credentials, and returning a JWT token.
|
||||
|
@ -19,9 +19,7 @@ func CreateMux(cfg *config.Config) (r *mux.Router) {
|
||||
r.Use(cors.HandlerForOrigin("*"))
|
||||
|
||||
app(r.PathPrefix("/api").Subrouter(), cfg)
|
||||
|
||||
frontend := web.Frontend
|
||||
r.PathPrefix("/").Handler(middlewares.AddPrefix("/dist/frontend/browser", http.FileServerFS(frontend)))
|
||||
frontend(r.PathPrefix("/"))
|
||||
|
||||
r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// The CORS middleware should set up the headers for you
|
||||
@ -31,15 +29,28 @@ func CreateMux(cfg *config.Config) (r *mux.Router) {
|
||||
return
|
||||
}
|
||||
|
||||
func frontend(r *mux.Route) {
|
||||
frontend := web.Frontend
|
||||
r.Handler(
|
||||
middlewares.AddPrefix("/dist/frontend/browser",
|
||||
middlewares.FallbackFile(
|
||||
frontend,
|
||||
"/dist/frontend/browser/index.html",
|
||||
http.FileServerFS(frontend),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
func app(r *mux.Router, cfg *config.Config) {
|
||||
db := model.Init()
|
||||
blg := posts.New(db)
|
||||
auth := auth.New(&cfg.Auth, db)
|
||||
|
||||
// auth
|
||||
r.HandleFunc("/login", auth.Login).Methods("POST")
|
||||
r.HandleFunc("/signup", auth.Signup).Methods("POST")
|
||||
r.Handle("/logout", auth.Authenticated(auth.Logout)).Methods("DELETE")
|
||||
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")
|
||||
|
||||
// Posts
|
||||
r.Handle("/posts", auth.Authenticated(blg.SavePost, model.RoleUser, model.RoleAdmin)).Methods("POST")
|
||||
|
61
pkg/middlewares/fallback.go
Normal file
61
pkg/middlewares/fallback.go
Normal file
@ -0,0 +1,61 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FallbackFile serves a fallback file if the next handler returns a 404.
|
||||
// fs: The file system to serve the fallback file from.
|
||||
// name: The name of the fallback file.
|
||||
// next: The next handler to serve.
|
||||
func FallbackFile(fs fs.FS, name string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
bw := &buffedResponseWriter{
|
||||
w,
|
||||
0,
|
||||
bytes.NewBuffer([]byte{}),
|
||||
}
|
||||
next.ServeHTTP(bw, r)
|
||||
|
||||
if bw.statusCode == http.StatusNotFound {
|
||||
bw.Clear()
|
||||
|
||||
http.ServeFileFS(bw, r, fs, name)
|
||||
bw.WriteHeader(http.StatusOK)
|
||||
bw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
}
|
||||
bw.Apply()
|
||||
})
|
||||
}
|
||||
|
||||
type buffedResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
buff *bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *buffedResponseWriter) WriteHeader(code int) {
|
||||
b.statusCode = code
|
||||
}
|
||||
|
||||
func (b *buffedResponseWriter) Write(p []byte) (int, error) {
|
||||
return b.buff.Write(p)
|
||||
}
|
||||
|
||||
func (b *buffedResponseWriter) Clear() {
|
||||
b.buff.Reset()
|
||||
}
|
||||
|
||||
func (b *buffedResponseWriter) Apply() {
|
||||
_, err := b.ResponseWriter.Write(b.buff.Bytes())
|
||||
if err != nil {
|
||||
log.Println("Error while applying buffedResponseWriter: ", err)
|
||||
b.ResponseWriter.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
if b.statusCode != 200 {
|
||||
b.ResponseWriter.WriteHeader(b.statusCode)
|
||||
}
|
||||
}
|
11
web/package-lock.json
generated
11
web/package-lock.json
generated
@ -20,6 +20,7 @@
|
||||
"marked": "^15.0.8",
|
||||
"postcss": "^8.5.3",
|
||||
"rxjs": "~7.8.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
@ -13581,6 +13582,16 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz",
|
||||
"integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.3.tgz",
|
||||
|
@ -22,6 +22,7 @@
|
||||
"marked": "^15.0.8",
|
||||
"postcss": "^8.5.3",
|
||||
"rxjs": "~7.8.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
|
@ -6,6 +6,7 @@
|
||||
</a>
|
||||
<nav class="flex gap-5">
|
||||
<a routerLink="/dashboard" *ngIf="loggedIn()">posts</a>
|
||||
<a routerLink="/dashboard/account" *ngIf="loggedIn()">account</a>
|
||||
<button *ngIf="!loggedIn()" (click)="toggleLogin()">login</button>
|
||||
<button *ngIf="loggedIn()" (click)="logOut()">logout</button>
|
||||
</nav>
|
||||
|
@ -24,6 +24,8 @@ describe('AppComponent', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend');
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain(
|
||||
'Hello, frontend',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -3,9 +3,9 @@ import { HomeComponent } from './routes/home/home.component';
|
||||
import { PostComponent } from './routes/post/post.component';
|
||||
import { DashboardComponent } from './routes/dashboard/dashboard.component';
|
||||
import { LoggedInGuard } from './shared/guards/logged-in.guard';
|
||||
import { PostEditorComponent } from './components/post-editor/post-editor.component';
|
||||
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';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: HomeComponent },
|
||||
@ -22,8 +22,11 @@ export const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: DashboardComponent,
|
||||
canActivate: [LoggedInGuard],
|
||||
children: [
|
||||
{ path: 'account', component: AccountComponent },
|
||||
{ path: '', component: DashboardComponent },
|
||||
],
|
||||
data: { requiresAuth: true },
|
||||
},
|
||||
{ path: 'tst', component: PostEditorComponent },
|
||||
];
|
||||
|
8
web/src/app/components/button/button.component.html
Normal file
8
web/src/app/components/button/button.component.html
Normal file
@ -0,0 +1,8 @@
|
||||
<button
|
||||
(click)="(click)"
|
||||
[disabled]="disabled"
|
||||
[class]="getStyle()"
|
||||
[routerLink]="link ? link : null"
|
||||
>
|
||||
<ng-content />
|
||||
</button>
|
23
web/src/app/components/button/button.component.spec.ts
Normal file
23
web/src/app/components/button/button.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ButtonComponent } from './button.component';
|
||||
|
||||
describe('ButtonComponent', () => {
|
||||
let component: ButtonComponent;
|
||||
let fixture: ComponentFixture<ButtonComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ButtonComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
33
web/src/app/components/button/button.component.ts
Normal file
33
web/src/app/components/button/button.component.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
@Component({
|
||||
selector: 'app-button',
|
||||
imports: [NgIf, RouterLink],
|
||||
templateUrl: './button.component.html',
|
||||
})
|
||||
export class ButtonComponent {
|
||||
@Output() click = new EventEmitter<Event>();
|
||||
@Input() disabled: boolean = false;
|
||||
@Input() color: 'yellow' | 'orange' = 'yellow';
|
||||
@Input() link: null | string = null;
|
||||
|
||||
getStyle() {
|
||||
let col = 'a';
|
||||
|
||||
switch (this.color) {
|
||||
case 'yellow':
|
||||
col = 'bg-amber-400';
|
||||
break;
|
||||
case 'orange':
|
||||
col = 'bg-orange-400';
|
||||
}
|
||||
|
||||
return twMerge(
|
||||
'p-1 px-5 transition-colors rounded-full hover:bg-amber-500 ',
|
||||
col,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<form [formGroup]="form" (ngSubmit)="submit()" class="grid grid-cols-3 gap-5">
|
||||
<label>Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="password"
|
||||
class="p-1 border-2 rounded-md col-span-2"
|
||||
/>
|
||||
@if (form.invalid && this.form.touched) {
|
||||
<p class="col-span-3 text-red-600 italic">
|
||||
Password must be at least 6 chars.
|
||||
</p>
|
||||
}
|
||||
<button
|
||||
type="submit"
|
||||
required
|
||||
[class.bg-gray-500]="form.invalid"
|
||||
class="col-start-3 col-span-1 bg-blue-300 p-2 rounded-md drop-shadow-md hover:drop-shadow-xl active:bg-blue-600 disabled:bg-gray-500"
|
||||
>
|
||||
Change Password
|
||||
</button>
|
||||
</form>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChangePasswordComponent } from './change-password.component';
|
||||
|
||||
describe('ChangePasswordComponent', () => {
|
||||
let component: ChangePasswordComponent;
|
||||
let fixture: ComponentFixture<ChangePasswordComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ChangePasswordComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChangePasswordComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
import { Component, EventEmitter, inject, Output, output } from '@angular/core';
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { AuthService } from '../../shared/services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-password',
|
||||
imports: [ReactiveFormsModule],
|
||||
templateUrl: './change-password.component.html',
|
||||
})
|
||||
export class ChangePasswordComponent {
|
||||
@Output() done = new EventEmitter();
|
||||
private auth = inject(AuthService);
|
||||
form = new FormGroup({
|
||||
password: new FormControl('', [
|
||||
Validators.required,
|
||||
Validators.minLength(6),
|
||||
]),
|
||||
});
|
||||
|
||||
submit() {
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.auth
|
||||
.changePassword(this.form.controls.password.value!)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
this.done.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<div
|
||||
*ngIf="open === true"
|
||||
class="fixed flex top-0 justify-center items-center w-screen h-screen z-50 backdrop-blur-md bg-black/25"
|
||||
class="fixed flex top-0 right-0 left-0 justify-center items-center w-screen h-screen z-50 backdrop-blur-md bg-black/25"
|
||||
>
|
||||
<div class="p-10 bg-white drop-shadow-md rounded-md">
|
||||
<button (click)="toggleOpen()" class="absolute top-0 right-1 p-3">X</button>
|
||||
|
@ -0,0 +1,9 @@
|
||||
<h1 class="text-2xl">Actions</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5 mt-5">
|
||||
<app-button (click)="auth.logout()">logout</app-button>
|
||||
<app-button (click)="changePW()">Change Password</app-button>
|
||||
</div>
|
||||
|
||||
<app-modal [(open)]="changePWOpen">
|
||||
<app-change-password (done)="changePWOpen = false" />
|
||||
</app-modal>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccountComponent } from './account.component';
|
||||
|
||||
describe('AccountComponent', () => {
|
||||
let component: AccountComponent;
|
||||
let fixture: ComponentFixture<AccountComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AccountComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AccountComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
19
web/src/app/routes/dashboard/account/account.component.ts
Normal file
19
web/src/app/routes/dashboard/account/account.component.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { AuthService } from '../../../shared/services/auth.service';
|
||||
import { ButtonComponent } from '../../../components/button/button.component';
|
||||
import { ModalComponent } from '../../../components/modal/modal.component';
|
||||
import { ChangePasswordComponent } from '../../../components/change-password/change-password.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
imports: [ButtonComponent, ModalComponent, ChangePasswordComponent],
|
||||
templateUrl: './account.component.html',
|
||||
})
|
||||
export class AccountComponent {
|
||||
auth = inject(AuthService);
|
||||
changePWOpen: boolean = false;
|
||||
|
||||
changePW() {
|
||||
this.changePWOpen = true;
|
||||
}
|
||||
}
|
@ -18,17 +18,18 @@
|
||||
<p class="col-start-1 col-span-4 row-start-2 row-span-2">
|
||||
<strong>TL;DR; </strong>{{ post.tldr }}
|
||||
</p>
|
||||
<a
|
||||
[routerLink]="`/post/${post.id}/edit`"
|
||||
class="col-start-5 row-start-1 bg-amber-400 rounded-full p-1 px-5 hover:bg-amber-500 active:bg-amber-600 transition-colors text-center"
|
||||
<app-button
|
||||
[link]="`/post/${post.id}/edit`"
|
||||
class="col-start-5 row-start-1"
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
<button
|
||||
</app-button>
|
||||
<app-button
|
||||
(click)="delete(post.id)"
|
||||
class="col-start-5 row-start-3 bg-orange-400 rounded-full p-1 px-5 hover:bg-amber-500 active:bg-orange-600 transition-colors"
|
||||
class="col-start-5 row-start-3"
|
||||
color="orange"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</app-button>
|
||||
</article>
|
||||
</section>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Component, effect, inject } from '@angular/core';
|
||||
import { PostEditorComponent } from '../../components/post-editor/post-editor.component';
|
||||
import { NgFor } from '@angular/common';
|
||||
import { PostsService } from '../../shared/services/posts.service';
|
||||
import { RouterLink, RouterOutlet } from '@angular/router';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { ButtonComponent } from '../../components/button/button.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
imports: [NgFor, RouterLink],
|
||||
imports: [NgFor, RouterLink, ButtonComponent],
|
||||
standalone: true,
|
||||
templateUrl: './dashboard.component.html',
|
||||
})
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { LoginResponse, User, Claims } from '../interfaces/auth';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ActivatedRouteSnapshot, Router } from '@angular/router';
|
||||
|
||||
const JWT_KEY = 'token';
|
||||
|
||||
@ -16,6 +17,7 @@ const JWT_KEY = 'token';
|
||||
})
|
||||
export class AuthService {
|
||||
private http = inject(HttpClient);
|
||||
private router = inject(Router);
|
||||
jwt: WritableSignal<string | null> = signal(null);
|
||||
claims: WritableSignal<Claims | null> = signal(null);
|
||||
timeout: any | null = null;
|
||||
@ -73,12 +75,35 @@ export class AuthService {
|
||||
|
||||
login(user: User) {
|
||||
this.http
|
||||
.post<LoginResponse>(`${environment.apiRoot}/login`, user)
|
||||
.post<LoginResponse>(`${environment.apiRoot}/auth/login`, user)
|
||||
.subscribe((res) => this.jwt.set(res.token));
|
||||
}
|
||||
|
||||
signup(user: User) {
|
||||
this.http
|
||||
.post<LoginResponse>(`${environment.apiRoot}/auth/signup`, user)
|
||||
.subscribe((res) => this.jwt.set(res.token));
|
||||
}
|
||||
|
||||
changePassword(password: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
this.http
|
||||
.put(`${environment.apiRoot}/auth/password`, { password: password })
|
||||
.subscribe({
|
||||
complete: () => resolve(true),
|
||||
error: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.http.delete(`${environment.apiRoot}/logout`).subscribe(() => {
|
||||
this.http.delete(`${environment.apiRoot}/auth/logout`).subscribe(() => {
|
||||
this.jwt.set(null);
|
||||
|
||||
// move away if protected
|
||||
if (isOnProtectedRoute(this.router.routerState.snapshot.root)) {
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -111,3 +136,11 @@ function extractExpiration(token: string): Date {
|
||||
const jwt = parseJwt(token);
|
||||
return new Date(jwt.exp * 1000);
|
||||
}
|
||||
function isOnProtectedRoute(route: ActivatedRouteSnapshot): boolean {
|
||||
// If this segment has the flag, return true
|
||||
if (route.data && route.data['requiresAuth']) {
|
||||
return true;
|
||||
}
|
||||
// Recursively check children
|
||||
return route.children.some((child) => isOnProtectedRoute(child));
|
||||
}
|
||||
|
Reference in New Issue
Block a user