change password
All checks were successful
Release / publish (push) Successful in 4m9s

This commit is contained in:
u80864958
2025-05-13 14:06:11 +02:00
parent 73ff28347a
commit 30dac9f12f
21 changed files with 288 additions and 16 deletions

2
go.mod
View File

@ -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
View File

@ -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=

View File

@ -48,6 +48,22 @@ 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.
@ -86,7 +102,7 @@ func (s *Service) ChangePassword(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(http.StatusOK)
w.WriteHeader(http.StatusNoContent)
}
// Login handles user login by decoding request body, verifying credentials, and returning a JWT token.

11
web/package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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>

View File

@ -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',
);
});
});

View File

@ -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 },
];

View File

@ -0,0 +1,8 @@
<button
(click)="(click)"
[disabled]="disabled"
[class]="getStyle()"
[routerLink]="link ? link : null"
>
<ng-content />
</button>

View 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();
});
});

View 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,
);
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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();
}
});
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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();
});
});

View 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;
}
}

View File

@ -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>

View File

@ -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',
})

View File

@ -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;
@ -76,9 +78,32 @@ export class AuthService {
.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}/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));
}