From 5c3c0a700dd8e392256b92980aae08b9e143e1ab Mon Sep 17 00:00:00 2001 From: amirhosin6402 Date: Thu, 23 Jul 2026 10:34:07 +0330 Subject: [PATCH 1/2] fix: recover from stale user sessions --- ClientApp/src/app/app.component.spec.ts | 37 +++++++++++++++++++ .../components/newnote/newnote.component.ts | 8 ++-- .../app/environmets/AuthenticationModel.ts | 31 ++++++++++++++-- ClientApp/src/app/services/base.api.ts | 34 +++++++++++++---- .../Controllers/AddNoteController.cs | 13 ++++++- scripts/e2e.ps1 | 24 ++++++++++++ 6 files changed, 132 insertions(+), 15 deletions(-) diff --git a/ClientApp/src/app/app.component.spec.ts b/ClientApp/src/app/app.component.spec.ts index 520887f..93cbb8a 100644 --- a/ClientApp/src/app/app.component.spec.ts +++ b/ClientApp/src/app/app.component.spec.ts @@ -1,7 +1,44 @@ import { ApiBaseSettings } from './environmets/ApiSettings'; +import { AuthenticationModel } from './environmets/AuthenticationModel'; describe('API configuration', () => { it('uses a same-origin API URL', () => { expect(new ApiBaseSettings().baseUrl).toBe('/api'); }); }); + +describe('AuthenticationModel', () => { + const authentication = new AuthenticationModel(); + + afterEach(() => localStorage.clear()); + + it('accepts a token that has not expired', () => { + authentication.Set(createToken(Math.floor(Date.now() / 1000) + 60), 'test-user'); + + expect(authentication.IsUserLogin()).toBeTrue(); + expect(authentication.GetUserName()).toBe('test-user'); + }); + + it('clears an expired token instead of keeping the user in a login loop', () => { + authentication.Set(createToken(Math.floor(Date.now() / 1000) - 60), 'test-user'); + + expect(authentication.IsUserLogin()).toBeFalse(); + expect(authentication.GetToken()).toBeNull(); + expect(authentication.GetUserName()).toBeNull(); + }); + + it('clears malformed tokens', () => { + authentication.Set('not-a-jwt', 'test-user'); + + expect(authentication.IsUserLogin()).toBeFalse(); + expect(authentication.GetToken()).toBeNull(); + }); +}); + +function createToken(exp: number): string { + const payload = btoa(JSON.stringify({ exp })) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + return `header.${payload}.signature`; +} diff --git a/ClientApp/src/app/components/newnote/newnote.component.ts b/ClientApp/src/app/components/newnote/newnote.component.ts index 1cfda46..75c9a18 100644 --- a/ClientApp/src/app/components/newnote/newnote.component.ts +++ b/ClientApp/src/app/components/newnote/newnote.component.ts @@ -94,9 +94,10 @@ export class NewNoteComponent implements OnInit { } }, error:(err)=>{ - if(err.status == 401) + this.isLoading = false; + if(err.status !== 401) { - this.router.navigate(['sign-in']) + this.showErrorMessage(); } console.log(err); } @@ -110,6 +111,7 @@ export class NewNoteComponent implements OnInit { } private showSuccessMessage(): void { + this.isLoading = false; this.snackBar.open('نوت با موفقیت ذخیره شد', 'باشه', { duration: 3000, horizontalPosition: 'start', @@ -133,4 +135,4 @@ export class NewNoteComponent implements OnInit { class CreateNoteSettingDto { topic=''; description =''; -} \ No newline at end of file +} diff --git a/ClientApp/src/app/environmets/AuthenticationModel.ts b/ClientApp/src/app/environmets/AuthenticationModel.ts index 8be9c76..a50a986 100644 --- a/ClientApp/src/app/environmets/AuthenticationModel.ts +++ b/ClientApp/src/app/environmets/AuthenticationModel.ts @@ -34,12 +34,35 @@ export class AuthenticationModel{ } IsUserLogin() { - var token = localStorage.getItem("TokenAuth"); - if(token !== '' && token !== null) - { + const token = this.GetToken(); + if (!token) { + return false; + } + + try { + const payloadPart = token.split('.')[1]; + if (!payloadPart) { + this.LogOut(); + return false; + } + + const normalizedPayload = payloadPart + .replace(/-/g, '+') + .replace(/_/g, '/') + .padEnd(Math.ceil(payloadPart.length / 4) * 4, '='); + const payload = JSON.parse(atob(normalizedPayload)); + const expiresAt = Number(payload.exp) * 1000; + + if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) { + this.LogOut(); + return false; + } + return true; + } catch { + this.LogOut(); + return false; } - return false; } LogOut() { diff --git a/ClientApp/src/app/services/base.api.ts b/ClientApp/src/app/services/base.api.ts index 6e14feb..f9d300a 100644 --- a/ClientApp/src/app/services/base.api.ts +++ b/ClientApp/src/app/services/base.api.ts @@ -1,5 +1,7 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { catchError, throwError } from 'rxjs'; import { ApiBaseSettings } from '../environmets/ApiSettings'; import { AuthenticationModel } from '../environmets/AuthenticationModel'; @@ -11,7 +13,11 @@ export class ApiService { apiSetting = new ApiBaseSettings(); baseUrl = this.apiSetting.baseUrl; - constructor(private http: HttpClient, private authentication:AuthenticationModel) { } + constructor( + private http: HttpClient, + private authentication: AuthenticationModel, + private router: Router + ) { } get(url: string, params?: any) { let headers = new HttpHeaders({ 'Content-Type': 'application/json' }); @@ -27,7 +33,8 @@ export class ApiService { params, headers: headers }; - return this.http.get(this.getUrl(url), httpOptions); + return this.http.get(this.getUrl(url), httpOptions) + .pipe(catchError(error => this.handleError(error))); } post(url: string, params: any, contentType='application/json') { @@ -43,7 +50,8 @@ export class ApiService { 'Bearer ' + this.authentication.GetToken() ); } - return this.http.post(this.getUrl(url), params, {headers:headers}); + return this.http.post(this.getUrl(url), params, {headers:headers}) + .pipe(catchError(error => this.handleError(error))); } put(url: string, params: any) { @@ -55,7 +63,8 @@ export class ApiService { 'Bearer ' + this.authentication.GetToken() ); } - return this.http.put(this.getUrl(url), params, {headers:headers}); + return this.http.put(this.getUrl(url), params, {headers:headers}) + .pipe(catchError(error => this.handleError(error))); } delete(url: string) { @@ -67,7 +76,8 @@ export class ApiService { 'Bearer ' + this.authentication.GetToken() ); } - return this.http.delete(this.getUrl(url), {headers:headers}); + return this.http.delete(this.getUrl(url), {headers:headers}) + .pipe(catchError(error => this.handleError(error))); } getBlob(url: string) { @@ -76,10 +86,20 @@ export class ApiService { headers = headers.set('Authorization', 'Bearer ' + this.authentication.GetToken()); } - return this.http.get(this.getUrl(url), { headers, responseType: 'blob' }); + return this.http.get(this.getUrl(url), { headers, responseType: 'blob' }) + .pipe(catchError(error => this.handleError(error))); } private getUrl(url: string): string { return url.startsWith('/api/') ? url : `${this.baseUrl}${url}`; } + + private handleError(error: HttpErrorResponse) { + if (error.status === 401) { + this.authentication.LogOut(); + void this.router.navigate(['/sign-in']); + } + + return throwError(() => error); + } } diff --git a/NoteBookKeeper.Api/Controllers/AddNoteController.cs b/NoteBookKeeper.Api/Controllers/AddNoteController.cs index efd9a14..9bff457 100644 --- a/NoteBookKeeper.Api/Controllers/AddNoteController.cs +++ b/NoteBookKeeper.Api/Controllers/AddNoteController.cs @@ -20,11 +20,22 @@ public class AddNoteController(NoteKeeperContext context) : ControllerBase [HttpPost("[action]")] public async Task CreateNoteSetting(CreateNoteSettingDto request) { + var userId = GetUserId(); + var userExists = await context.Users.AnyAsync(user => user.Id == userId); + if (!userExists) + { + return Unauthorized(new ProblemDetails + { + Title = "Your session is no longer linked to an active user. Please sign in again.", + Status = StatusCodes.Status401Unauthorized + }); + } + var noteSetting = new NoteSetting { Topic = request.Topic.Trim(), Description = request.Description?.Trim(), - UserId = GetUserId(), + UserId = userId, Uuid = Guid.NewGuid() }; diff --git a/scripts/e2e.ps1 b/scripts/e2e.ps1 index 3aef2c5..4370bf3 100644 --- a/scripts/e2e.ps1 +++ b/scripts/e2e.ps1 @@ -158,6 +158,29 @@ try { if ($_.Exception.Response.StatusCode.value__ -ne 404) { throw } } + $deleteStaleUserSql = "DO `$`$ BEGIN EXECUTE format('DELETE FROM %I WHERE %I = %L', 'Users', 'NormalizedUserName', '$($secondUser.ToUpperInvariant())'); END `$`$;" + & (Join-Path $postgresBin 'psql.exe') ` + -h 127.0.0.1 ` + -p $PostgresPort ` + -U notekeeper ` + -d notekeeper ` + -c $deleteStaleUserSql | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Could not prepare the stale-session check.' } + + try { + Invoke-WebRequest ` + "$baseUrl/api/AddNote/CreateNoteSetting" ` + -Method Post ` + -Headers $otherHeaders ` + -ContentType 'application/json' ` + -Body $topicBody ` + -UseBasicParsing | Out-Null + throw 'A token for a missing user was accepted.' + } + catch { + if ($_.Exception.Response.StatusCode.value__ -ne 401) { throw } + } + [PSCustomObject]@{ Health = 'Healthy' Angular = 'Served' @@ -166,6 +189,7 @@ try { PublicProfilePrivacy = 'Passed' NoteAndAttachment = 'Passed' OwnershipIsolation = 'Passed' + StaleSessionHandling = 'Passed' } | Format-List } catch { From e66a6f011481b6fac8166c37ca1fb364678bb1e4 Mon Sep 17 00:00:00 2001 From: amirhosin6402 Date: Thu, 23 Jul 2026 10:40:17 +0330 Subject: [PATCH 2/2] chore: translate application to English --- ClientApp/src/app/app.component.scss | 19 ++------ .../authentication/login/login.component.html | 13 ++--- .../register/register.component.html | 18 +++---- .../add-note/add-note.component.html | 43 ++++++++--------- .../components/add-note/add-note.component.ts | 13 ++--- .../app/components/home/home.component.html | 17 ++++--- .../app/components/home/home.component.scss | 11 +---- .../components/newnote/newnote.component.html | 24 +++++----- .../components/newnote/newnote.component.scss | 7 +-- .../components/newnote/newnote.component.ts | 6 +-- .../note-detail/note-detail.component.html | 12 ++--- .../note-detail/note-detail.component.scss | 17 +------ .../note-detail/note-detail.component.ts | 12 +++-- .../note-list.component.html | 18 +++---- .../topic-notes/topic-notes.component.html | 12 ++--- .../loading/loadingoverlay.component .ts | 4 +- .../navigation/navigation.component.scss | 1 - .../navigation/navigation.component.ts | 10 ++-- ClientApp/src/index.html | 4 +- .../Controllers/AddNoteController.cs | 6 +-- .../Controllers/UserController.cs | 4 +- NoteBookKeeper.Api/Models/RegisterUserDto.cs | 2 +- NoteBookKeeper.Api/NoteBookKeeper.Api.http | 8 ++-- README.md | 48 +++++++++---------- 24 files changed, 143 insertions(+), 186 deletions(-) diff --git a/ClientApp/src/app/app.component.scss b/ClientApp/src/app/app.component.scss index a4add75..bade6fd 100644 --- a/ClientApp/src/app/app.component.scss +++ b/ClientApp/src/app/app.component.scss @@ -20,24 +20,11 @@ } } -// RTL & Font setup +// Application font * { - font-family: Vazir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + font-family: Roboto, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; } -// Material theme setup .mat-typography { - font-family: Vazir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + font-family: Roboto, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; } - -// Material Components RTL setup -[dir='rtl'] { - .mat-form-field-appearance-outline { - .mat-form-field-outline-start { - border-radius: 0 4px 4px 0; - } - .mat-form-field-outline-end { - border-radius: 4px 0 0 4px; - } - } -} \ No newline at end of file diff --git a/ClientApp/src/app/authentication/login/login.component.html b/ClientApp/src/app/authentication/login/login.component.html index 370f475..59cc56f 100644 --- a/ClientApp/src/app/authentication/login/login.component.html +++ b/ClientApp/src/app/authentication/login/login.component.html @@ -1,23 +1,24 @@ - diff --git a/ClientApp/src/app/authentication/register/register.component.html b/ClientApp/src/app/authentication/register/register.component.html index 0f69e74..fe89b0a 100644 --- a/ClientApp/src/app/authentication/register/register.component.html +++ b/ClientApp/src/app/authentication/register/register.component.html @@ -1,31 +1,31 @@ -
+
- ثبت نام + Create an account
- نام کاربری + Username - ایمیل + Email - رمز عبور + Password - تکرار رمز عبور - + Confirm password +
-
\ No newline at end of file +
diff --git a/ClientApp/src/app/components/add-note/add-note.component.html b/ClientApp/src/app/components/add-note/add-note.component.html index cd2dc6d..97deca5 100644 --- a/ClientApp/src/app/components/add-note/add-note.component.html +++ b/ClientApp/src/app/components/add-note/add-note.component.html @@ -2,25 +2,25 @@
- افزودن جزوه جدید + Create a note
- +
- - -
@@ -28,13 +28,13 @@
- - -
@@ -42,10 +42,10 @@
- -
@@ -53,7 +53,7 @@
-
@@ -64,7 +64,6 @@ class="editor-content" contenteditable="true" (input)="onEditorInput($event)" - dir="rtl" >
@@ -72,13 +71,13 @@ - لینک مرتبط - + Related link + - کلمات کلیدی + Keywords @@ -98,7 +97,7 @@
- +
cloud_upload - برای آپلود فایل کلیک کنید یا فایل را اینجا رها کنید + Click to upload files or drop them here
@@ -124,8 +123,8 @@
-
\ No newline at end of file +
diff --git a/ClientApp/src/app/components/add-note/add-note.component.ts b/ClientApp/src/app/components/add-note/add-note.component.ts index 700ece6..f6fad0f 100644 --- a/ClientApp/src/app/components/add-note/add-note.component.ts +++ b/ClientApp/src/app/components/add-note/add-note.component.ts @@ -76,7 +76,7 @@ export class AddNoteComponent implements OnInit { formatDoc(command: string) { if (command === 'createLink') { - const url = prompt('لطفاً URL را وارد کنید:'); + const url = prompt('Enter a URL:'); if (url) { document.execCommand(command, false, url); } @@ -125,7 +125,7 @@ export class AddNoteComponent implements OnInit { if (this.noteForm.valid) { if(this.searchWords.length == 0) { - this.showErrorMessage('کلمات کلیدی نمیتواند خالی باشد'); + this.showErrorMessage('Add at least one keyword.'); return; } this.loading = true; @@ -142,7 +142,6 @@ export class AddNoteComponent implements OnInit { formData.append('files', file); }); - // contentType parameter رو حذف کنید this.apiService.post(this.baseUrls.createNoteSettingItem, formData) .subscribe({ next: () => { @@ -159,20 +158,18 @@ export class AddNoteComponent implements OnInit { } private showSuccessMessage(): void { - this.snackBar.open('نوت با موفقیت ذخیره شد', 'باشه', { + this.snackBar.open('Note saved successfully.', 'OK', { duration: 3000, horizontalPosition: 'start', - direction: 'rtl', panelClass: 'success-snackbar' }); this.router.navigate(['note-list']) } - private showErrorMessage(errMsg:string='خطا در ذخیره نوت'): void { - this.snackBar.open(errMsg, 'تلاش مجدد', { + private showErrorMessage(errMsg:string='Could not save the note.'): void { + this.snackBar.open(errMsg, 'Try again', { duration: 5000, horizontalPosition: 'start', - direction: 'rtl', panelClass: 'error-snackbar' }); } diff --git a/ClientApp/src/app/components/home/home.component.html b/ClientApp/src/app/components/home/home.component.html index 8525fcd..5335aa0 100644 --- a/ClientApp/src/app/components/home/home.component.html +++ b/ClientApp/src/app/components/home/home.component.html @@ -7,24 +7,23 @@
-

نوت هاتو بزار جا ما

-

با قابلیت جست و جو توی جزوت میتونی

-

دنیارو بدست بیاری

+

Keep every idea within reach

+

Organize your notes and find anything in seconds.

+

Your knowledge, always ready when you need it.

-

به نوت کیپر خوش اومدی

+

Welcome to NoteKeeper

- در اینجا میتونی نوت هاتو نگه داری و توشون جست و جو کنی، فقط کافیه - از منو نوت جدید رو انتخاب کنی + Create categories, save detailed notes, attach files, and search your content whenever you need it.

-
\ No newline at end of file + diff --git a/ClientApp/src/app/components/home/home.component.scss b/ClientApp/src/app/components/home/home.component.scss index 72dd2e4..44181f6 100644 --- a/ClientApp/src/app/components/home/home.component.scss +++ b/ClientApp/src/app/components/home/home.component.scss @@ -4,7 +4,6 @@ position: relative; overflow: hidden; background: #f8f9fa; - direction: rtl; } .background-pattern { @@ -140,12 +139,6 @@ } } - // Add Persian/Arabic font support - @font-face { - font-family: 'Vazir'; - src: url('../../../assets/Vazir.woff2') format('woff2'); - } - :host { - font-family: 'Vazir', system-ui, -apple-system, sans-serif; - } \ No newline at end of file + font-family: Roboto, system-ui, -apple-system, sans-serif; + } diff --git a/ClientApp/src/app/components/newnote/newnote.component.html b/ClientApp/src/app/components/newnote/newnote.component.html index bfe3199..a6e5e37 100644 --- a/ClientApp/src/app/components/newnote/newnote.component.html +++ b/ClientApp/src/app/components/newnote/newnote.component.html @@ -2,8 +2,8 @@
- افزودن موضوع نوت - لطفاً اطلاعات نوت خود را وارد کنید + Create a category + Enter a name and an optional description. @@ -13,23 +13,23 @@ autocomplete="off"> - موضوع + Category name + placeholder="Enter a category name"> - موضوع اجباری است + A category name is required - موضوع باید حداقل ۳ حرف باشد + The category name must contain at least 3 characters - توضیحات + Description