From 76d1375f19d8d96f390bdf5df8e9a3211ac7391e Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Sun, 4 Feb 2024 16:17:55 +0100 Subject: [PATCH 01/16] Start try-catch earlier > db calls in the try block --- pollor.Server/Controllers/AuthController.cs | 88 ++++++++++++--------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/pollor.Server/Controllers/AuthController.cs b/pollor.Server/Controllers/AuthController.cs index 9bf2bde..d2c0e8f 100644 --- a/pollor.Server/Controllers/AuthController.cs +++ b/pollor.Server/Controllers/AuthController.cs @@ -37,27 +37,28 @@ public IActionResult Register([FromBody] RegisterModel registerUser) return BadRequest(new { message = "Password must be longer than 8 characters." }); } - bool isUsernameAvailable = new PollorDbContext().UserAuthModel.Where(u => u.username!.ToLower().Equals(registerUser.username!.ToLower())).IsNullOrEmpty(); - if (isUsernameAvailable == false) { - return BadRequest(new { message = "Username is already taken, please login or use another username." }); - } + try + { - bool isEmailAvailable = new PollorDbContext().UserAuthModel.Where(u => u.emailaddress!.ToLower().Equals(registerUser.emailaddress!.ToLower())).IsNullOrEmpty(); - if (isEmailAvailable == false) { - return BadRequest(new { message = "Emailaddress is already taken, please login or use another emailaddress." }); - } + bool isUsernameAvailable = new PollorDbContext().UserAuthModel.Where(u => u.username!.ToLower().Equals(registerUser.username!.ToLower())).IsNullOrEmpty(); + if (isUsernameAvailable == false) { + return BadRequest(new { message = "Username is already taken, please login or use another username." }); + } - var hasher = new PasswordHasher(); - var hashedPass = hasher.HashPassword(registerUser, registerUser.password!); - UserAuthModel tempUser = new UserAuthModel() { - username = registerUser.username, - password = hashedPass, - emailaddress = registerUser.emailaddress, - created_at = DateTime.Now, - }; + bool isEmailAvailable = new PollorDbContext().UserAuthModel.Where(u => u.emailaddress!.ToLower().Equals(registerUser.emailaddress!.ToLower())).IsNullOrEmpty(); + if (isEmailAvailable == false) { + return BadRequest(new { message = "Emailaddress is already taken, please login or use another emailaddress." }); + } + + var hasher = new PasswordHasher(); + var hashedPass = hasher.HashPassword(registerUser, registerUser.password!); + UserAuthModel tempUser = new UserAuthModel() { + username = registerUser.username, + password = hashedPass, + emailaddress = registerUser.emailaddress, + created_at = DateTime.Now, + }; - try - { using (var context = new PollorDbContext()) { // Create new user @@ -96,33 +97,42 @@ public IActionResult Login([FromBody] LoginModel loginUser) return BadRequest(new { message = "Invalid client request" }); } - var authUser = new PollorDbContext().UserAuthModel.Where(u => u.username!.ToLower().Equals(loginUser.username!.ToLower())).FirstOrDefault(); - if (authUser == null) { - return Unauthorized(new { message = "Username or password is wrong!" }); - } - - var hasher = new PasswordHasher(); - PasswordVerificationResult passwordIsOk = hasher.VerifyHashedPassword(loginUser, authUser.password!, loginUser.password!); + try + { + var authUser = new PollorDbContext().UserAuthModel.Where(u => u.username!.ToLower().Equals(loginUser.username!.ToLower())).FirstOrDefault(); + if (authUser == null) + { + return Unauthorized(new { message = "Username or password is wrong!" }); + } - if (passwordIsOk == PasswordVerificationResult.Failed) { - return Unauthorized(new { message = "Username or password is wrong!" }); - } + var hasher = new PasswordHasher(); + PasswordVerificationResult passwordIsOk = hasher.VerifyHashedPassword(loginUser, authUser.password!, loginUser.password!); - if (authUser.username == loginUser.username && (passwordIsOk == PasswordVerificationResult.Success || passwordIsOk == PasswordVerificationResult.SuccessRehashNeeded)) - { - if (passwordIsOk == PasswordVerificationResult.SuccessRehashNeeded) { - // rehash password and save to DB - _logger.LogError("Rehash password and save to DB"); + if (passwordIsOk == PasswordVerificationResult.Failed) + { + return Unauthorized(new { message = "Username or password is wrong!" }); } - int tokenLongerValid = (bool)loginUser.tokenLongerValid ? 14 : 1;// true = 14, false = 1 - var currentUser = new PollorDbContext().Users.Where(u => u.username!.ToLower().Equals(authUser.username!.ToLower())).FirstOrDefault(); - var tokenOptions = GetJwtTokenOptions(tokenLongerValid, currentUser!); - var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions); + if (authUser.username == loginUser.username && (passwordIsOk == PasswordVerificationResult.Success || passwordIsOk == PasswordVerificationResult.SuccessRehashNeeded)) + { + if (passwordIsOk == PasswordVerificationResult.SuccessRehashNeeded) + { + // rehash password and save to DB + _logger.LogError("Rehash password and save to DB"); + } - return Ok(new AuthenticatedResponse { token = tokenString, user = currentUser}); - } + int tokenLongerValid = (bool)loginUser.tokenLongerValid ? 14 : 1;// true = 14, false = 1 + var currentUser = new PollorDbContext().Users.Where(u => u.username!.ToLower().Equals(authUser.username!.ToLower())).FirstOrDefault(); + var tokenOptions = GetJwtTokenOptions(tokenLongerValid, currentUser!); + var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions); + return Ok(new AuthenticatedResponse { token = tokenString, user = currentUser }); + } + } catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return Unauthorized(new { message = ex.Message }); + } return Unauthorized(new { message = "something went wrong" } ); } From 4d3c30471b08e551885c3c77858a47a30e076cfb Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Sun, 4 Feb 2024 16:18:50 +0100 Subject: [PATCH 02/16] Update styling and header --- pollor.client/angular.json | 9 +- .../src/app/header/header.component.html | 97 +++++++++++++++++-- .../app/user-login/user-login.component.html | 11 ++- .../user-register/user-register.component.ts | 9 +- pollor.client/src/styles.css | 5 + 5 files changed, 110 insertions(+), 21 deletions(-) diff --git a/pollor.client/angular.json b/pollor.client/angular.json index b307ff4..472572c 100644 --- a/pollor.client/angular.json +++ b/pollor.client/angular.json @@ -34,10 +34,9 @@ "src/assets" ], "styles": [ - "src/styles.css", "./node_modules/bootstrap/dist/css/bootstrap.min.css", - "./node_modules/bootstrap-icons/font/bootstrap-icons.css" - + "./node_modules/bootstrap-icons/font/bootstrap-icons.css", + "src/styles.css" ], "scripts": [ "./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" @@ -97,9 +96,9 @@ "src/assets" ], "styles": [ - "src/styles.css", "node_modules/bootstrap/dist/css/bootstrap.min.css", - "./node_modules/bootstrap-icons/font/bootstrap-icons.css" + "./node_modules/bootstrap-icons/font/bootstrap-icons.css", + "src/styles.css" ], "scripts": [ "./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" diff --git a/pollor.client/src/app/header/header.component.html b/pollor.client/src/app/header/header.component.html index 69a57e8..b979181 100644 --- a/pollor.client/src/app/header/header.component.html +++ b/pollor.client/src/app/header/header.component.html @@ -30,16 +30,83 @@

User actions diff --git a/pollor.client/src/app/user-login/user-login.component.html b/pollor.client/src/app/user-login/user-login.component.html index 6978025..afd6163 100644 --- a/pollor.client/src/app/user-login/user-login.component.html +++ b/pollor.client/src/app/user-login/user-login.component.html @@ -43,7 +43,8 @@

Login to POLLOR

-
+ +
@@ -52,7 +53,13 @@
Login to POLLOR
Loading...
+
+ +
-
{{ loginError }}
+
{{ loginError }}
diff --git a/pollor.client/src/app/user-register/user-register.component.ts b/pollor.client/src/app/user-register/user-register.component.ts index e384aec..514f040 100644 --- a/pollor.client/src/app/user-register/user-register.component.ts +++ b/pollor.client/src/app/user-register/user-register.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { AuthService } from '../_auth/auth.service'; -import { AbstractControl, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { AbstractControl, AbstractControlOptions, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { finalize } from 'rxjs/operators'; import { AlertMessage } from '../alert-message/alert-message'; import { ApiService } from '../_api/api.service'; @@ -28,9 +28,10 @@ export class UserRegisterComponent { password: new FormControl(null, [Validators.required, Validators.minLength(8)]), confirmPassword: new FormControl(null, [Validators.required, Validators.minLength(8)]) }, - { - validator: this.ConfirmedValidator('password', 'confirmPassword'), - }); + { + validator: this.ConfirmedValidator('password', 'confirmPassword') + } as AbstractControlOptions + ); if (this.authService.isAuthenticated()) { this.validateUserAndRedirectToProfile(); // validate and navigate to role profile page diff --git a/pollor.client/src/styles.css b/pollor.client/src/styles.css index 7ad8f7c..e1ba89a 100644 --- a/pollor.client/src/styles.css +++ b/pollor.client/src/styles.css @@ -8,3 +8,8 @@ html { table { margin: 0 auto; } + +.card-header { + background-color: seagreen; + color: white; +} From a46534a8b0ca6ffa80f80246f798565ff712bfa8 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Mon, 5 Feb 2024 05:25:18 +0100 Subject: [PATCH 03/16] Add create-poll with DateTime picker. Picker works almost perfect.. --- .../src/app/_interfaces/answers.interface.ts | 14 +- .../src/app/_interfaces/polls.interface.ts | 12 +- .../src/app/_interfaces/user.interface.ts | 4 +- .../src/app/_interfaces/votes.interface.ts | 13 +- pollor.client/src/app/app-routing.module.ts | 11 ++ pollor.client/src/app/app.module.ts | 6 +- .../app/create-poll/create-poll.component.css | 12 ++ .../create-poll/create-poll.component.html | 98 ++++++++++ .../create-poll/create-poll.component.spec.ts | 23 +++ .../app/create-poll/create-poll.component.ts | 145 +++++++++++++++ .../date-time-picker.component.css | 0 .../date-time-picker.component.html | 56 ++++++ .../date-time-picker.component.spec.ts | 23 +++ .../date-time-picker.component.ts | 173 ++++++++++++++++++ .../app/date-time-picker/date-time.model.ts | 90 +++++++++ .../src/app/polls/polls.component.ts | 6 +- 16 files changed, 669 insertions(+), 17 deletions(-) create mode 100644 pollor.client/src/app/create-poll/create-poll.component.css create mode 100644 pollor.client/src/app/create-poll/create-poll.component.html create mode 100644 pollor.client/src/app/create-poll/create-poll.component.spec.ts create mode 100644 pollor.client/src/app/create-poll/create-poll.component.ts create mode 100644 pollor.client/src/app/date-time-picker/date-time-picker.component.css create mode 100644 pollor.client/src/app/date-time-picker/date-time-picker.component.html create mode 100644 pollor.client/src/app/date-time-picker/date-time-picker.component.spec.ts create mode 100644 pollor.client/src/app/date-time-picker/date-time-picker.component.ts create mode 100644 pollor.client/src/app/date-time-picker/date-time.model.ts diff --git a/pollor.client/src/app/_interfaces/answers.interface.ts b/pollor.client/src/app/_interfaces/answers.interface.ts index 393fbed..dc6b1b3 100644 --- a/pollor.client/src/app/_interfaces/answers.interface.ts +++ b/pollor.client/src/app/_interfaces/answers.interface.ts @@ -1,10 +1,14 @@ -import { IPolls } from "./polls.interface"; -import { IVotes } from "./votes.interface"; +import { IPoll } from "./polls.interface"; +import { IVote } from "./votes.interface"; -export interface IAnswers{ +export interface IAnswer { id: string; - poll_id: IPolls; + poll_id: IPoll; poll_answer: string; created_at: Date; - votes: IVotes[]; + votes: IVote[]; +} + +export interface ICreateAnswer { + poll_answer: string; } diff --git a/pollor.client/src/app/_interfaces/polls.interface.ts b/pollor.client/src/app/_interfaces/polls.interface.ts index 357f491..e028ec6 100644 --- a/pollor.client/src/app/_interfaces/polls.interface.ts +++ b/pollor.client/src/app/_interfaces/polls.interface.ts @@ -1,10 +1,16 @@ -import { IAnswers } from "./answers.interface"; +import { IAnswer, ICreateAnswer } from "./answers.interface"; -export interface IPolls { +export interface IPoll { id: string; user_id: number; question: number; ending_date: Date; created_at: Date; - answers: IAnswers[]; + answers: IAnswer[]; +} + +export interface ICreatePoll { + question: number; + ending_date: Date; + answers: ICreateAnswer[]; } diff --git a/pollor.client/src/app/_interfaces/user.interface.ts b/pollor.client/src/app/_interfaces/user.interface.ts index e8c4d4c..527e026 100644 --- a/pollor.client/src/app/_interfaces/user.interface.ts +++ b/pollor.client/src/app/_interfaces/user.interface.ts @@ -1,4 +1,4 @@ -import { IPolls } from "./polls.interface"; +import { IPoll } from "./polls.interface"; export interface IUser { id: number; @@ -8,7 +8,7 @@ export interface IUser { last_name: string; role: string; created_at: Date; - polls: IPolls[]; + polls: IPoll[]; } export interface IUserChangePassword { diff --git a/pollor.client/src/app/_interfaces/votes.interface.ts b/pollor.client/src/app/_interfaces/votes.interface.ts index 13d412b..751d30c 100644 --- a/pollor.client/src/app/_interfaces/votes.interface.ts +++ b/pollor.client/src/app/_interfaces/votes.interface.ts @@ -1,11 +1,18 @@ -import { IAnswers } from "./answers.interface"; +import { IAnswer } from "./answers.interface"; -export interface IVotes { +export interface IVote { id: string; - answer_id: IAnswers; + answer_id: IAnswer; ipv4_address: Blob; ipv6_address: Blob; mac_address: Blob; voted_at: Date; created_at: Date; } + +export interface ICreateVote { + answer_id: IAnswer; + ipv4_address: Blob; + ipv6_address: Blob; + mac_address: Blob; +} diff --git a/pollor.client/src/app/app-routing.module.ts b/pollor.client/src/app/app-routing.module.ts index 571669a..6846a2d 100644 --- a/pollor.client/src/app/app-routing.module.ts +++ b/pollor.client/src/app/app-routing.module.ts @@ -10,6 +10,8 @@ import { UserRegisterComponent } from './user-register/user-register.component'; import { UserProfileComponent } from './user-profile/user-profile.component'; import { UserAdminProfileComponent } from './user-admin-profile/user-admin-profile.component'; import { UserLogoutComponent } from './user-logout/user-logout.component'; +import { CreatePollComponent } from './create-poll/create-poll.component'; +import { TestComponent } from './test/test.component'; const routes: Routes = [ { @@ -42,6 +44,15 @@ const routes: Routes = [ path: 'polls', component: PollsComponent }, + { + path: 'create-poll', + component: CreatePollComponent, + canActivate: [() => inject(AuthGuard).canActivate()] + }, + { + path: 'test', + component: TestComponent + }, { path: '**', component: PageNotFoundComponent diff --git a/pollor.client/src/app/app.module.ts b/pollor.client/src/app/app.module.ts index 0b42a25..85d3feb 100644 --- a/pollor.client/src/app/app.module.ts +++ b/pollor.client/src/app/app.module.ts @@ -23,6 +23,8 @@ import { UserRegisterComponent } from './user-register/user-register.component'; import { AlertMessage } from './alert-message/alert-message'; import { UserAdminProfileComponent } from './user-admin-profile/user-admin-profile.component'; import { UserLogoutComponent } from './user-logout/user-logout.component'; +import { CreatePollComponent } from './create-poll/create-poll.component'; +import { DateTimePickerComponent } from './date-time-picker/date-time-picker.component'; @NgModule({ declarations: [ @@ -36,7 +38,9 @@ import { UserLogoutComponent } from './user-logout/user-logout.component'; UserLoginComponent, UserRegisterComponent, UserAdminProfileComponent, - UserLogoutComponent + UserLogoutComponent, + CreatePollComponent, + DateTimePickerComponent ], imports: [ BrowserModule, diff --git a/pollor.client/src/app/create-poll/create-poll.component.css b/pollor.client/src/app/create-poll/create-poll.component.css new file mode 100644 index 0000000..f835795 --- /dev/null +++ b/pollor.client/src/app/create-poll/create-poll.component.css @@ -0,0 +1,12 @@ +.card { + max-width: 900px; + margin: 0 auto; +} + +.poll-answers { + text-align: right; +} + +.poll-closing-date { + text-align: left; +} diff --git a/pollor.client/src/app/create-poll/create-poll.component.html b/pollor.client/src/app/create-poll/create-poll.component.html new file mode 100644 index 0000000..0989acc --- /dev/null +++ b/pollor.client/src/app/create-poll/create-poll.component.html @@ -0,0 +1,98 @@ +
+ +

+ Create here your new poll. Afterward you can edit the poll in My Polls. Please be sure to at least add two answers. Otherwise what is there to choose, right? +

+ +
+
+
Create a poll
+
+
+
+ + +
+ + + +
+
+ Poll question is required. +
+
+ Poll question cannot be longer than 512 +
+
+
+ + +
+
+
+
+
+
+
+ + +
+ +
+
+ +
+
+
+ Poll answer is required. +
+
+ Poll answer cannot be longer than 256 charachters. +
+
+
+
+
+
+
+ + +
+ Make sure to enter at least 2 unique answers +
+
+ + +
+ + + +
+ Poll Closing Date is required. +
+
+ Poll Closing Date must be a valid date. +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ {{ createPollError }} +
+
+
diff --git a/pollor.client/src/app/create-poll/create-poll.component.spec.ts b/pollor.client/src/app/create-poll/create-poll.component.spec.ts new file mode 100644 index 0000000..7a4c55a --- /dev/null +++ b/pollor.client/src/app/create-poll/create-poll.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreatePollComponent } from './create-poll.component'; + +describe('CreatePollComponent', () => { + let component: CreatePollComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CreatePollComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CreatePollComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pollor.client/src/app/create-poll/create-poll.component.ts b/pollor.client/src/app/create-poll/create-poll.component.ts new file mode 100644 index 0000000..4d94a68 --- /dev/null +++ b/pollor.client/src/app/create-poll/create-poll.component.ts @@ -0,0 +1,145 @@ +import { Component } from '@angular/core'; +import { AbstractControl, AbstractControlOptions, FormArray, FormBuilder, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms'; +import { ApiService } from '../_api/api.service'; +import { finalize } from 'rxjs'; +import { AlertMessage } from '../alert-message/alert-message'; +import { ICreatePoll } from '../_interfaces/polls.interface'; +import { ICreateAnswer } from "../_interfaces/answers.interface"; + +@Component({ + selector: 'app-create-poll', + templateUrl: './create-poll.component.html', + styleUrl: './create-poll.component.css' +}) +export class CreatePollComponent { + createPollError: string = ''; + loading: boolean = false; + createPollForm!: FormGroup; + + dateModel: Date = new Date(); + stringDateModel: string = new Date().toString(); + + constructor( + private fb: FormBuilder, + private apiService: ApiService, + private alertMessage: AlertMessage + ) { + this.initForm(); + } + + initForm() { + this.createPollForm = this.fb.group({ + pollQuestion: new FormControl(null, [Validators.required, Validators.maxLength(512)]), + pollAnswers: new FormArray([]), + pollClosingDate: new FormControl(new Date(2000, 0), [Validators.required]) + }, + { + validators: [ + this.ExtraAnswerValidation('pollAnswers'), + this.DateTimeValidator('pollClosingDate') + ] + } as AbstractControlOptions + ); + + this.addAnswer(); + this.addAnswer(); + } + + addAnswer() { + const answerForm = this.fb.group({ + answer: new FormControl('', [Validators.required, Validators.maxLength(256)]) + }); + this.pollAnswers.push(answerForm); + } + + deleteAnswer(answerIndex: number) { + this.pollAnswers.removeAt(answerIndex); + } + + get pollQuestion(): AbstractControl { return this.createPollForm.get('pollQuestion')!; } + get pollClosingDate(): AbstractControl { return this.createPollForm.get('pollClosingDate')!; } + get pollAnswers() { + return this.createPollForm.controls['pollAnswers'] as FormArray; + } + + DateTimeValidator(dateTime: string) { + return (formGroup: FormGroup) => { + const control = formGroup.controls[dateTime]; + + if ( + control.errors && + !control.errors.isValid + ) { + return; + } + + const date = new Date(control.value); + const isValid = !isNaN(date.valueOf()); + if (!isValid) { + control.setErrors({ nonValidDate: true }); + } else { + control.setErrors(null); + } + }; + } + + ExtraAnswerValidation(fieldName: string) { + return (formGroup: FormGroup) => { + const answers = formGroup.controls[fieldName]; + + if ( + answers.errors && + !answers.errors.tooShortLength + ) { + return; + } + if (answers.value.length < 2) { + answers.setErrors({ tooShortLength: true }); + } else { + answers.setErrors(null); + } + }; + } + + createPoll(): void { + if (this.createPollForm.valid) { + const newAnswers: ICreateAnswer[] = []; + + this.createPollForm.value.pollAnswers.forEach(function (value: any) { + console.log(value.answer); + newAnswers.push( + { + poll_answer: value.answer + }); + }); + + const newPoll: ICreatePoll = { + question: this.createPollForm.value.pollQuestion, + ending_date: this.createPollForm.value.pollClosingDate, + answers: newAnswers + }; + + this.loading = true; // Start the loading spinner + this.apiService + .post('api/poll', newPoll) + .pipe( + finalize(() => { + this.loading = false; //Stop the loading spinner + }) + ) + .subscribe({ + next: (res: any) => { + this.createPollError = ''; + this.initForm(); + this.alertMessage.addSuccessAlert("Poll has been created", `Poll '${res.question}' has been created!`); + }, + error: (err: any) => { + const msg = ((err.error && err.error.message) ? err.error.message : err.message); + this.createPollError = err.status + ' - ' + msg; + console.error('Poll Creation Error:', err); + this.alertMessage.addErrorAlert("Poll Creation Error", msg); + }, + }); + } + } +} diff --git a/pollor.client/src/app/date-time-picker/date-time-picker.component.css b/pollor.client/src/app/date-time-picker/date-time-picker.component.css new file mode 100644 index 0000000..e69de29 diff --git a/pollor.client/src/app/date-time-picker/date-time-picker.component.html b/pollor.client/src/app/date-time-picker/date-time-picker.component.html new file mode 100644 index 0000000..9ee1502 --- /dev/null +++ b/pollor.client/src/app/date-time-picker/date-time-picker.component.html @@ -0,0 +1,56 @@ +
+ + +
+ +
+
+ + +
+ + +
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+
diff --git a/pollor.client/src/app/date-time-picker/date-time-picker.component.spec.ts b/pollor.client/src/app/date-time-picker/date-time-picker.component.spec.ts new file mode 100644 index 0000000..56069b0 --- /dev/null +++ b/pollor.client/src/app/date-time-picker/date-time-picker.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DateTimePickerComponent } from './date-time-picker.component'; + +describe('DateTimePickerComponent', () => { + let component: DateTimePickerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DateTimePickerComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DateTimePickerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pollor.client/src/app/date-time-picker/date-time-picker.component.ts b/pollor.client/src/app/date-time-picker/date-time-picker.component.ts new file mode 100644 index 0000000..0747ca7 --- /dev/null +++ b/pollor.client/src/app/date-time-picker/date-time-picker.component.ts @@ -0,0 +1,173 @@ +import { + Component, + OnInit, + Input, + forwardRef, + ViewChild, + AfterViewInit, + Injector +} from "@angular/core"; +import { + NgbTimeStruct, + NgbDateStruct, + NgbPopoverConfig, + NgbPopover, + NgbDatepicker +} from "@ng-bootstrap/ng-bootstrap"; +import { + NG_VALUE_ACCESSOR, + ControlValueAccessor, + NgControl +} from "@angular/forms"; +import { DatePipe } from "@angular/common"; +import { DateTimeModel } from "./date-time.model"; +import { noop } from "rxjs"; + +@Component({ + selector: "app-date-time-picker", + templateUrl: "./date-time-picker.component.html", + styleUrls: ["./date-time-picker.component.scss"], + providers: [ + DatePipe, + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DateTimePickerComponent), + multi: true + } + ] +}) +export class DateTimePickerComponent + implements ControlValueAccessor, OnInit, AfterViewInit { + @Input() + dateString: string | null | undefined; + + @Input() + inputDatetimeFormat = "d MMM yyyy - H:mm:ss"; + @Input() + hourStep = 1; + @Input() + minuteStep = 15; + @Input() + secondStep = 30; + @Input() + seconds = true; + + @Input() + disabled = false; + + datetime: DateTimeModel = new DateTimeModel(); + firstTimeAssign = true; + + @ViewChild(NgbPopover, { static: true }) + popover!: NgbPopover; + + onTouched: () => void = noop; + onChange: (_: any) => void = noop; + + ngControl!: NgControl; + + constructor(private config: NgbPopoverConfig, private inj: Injector) { + config.autoClose = "outside"; + config.placement = "auto"; + } + + ngOnInit(): void { + this.ngControl = this.inj.get(NgControl); + } + + ngAfterViewInit(): void { + + } + + writeValue(newModel: string) { + if (newModel) { + this.datetime = Object.assign( + this.datetime, + DateTimeModel.fromLocalString(newModel) + ); + this.dateString = newModel; + this.setDateStringModel(); + } else { + this.datetime = new DateTimeModel(); + } + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + onInputChange($event: any) { + const value = $event.target.value; + const dt = DateTimeModel.fromLocalString(value); + + if (dt) { + this.datetime = dt; + this.setDateStringModel(); + } else if (value.trim() === "") { + this.datetime = new DateTimeModel(); + this.dateString = ""; + this.onChange(this.dateString); + } else { + this.onChange(value); + } + } + + onDateChange($event: string | NgbDateStruct, dp: NgbDatepicker) { + const date = new DateTimeModel($event); + + if (!date) { + this.dateString = this.dateString; + return; + } + + if (!this.datetime) { + this.datetime = date; + } + + this.datetime.year = date.year; + this.datetime.month = date.month; + this.datetime.day = date.day; + + const tempdate = this.datetime.toString(); + + const adjustedDate = new Date(tempdate); + if (this.datetime.timeZoneOffset !== adjustedDate.getTimezoneOffset()) { + this.datetime.timeZoneOffset = adjustedDate.getTimezoneOffset(); + } + + this.setDateStringModel(); + } + + onTimeChange(event: NgbTimeStruct) { + this.datetime.hour = event.hour; + this.datetime.minute = event.minute; + this.datetime.second = event.second; + + this.setDateStringModel(); + } + + setDateStringModel() { + this.dateString = this.datetime.toString(); + + if (!this.firstTimeAssign) { + this.onChange(this.dateString); + } else { + // Skip very first assignment to null done by Angular + if (this.dateString !== null) { + this.firstTimeAssign = false; + } + } + } + + inputBlur($event: any) { + this.onTouched(); + } +} diff --git a/pollor.client/src/app/date-time-picker/date-time.model.ts b/pollor.client/src/app/date-time-picker/date-time.model.ts new file mode 100644 index 0000000..ae81571 --- /dev/null +++ b/pollor.client/src/app/date-time-picker/date-time.model.ts @@ -0,0 +1,90 @@ +import { NgbTimeStruct, NgbDateStruct } from "@ng-bootstrap/ng-bootstrap"; +import { DatePipe } from "@angular/common"; + +export interface NgbDateTimeStruct extends NgbDateStruct, NgbTimeStruct { } + +export class DateTimeModel implements NgbDateTimeStruct { + year!: number; + month!: number; + day!: number; + hour!: number; + minute!: number; + second!: number; + + timeZoneOffset!: number; + + public constructor(init?: Partial) { + Object.assign(this, init); + } + + public static fromLocalString(dateString: string): DateTimeModel | null { + const date = new Date(dateString); + + const isValidDate = !isNaN(date.valueOf()); + + if (!dateString || !isValidDate) { + return null; + } + + return new DateTimeModel({ + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate(), + hour: date.getHours(), + minute: date.getMinutes(), + second: date.getSeconds(), + timeZoneOffset: date.getTimezoneOffset() + }); + } + + private isInteger(value: any): value is number { + return ( + typeof value === "number" && + isFinite(value) && + Math.floor(value) === value + ); + } + + public toString(): string { + if ( + this.isInteger(this.year) && + this.isInteger(this.month) && + this.isInteger(this.day) + ) { + const year = this.year.toString().padStart(2, "0"); + const month = this.month.toString().padStart(2, "0"); + const day = this.day.toString().padStart(2, "0"); + + if (!this.hour) { + this.hour = 0; + } + if (!this.minute) { + this.minute = 0; + } + if (!this.second) { + this.second = 0; + } + if (!this.timeZoneOffset) { + this.timeZoneOffset = new Date().getTimezoneOffset(); + } + + const hour = this.hour.toString().padStart(2, "0"); + const minute = this.minute.toString().padStart(2, "0"); + const second = this.second.toString().padStart(2, "0"); + + const tzo = -this.timeZoneOffset; + const dif = tzo >= 0 ? "+" : "-", + pad = function (num: any) { + const norm = Math.floor(Math.abs(num)); + return (norm < 10 ? "0" : "") + norm; + }; + + const isoString = `${pad(year)}-${pad(month)}-${pad(day)}T${pad( + hour + )}:${pad(minute)}:${pad(second)}${dif}${pad(tzo / 60)}:${pad(tzo % 60)}`; + return isoString; + } + + return null as any; + } +} diff --git a/pollor.client/src/app/polls/polls.component.ts b/pollor.client/src/app/polls/polls.component.ts index 1678c4a..de7172e 100644 --- a/pollor.client/src/app/polls/polls.component.ts +++ b/pollor.client/src/app/polls/polls.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { ApiService } from '../_api/api.service'; -import { IPolls } from '../_interfaces/polls.interface'; +import { IPoll } from '../_interfaces/polls.interface'; import { AlertMessage } from '../alert-message/alert-message'; @@ -10,7 +10,7 @@ import { AlertMessage } from '../alert-message/alert-message'; styleUrl: './polls.component.css' }) export class PollsComponent { - public polls: IPolls[] = []; + public polls: IPoll[] = []; public pollsLoaded: boolean = false; public pollLoadingMsg: string = "Loading polls..."; @@ -26,7 +26,7 @@ export class PollsComponent { } getPolls() { - this.apiService.get('api/polls') + this.apiService.get('api/polls') .subscribe({ next: (response) => { this.polls = response; From d498bf46f41156ab71e0190b8fff26d07213643e Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Mon, 5 Feb 2024 05:26:49 +0100 Subject: [PATCH 04/16] Update poll creation --- pollor.Server/Controllers/PollsController.cs | 23 +++++++++++++++++++- pollor.client/src/app/app-routing.module.ts | 5 ----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/pollor.Server/Controllers/PollsController.cs b/pollor.Server/Controllers/PollsController.cs index 7e61183..02e4b0e 100644 --- a/pollor.Server/Controllers/PollsController.cs +++ b/pollor.Server/Controllers/PollsController.cs @@ -69,7 +69,28 @@ public IActionResult AddPoll(PollModel poll) { try { using (var context = new PollorDbContext()) { - EntityEntry newPoll = context.Polls.Add(poll); + var userClaims = HttpContext.User; + var userId = userClaims.Claims.Where(e => e.Type.Equals("userId")).Select(e => e.Value).SingleOrDefault()!; + + DateTime now = DateTime.Now; + List answers = new List(); + foreach (var a in poll.Answers) { + answers.Add(new AnswerModel() + { + poll_answer = a.poll_answer, + created_at = now + }); + } + + EntityEntry newPoll = context.Polls + .Add(new PollModel() + { + user_id = int.Parse(userId), + question = poll.question, + Answers = answers, + ending_date = poll.ending_date, + created_at = now + }); context.SaveChanges(); if (newPoll == null) { diff --git a/pollor.client/src/app/app-routing.module.ts b/pollor.client/src/app/app-routing.module.ts index 6846a2d..ee745fe 100644 --- a/pollor.client/src/app/app-routing.module.ts +++ b/pollor.client/src/app/app-routing.module.ts @@ -11,7 +11,6 @@ import { UserProfileComponent } from './user-profile/user-profile.component'; import { UserAdminProfileComponent } from './user-admin-profile/user-admin-profile.component'; import { UserLogoutComponent } from './user-logout/user-logout.component'; import { CreatePollComponent } from './create-poll/create-poll.component'; -import { TestComponent } from './test/test.component'; const routes: Routes = [ { @@ -49,10 +48,6 @@ const routes: Routes = [ component: CreatePollComponent, canActivate: [() => inject(AuthGuard).canActivate()] }, - { - path: 'test', - component: TestComponent - }, { path: '**', component: PageNotFoundComponent From 331c450f6d09cb6e14c4547454e1e06ca6505d7c Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Mon, 5 Feb 2024 05:28:55 +0100 Subject: [PATCH 05/16] Update sql migration with datetime2 --- database/migration.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/database/migration.sql b/database/migration.sql index 5380249..9509953 100644 --- a/database/migration.sql +++ b/database/migration.sql @@ -19,7 +19,7 @@ CREATE TABLE [dbo].[users]( [first_name] [nvarchar](64) NULL, [last_name] [nvarchar](64) NULL, [role] [nvarchar](32) NULL DEFAULT 'Basic', - [created_at] [datetime] NOT NULL, + [created_at] [datetime2](7) NOT NULL, CONSTRAINT PK_users PRIMARY KEY NONCLUSTERED (id), CONSTRAINT UC_Users UNIQUE (id,emailaddress,username) ) ON [PRIMARY] @@ -29,8 +29,8 @@ CREATE TABLE [dbo].[polls]( [id] [int] IDENTITY(1,1) NOT NULL, [user_id] [int] NOT NULL, [question] [nvarchar](512) NOT NULL, - [ending_date] [datetime] NOT NULL, - [created_at] [datetime] NOT NULL, + [ending_date] [datetime2](7) NOT NULL, + [created_at] [datetime2](7) NOT NULL, CONSTRAINT PK_polls PRIMARY KEY NONCLUSTERED (id), CONSTRAINT FK_poll_user FOREIGN KEY (user_id) REFERENCES users (id) @@ -45,7 +45,7 @@ CREATE TABLE [dbo].[answers]( [id] [int] IDENTITY(1,1) NOT NULL, [poll_id] [int] NOT NULL, [poll_answer] [nvarchar](256) NOT NULL, - [created_at] [datetime] NOT NULL, + [created_at] [datetime2](7) NOT NULL, CONSTRAINT PK_answers PRIMARY KEY NONCLUSTERED (id), CONSTRAINT FK_answer_poll FOREIGN KEY (poll_id) REFERENCES polls (id) @@ -60,8 +60,8 @@ CREATE TABLE [dbo].[votes]( [ipv4_address] [varbinary](4) NULL, [ipv6_address] [varbinary](16) NULL, [mac_address] [char](12) NULL, - [voted_at] [datetime] NOT NULL, - [created_at] [datetime] NOT NULL, + [voted_at] [datetime2](7) NOT NULL, + [created_at] [datetime2](7) NOT NULL, CONSTRAINT PK_votes PRIMARY KEY NONCLUSTERED (id), CONSTRAINT FK_vote_answer FOREIGN KEY (answer_id) REFERENCES answers (id) From a41bd1a9f1f8e7a29448055014ca8869626bbccf Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Tue, 6 Feb 2024 00:45:31 +0100 Subject: [PATCH 06/16] Update view of /polls, edit account routes --- pollor.client/src/app/_auth/auth.service.ts | 2 +- pollor.client/src/app/app-routing.module.ts | 20 ++++-- .../create-poll/create-poll.component.html | 57 ++++++++-------- .../app/create-poll/create-poll.component.ts | 38 ++++++++--- .../date-time-picker.component.ts | 4 +- .../src/app/header/header.component.html | 4 +- .../src/app/polls/polls.component.html | 49 ++++++++----- .../src/app/polls/polls.component.ts | 68 ++++++++++++++++++- .../user-admin-profile.component.html | 1 - 9 files changed, 179 insertions(+), 64 deletions(-) diff --git a/pollor.client/src/app/_auth/auth.service.ts b/pollor.client/src/app/_auth/auth.service.ts index 44fe389..bee4bf8 100644 --- a/pollor.client/src/app/_auth/auth.service.ts +++ b/pollor.client/src/app/_auth/auth.service.ts @@ -55,7 +55,7 @@ export class AuthService { } public navigateDashboard(): void { - const dashboardRoute = this.getRole().toLowerCase() === 'admin' ? '/account/admin-profile' : '/account/profile'; + const dashboardRoute = this.getRole().toLowerCase() === 'admin' ? '/account/adminpanel' : '/account'; this.router.navigate([dashboardRoute]); console.log(`${this.getRole()} dashboard route`); } diff --git a/pollor.client/src/app/app-routing.module.ts b/pollor.client/src/app/app-routing.module.ts index ee745fe..3836051 100644 --- a/pollor.client/src/app/app-routing.module.ts +++ b/pollor.client/src/app/app-routing.module.ts @@ -18,26 +18,36 @@ const routes: Routes = [ component: HomeComponent }, { - path: 'account/login', + path: 'account', + children: [ + { + path: '', + component: UserProfileComponent, + canActivate: [() => inject(AuthGuard).canActivate()], + }, + { + path: 'login', component: UserLoginComponent }, { - path: 'account/register', + path: 'register', component: UserRegisterComponent }, { - path: 'account/logout', + path: 'logout', component: UserLogoutComponent }, { - path: 'account/profile', + path: 'profile', component: UserProfileComponent, canActivate: [() => inject(AuthGuard).canActivate()] }, { - path: 'account/admin-profile', + path: 'adminpanel', component: UserAdminProfileComponent, canActivate: [() => inject(RoleGuard).canActivate('admin')] + }, + ] }, { path: 'polls', diff --git a/pollor.client/src/app/create-poll/create-poll.component.html b/pollor.client/src/app/create-poll/create-poll.component.html index 0989acc..357770d 100644 --- a/pollor.client/src/app/create-poll/create-poll.component.html +++ b/pollor.client/src/app/create-poll/create-poll.component.html @@ -62,37 +62,40 @@
Create a poll
- -
- - - -
- Poll Closing Date is required. -
-
- Poll Closing Date must be a valid date. -
-
- -
-
- + +
+ + + +
+ Poll Closing Date is required.
-
- +
+ Poll Closing Date must be a valid date.
-
- +
+ Poll Closing Date must be a future date.
- +
+ +
+
+ +
+
+ +
+
+ +
+
{{ createPollError }} -
+
diff --git a/pollor.client/src/app/create-poll/create-poll.component.ts b/pollor.client/src/app/create-poll/create-poll.component.ts index 4d94a68..6c0519e 100644 --- a/pollor.client/src/app/create-poll/create-poll.component.ts +++ b/pollor.client/src/app/create-poll/create-poll.component.ts @@ -19,19 +19,24 @@ export class CreatePollComponent { dateModel: Date = new Date(); stringDateModel: string = new Date().toString(); + initClosingDate: Date = new Date(new Date(2000, 0, 1, 1)); + constructor( private fb: FormBuilder, private apiService: ApiService, private alertMessage: AlertMessage ) { - this.initForm(); + this.createPollForm = this.initForm(); + + this.addAnswer(); + this.addAnswer(); } initForm() { - this.createPollForm = this.fb.group({ + return this.fb.group({ pollQuestion: new FormControl(null, [Validators.required, Validators.maxLength(512)]), pollAnswers: new FormArray([]), - pollClosingDate: new FormControl(new Date(2000, 0), [Validators.required]) + pollClosingDate: new FormControl(this.initClosingDate, [Validators.required]) }, { validators: [ @@ -40,9 +45,6 @@ export class CreatePollComponent { ] } as AbstractControlOptions ); - - this.addAnswer(); - this.addAnswer(); } addAnswer() { @@ -68,13 +70,31 @@ export class CreatePollComponent { if ( control.errors && - !control.errors.isValid + !control.errors.nonValidDate && + !control.errors.closingDateNotSelectedYet && + !control.errors.closingDateIsThePast ) { return; } - const date = new Date(control.value); - const isValid = !isNaN(date.valueOf()); + if (control.value == this.initClosingDate) { + control.setErrors({ closingDateNotSelectedYet: true }); + return; + } else { + control.setErrors(null); + } + + const closingDate = new Date(control.value); + const now = new Date(Date.now()); + + if (closingDate.getTime() < now.getTime()) { + control.setErrors({ closingDateIsThePast: true }); + return; + } else { + control.setErrors(null); + } + + const isValid = !isNaN(closingDate.valueOf()); if (!isValid) { control.setErrors({ nonValidDate: true }); } else { diff --git a/pollor.client/src/app/date-time-picker/date-time-picker.component.ts b/pollor.client/src/app/date-time-picker/date-time-picker.component.ts index 0747ca7..4a635f9 100644 --- a/pollor.client/src/app/date-time-picker/date-time-picker.component.ts +++ b/pollor.client/src/app/date-time-picker/date-time-picker.component.ts @@ -42,7 +42,7 @@ export class DateTimePickerComponent dateString: string | null | undefined; @Input() - inputDatetimeFormat = "d MMM yyyy - H:mm:ss"; + inputDatetimeFormat = "d MMM yyyy - H:mm:00 (z)"; @Input() hourStep = 1; @Input() @@ -50,7 +50,7 @@ export class DateTimePickerComponent @Input() secondStep = 30; @Input() - seconds = true; + seconds = false; @Input() disabled = false; diff --git a/pollor.client/src/app/header/header.component.html b/pollor.client/src/app/header/header.component.html index b979181..0195ef8 100644 --- a/pollor.client/src/app/header/header.component.html +++ b/pollor.client/src/app/header/header.component.html @@ -128,7 +128,7 @@

@@ -142,7 +142,7 @@

diff --git a/pollor.client/src/app/polls/polls.component.html b/pollor.client/src/app/polls/polls.component.html index a263d66..01a9ecc 100644 --- a/pollor.client/src/app/polls/polls.component.html +++ b/pollor.client/src/app/polls/polls.component.html @@ -9,41 +9,58 @@

Polls

- + + - - + + - - - + + - - + + + +
Poll idCreated at Poll Question Poll AnswersPoll ending dateCreated atOpen forGo to poll
{{ poll.id }}{{ poll.question }} - + + + {{ poll.created_at | date: 'd MMM yyyy' }} + + {{ poll.question }} + - + - + - - - - + + + +
Answer id Answer VotesCreated at
{{ answer.id }}{{ answer.poll_answer }}{{ answer.votes.length }}{{ answer.created_at }}{{ answer.poll_answer }}{{ answer.votes.length }}
-
{{ poll.ending_date }}{{ poll.created_at }} + {{ convertMsToDaysAndHours(datetimeToMiliseconds(poll.ending_date) - datetimeToMiliseconds(currentDate) ) }} +
+
+ Closing date: +
+ {{ poll.ending_date | date: 'd-M-yyyy, HH:mm (z)' }} +
+ + + +
diff --git a/pollor.client/src/app/polls/polls.component.ts b/pollor.client/src/app/polls/polls.component.ts index de7172e..e0d1c7f 100644 --- a/pollor.client/src/app/polls/polls.component.ts +++ b/pollor.client/src/app/polls/polls.component.ts @@ -1,8 +1,11 @@ import { Component } from '@angular/core'; +import { formatDate } from '@angular/common'; + import { ApiService } from '../_api/api.service'; import { IPoll } from '../_interfaces/polls.interface'; import { AlertMessage } from '../alert-message/alert-message'; +import { map } from 'rxjs'; @Component({ selector: 'app-polls', @@ -16,6 +19,8 @@ export class PollsComponent { public pollLoadingMsg: string = "Loading polls..."; public pollLoadingColor: string = ""; + public currentDate: Date = new Date(); + constructor( private apiService: ApiService, private alertMessage: AlertMessage @@ -28,7 +33,13 @@ export class PollsComponent { getPolls() { this.apiService.get('api/polls') .subscribe({ - next: (response) => { + next: (response: IPoll[]) => { + + for (let i = 0; i < response.length; i++) { // convert SQL date to a Date to TypeScript understands + response[i].ending_date = new Date(response[i].ending_date) + response[i].created_at = new Date(response[i].created_at) + } + this.polls = response; this.pollsLoaded = true; }, @@ -42,4 +53,59 @@ export class PollsComponent { //complete: () => { } }); } + + datetimeToMiliseconds(datetime: Date): number { + return datetime.getTime(); + } + + convertMsToDaysAndHours(milliseconds: number) { + let seconds = Math.floor(milliseconds / 1000); + let minutes = Math.floor(seconds / 60); + let hours = Math.floor(minutes / 60); + let days = Math.floor(hours / 24); + const years = Math.floor(days / 365); + + seconds = seconds % 60; + minutes = minutes % 60; + hours = hours % 24; + days = days % 365; + + let makeStr: string = ''; + + if (years < 0 || days < 0 || hours < 0 && minutes < 0) { + makeStr = "Poll is closed"; + + if (years < 0) { + makeStr += ` ${this.padTo2Digits(years)} years ago.`; + } else if (days < 0) { + makeStr += ` ${this.padTo2Digits(days)} days ago.`; + } else if (hours < 0) { + makeStr += ` ${this.padTo2Digits(hours)} hours ago.`; + } else if (minutes < 0) { + makeStr += ` ${this.padTo2Digits(minutes)} minutes ago.`; + } + } else { + if (years != 0) { + makeStr += `${years} years, `; + (days == 0 && hours == 0 && minutes == 0) ? makeStr += '.' : makeStr += ', '; + } + if (days != 0) { + makeStr += `${days} days`; + (hours == 0 && minutes == 0) ? makeStr += '.' : makeStr += ', '; + } + if (hours != 0) { + makeStr += `${hours} hours`; + minutes == 0 ? makeStr += '.' : makeStr += ', '; + } + if (minutes != 0) { + makeStr += `and ${minutes} minutes.`; + } + } + + return makeStr; + } + + padTo2Digits(num: number) { + return num.toString().padStart(2, '0'); + } } diff --git a/pollor.client/src/app/user-admin-profile/user-admin-profile.component.html b/pollor.client/src/app/user-admin-profile/user-admin-profile.component.html index a9458c0..4bc7b40 100644 --- a/pollor.client/src/app/user-admin-profile/user-admin-profile.component.html +++ b/pollor.client/src/app/user-admin-profile/user-admin-profile.component.html @@ -2,5 +2,4 @@

Admin Page

- From 0879973f3f845926cdeeeaa7429cb0da4f08e62e Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Tue, 6 Feb 2024 14:59:25 +0100 Subject: [PATCH 07/16] Fix Closing Date --- .../src/app/polls/polls.component.html | 11 ++-- .../src/app/polls/polls.component.ts | 50 +++++++++++-------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/pollor.client/src/app/polls/polls.component.html b/pollor.client/src/app/polls/polls.component.html index 01a9ecc..3bee4f6 100644 --- a/pollor.client/src/app/polls/polls.component.html +++ b/pollor.client/src/app/polls/polls.component.html @@ -49,11 +49,12 @@

Polls

{{ convertMsToDaysAndHours(datetimeToMiliseconds(poll.ending_date) - datetimeToMiliseconds(currentDate) ) }} -
-
- Closing date: -
- {{ poll.ending_date | date: 'd-M-yyyy, HH:mm (z)' }} +
+ +
+
+ {{ poll.ending_date | date: 'd-M-yyyy, HH:mm (z)' }} +
diff --git a/pollor.client/src/app/polls/polls.component.ts b/pollor.client/src/app/polls/polls.component.ts index e0d1c7f..df6c45d 100644 --- a/pollor.client/src/app/polls/polls.component.ts +++ b/pollor.client/src/app/polls/polls.component.ts @@ -20,6 +20,7 @@ export class PollsComponent { public pollLoadingColor: string = ""; public currentDate: Date = new Date(); + public displayStyle: Map = new Map(); constructor( private apiService: ApiService, @@ -34,10 +35,13 @@ export class PollsComponent { this.apiService.get('api/polls') .subscribe({ next: (response: IPoll[]) => { - - for (let i = 0; i < response.length; i++) { // convert SQL date to a Date to TypeScript understands - response[i].ending_date = new Date(response[i].ending_date) - response[i].created_at = new Date(response[i].created_at) + // Edit received data + for (let i = 0; i < response.length; i++) { + // convert SQL date to a Date to TypeScript understands + response[i].ending_date = new Date(response[i].ending_date); + response[i].created_at = new Date(response[i].created_at); + // set info popup to display none; + this.displayStyle.set(response[i].id, "none"); } this.polls = response; @@ -60,10 +64,10 @@ export class PollsComponent { convertMsToDaysAndHours(milliseconds: number) { let seconds = Math.floor(milliseconds / 1000); - let minutes = Math.floor(seconds / 60); - let hours = Math.floor(minutes / 60); - let days = Math.floor(hours / 24); - const years = Math.floor(days / 365); + let minutes = (seconds >= 60 || seconds <= -60) ? Math.floor(seconds / 60) : 0; + let hours = (minutes >= 60 || minutes <= -60) ? Math.floor(minutes / 60) : 0; + let days = (hours >= 24 || hours <= -24) ? Math.floor(hours / 24) : 0; + let years = (days >= 365 || days <= -365)? Math.floor( days / 365) : 0; seconds = seconds % 60; minutes = minutes % 60; @@ -72,19 +76,17 @@ export class PollsComponent { let makeStr: string = ''; - if (years < 0 || days < 0 || hours < 0 && minutes < 0) { + if (years < 0 || days < 0 || hours < 0 || minutes < 0) { makeStr = "Poll is closed"; - if (years < 0) { - makeStr += ` ${this.padTo2Digits(years)} years ago.`; - } else if (days < 0) { - makeStr += ` ${this.padTo2Digits(days)} days ago.`; - } else if (hours < 0) { - makeStr += ` ${this.padTo2Digits(hours)} hours ago.`; - } else if (minutes < 0) { - makeStr += ` ${this.padTo2Digits(minutes)} minutes ago.`; - } + if (years < 0) makeStr += ` ${this.padTo2Digits(years)} years ago.`; + else if (days < 0) makeStr += ` ${this.padTo2Digits(days)} days ago.`; + else if (hours < 0) makeStr += ` ${this.padTo2Digits(hours)} hours ago.`; + else if (minutes < 0) makeStr += ` ${this.padTo2Digits(minutes)} minutes ago.`; + } else { + makeStr = "Poll open for "; + if (years != 0) { makeStr += `${years} years, `; (days == 0 && hours == 0 && minutes == 0) ? makeStr += '.' : makeStr += ', '; @@ -97,9 +99,7 @@ export class PollsComponent { makeStr += `${hours} hours`; minutes == 0 ? makeStr += '.' : makeStr += ', '; } - if (minutes != 0) { - makeStr += `and ${minutes} minutes.`; - } + if (minutes != 0) makeStr += `and ${minutes} minutes.`; } return makeStr; @@ -108,4 +108,12 @@ export class PollsComponent { padTo2Digits(num: number) { return num.toString().padStart(2, '0'); } + + mouseEnter(pollId: string) { + this.displayStyle.set(pollId, "block"); + } + + mouseLeave(pollId: string) { + this.displayStyle.set(pollId, "none"); + } } From 0eefc8a817f7883fe922b3118320b0a63f2d3d06 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Tue, 6 Feb 2024 15:08:57 +0100 Subject: [PATCH 08/16] Add cast vote page --- pollor.client/src/app/app-routing.module.ts | 37 +++++++------ pollor.client/src/app/app.module.ts | 4 +- pollor.client/src/app/poll/poll.component.css | 0 .../src/app/poll/poll.component.html | 35 ++++++++++++ .../src/app/poll/poll.component.spec.ts | 23 ++++++++ pollor.client/src/app/poll/poll.component.ts | 55 +++++++++++++++++++ 6 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 pollor.client/src/app/poll/poll.component.css create mode 100644 pollor.client/src/app/poll/poll.component.html create mode 100644 pollor.client/src/app/poll/poll.component.spec.ts create mode 100644 pollor.client/src/app/poll/poll.component.ts diff --git a/pollor.client/src/app/app-routing.module.ts b/pollor.client/src/app/app-routing.module.ts index 3836051..5640791 100644 --- a/pollor.client/src/app/app-routing.module.ts +++ b/pollor.client/src/app/app-routing.module.ts @@ -11,6 +11,7 @@ import { UserProfileComponent } from './user-profile/user-profile.component'; import { UserAdminProfileComponent } from './user-admin-profile/user-admin-profile.component'; import { UserLogoutComponent } from './user-logout/user-logout.component'; import { CreatePollComponent } from './create-poll/create-poll.component'; +import { PollComponent } from './poll/poll.component'; const routes: Routes = [ { @@ -27,32 +28,36 @@ const routes: Routes = [ }, { path: 'login', - component: UserLoginComponent - }, - { + component: UserLoginComponent + }, + { path: 'register', - component: UserRegisterComponent - }, - { + component: UserRegisterComponent + }, + { path: 'logout', - component: UserLogoutComponent - }, - { + component: UserLogoutComponent + }, + { path: 'profile', - component: UserProfileComponent, - canActivate: [() => inject(AuthGuard).canActivate()] - }, - { + component: UserProfileComponent, + canActivate: [() => inject(AuthGuard).canActivate()] + }, + { path: 'adminpanel', - component: UserAdminProfileComponent, - canActivate: [() => inject(RoleGuard).canActivate('admin')] - }, + component: UserAdminProfileComponent, + canActivate: [() => inject(RoleGuard).canActivate('admin')] + }, ] }, { path: 'polls', component: PollsComponent }, + { + path: 'poll/:id', + component: PollComponent + }, { path: 'create-poll', component: CreatePollComponent, diff --git a/pollor.client/src/app/app.module.ts b/pollor.client/src/app/app.module.ts index 85d3feb..5ff477c 100644 --- a/pollor.client/src/app/app.module.ts +++ b/pollor.client/src/app/app.module.ts @@ -25,6 +25,7 @@ import { UserAdminProfileComponent } from './user-admin-profile/user-admin-profi import { UserLogoutComponent } from './user-logout/user-logout.component'; import { CreatePollComponent } from './create-poll/create-poll.component'; import { DateTimePickerComponent } from './date-time-picker/date-time-picker.component'; +import { PollComponent } from './poll/poll.component'; @NgModule({ declarations: [ @@ -40,7 +41,8 @@ import { DateTimePickerComponent } from './date-time-picker/date-time-picker.com UserAdminProfileComponent, UserLogoutComponent, CreatePollComponent, - DateTimePickerComponent + DateTimePickerComponent, + PollComponent ], imports: [ BrowserModule, diff --git a/pollor.client/src/app/poll/poll.component.css b/pollor.client/src/app/poll/poll.component.css new file mode 100644 index 0000000..e69de29 diff --git a/pollor.client/src/app/poll/poll.component.html b/pollor.client/src/app/poll/poll.component.html new file mode 100644 index 0000000..9566646 --- /dev/null +++ b/pollor.client/src/app/poll/poll.component.html @@ -0,0 +1,35 @@ +

This component demonstrates fetching data from the server.

+ +
+
Cast you vote
+
+ +

{{ pollLoadingMsg }}

+ +

No poll data found

+ +
+ +

{{ poll.question }}

+ + + + + + + + + + + + + + + + +
AnswerTotal votesCast vote
{{ answer.poll_answer }}{{ answer.votes.length }} + +
+
+
+
diff --git a/pollor.client/src/app/poll/poll.component.spec.ts b/pollor.client/src/app/poll/poll.component.spec.ts new file mode 100644 index 0000000..bdc2400 --- /dev/null +++ b/pollor.client/src/app/poll/poll.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PollComponent } from './poll.component'; + +describe('PollComponent', () => { + let component: PollComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PollComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PollComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pollor.client/src/app/poll/poll.component.ts b/pollor.client/src/app/poll/poll.component.ts new file mode 100644 index 0000000..baa9cb6 --- /dev/null +++ b/pollor.client/src/app/poll/poll.component.ts @@ -0,0 +1,55 @@ +import { Component } from '@angular/core'; +import { IPoll } from '../_interfaces/polls.interface'; +import { ApiService } from '../_api/api.service'; +import { HttpParams } from '@angular/common/http'; +import { AlertMessage } from '../alert-message/alert-message'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'app-poll', + templateUrl: './poll.component.html', + styleUrl: './poll.component.css' +}) +export class PollComponent { + + public poll: IPoll | null | undefined; + + public pollLoaded: boolean = false; + public pollLoadingMsg: string = "Loading poll..."; + public pollLoadingColor: string = ""; + + constructor( + private apiService: ApiService, + private alertMessage: AlertMessage, + private route: ActivatedRoute + ) { } + + ngOnInit() { + const id = this.route.snapshot.paramMap.get('id')!; + + this.getPoll(id); + } + + getPoll(pollId: string) { + HttpParams + this.apiService.get(`api/poll/${ pollId }`) + .subscribe({ + next: (response) => { + this.poll = response; + this.pollLoaded = true; + }, + error: (err) => { + const msg = ((err.error && err.error.message) ? err.error.message : err.message); + this.pollLoadingMsg = err.status + ' - ' + msg; + this.pollLoadingColor = "red"; + console.error(err); + this.alertMessage.addInfoAlert("Cannot fetch polls", msg); + }, + //complete: () => { } + }); + } + + castVote() { + + } +} From d3271527cfbb0ac29813ad55a0dea06b3ae72432 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 9 Feb 2024 23:30:43 +0100 Subject: [PATCH 09/16] Add color to the header hamburger button --- pollor.client/src/app/header/header.component.css | 7 +++---- pollor.client/src/app/header/header.component.html | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pollor.client/src/app/header/header.component.css b/pollor.client/src/app/header/header.component.css index 6add56e..2f7856a 100644 --- a/pollor.client/src/app/header/header.component.css +++ b/pollor.client/src/app/header/header.component.css @@ -2,14 +2,13 @@ nav { background-color: seagreen; } -a.nav-link.active { +a.nav-link.active, a.nav-link:hover, button.navbar-toggler { background-color: white; color: black !important; } -a.nav-link:hover { - background-color: white; - color: black !important; + button.navbar-toggler:hover { + background-color: lightgray; } .dropdown-menu { diff --git a/pollor.client/src/app/header/header.component.html b/pollor.client/src/app/header/header.component.html index 0195ef8..9fbee3c 100644 --- a/pollor.client/src/app/header/header.component.html +++ b/pollor.client/src/app/header/header.component.html @@ -24,6 +24,7 @@

diff --git a/pollor.client/src/app/poll/poll.component.ts b/pollor.client/src/app/poll/poll.component.ts index baa9cb6..bcb7cd9 100644 --- a/pollor.client/src/app/poll/poll.component.ts +++ b/pollor.client/src/app/poll/poll.component.ts @@ -1,10 +1,15 @@ import { Component } from '@angular/core'; -import { IPoll } from '../_interfaces/polls.interface'; -import { ApiService } from '../_api/api.service'; import { HttpParams } from '@angular/common/http'; -import { AlertMessage } from '../alert-message/alert-message'; import { ActivatedRoute } from '@angular/router'; +import { IPoll } from '../_interfaces/poll.interface'; +import { ApiService } from '../_api/api.service'; +import { AlertMessage } from '../alert-message/alert-message'; +import { VoteService } from '../_services/vote.service'; +import { IpService } from '../_services/ip.service'; +import { ICreateVote } from '../_interfaces/vote.interface'; +import { IAnswer } from '../_interfaces/answer.interface'; + @Component({ selector: 'app-poll', templateUrl: './poll.component.html', @@ -18,10 +23,15 @@ export class PollComponent { public pollLoadingMsg: string = "Loading poll..."; public pollLoadingColor: string = ""; + public voteCastLoading: boolean = false; + public voteMsg: string = ""; + constructor( private apiService: ApiService, private alertMessage: AlertMessage, - private route: ActivatedRoute + private route: ActivatedRoute, + private voteService: VoteService, + private ipService: IpService ) { } ngOnInit() { @@ -49,7 +59,63 @@ export class PollComponent { }); } - castVote() { + castVote(answer: IAnswer) { + this.voteCastLoading = true; + + // Example usage + this.ipService.getIPv4Address().subscribe({ + next: (response) => { + const ipv4 = response; + this.ipService.getIPv6Address().subscribe({ + next: (response) => { + const ipv6 = response; + this.sendVote(answer, ipv4, ipv6); + }, + error: (error) => { + this.voteMsg = "Failed to get ipv6, try again."; + console.error(error.error); + this.alertMessage.addErrorAlert("Failed to get ipv6", error.error); + } + }); + }, + error: (error) => { + this.voteMsg = "Failed to get ipv4, try again."; + console.error(error.error); + this.alertMessage.addErrorAlert("Failed to get ipv4", error.error); + } + }); + } + + private sendVote(answer: IAnswer, ipv4: ArrayBuffer, ipv6: ArrayBuffer) { + const macAddress = this.ipService.getMacAddress(); + + const vote: ICreateVote = { + answer_id: answer, + ipv4_address: ipv4, + ipv6_address: ipv6, + mac_address: new ArrayBuffer(8) + }; + + console.log(vote); + + // Send the vote to the server + /*this.voteService.castVote(vote).subscribe({ + next: (response) => { + console.log(response); + this.voteMsg = "Vote was successfull !" + this.voteCastLoading = false; + // reload page, so that cast vote buttons will be grayed out as anonymous user has voted + }, + error: (error) => { + console.error(error); + const msg = ((error.error && error.error.message) ? error.error.message : error.message); + this.voteMsg = error.status + ' - ' + msg; + console.error('Cast Vote Error:', error); + this.alertMessage.addErrorAlert("Cast Vote Error", msg); + this.voteCastLoading = false; + }, + //complete: () => {} + });*/ } } From db3f24dad080805f1dcdf6e824626247a36099ae Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Mon, 12 Feb 2024 00:20:29 +0100 Subject: [PATCH 14/16] Complete cast vote --- database/migration.sql | 4 +- pollor.Server/Controllers/VotesController.cs | 21 ++- pollor.Server/Models/VoteModel.cs | 8 +- pollor.Server/Properties/launchSettings.json | 2 +- pollor.Server/pollor.Server.http | 2 +- .../src/app/_auth/auth.interceptor.ts | 21 ++- .../src/app/_interfaces/answer.interface.ts | 2 +- .../src/app/_interfaces/vote.interface.ts | 20 +-- pollor.client/src/app/_services/ip.service.ts | 65 ++----- .../src/app/poll/poll.component.html | 56 +++--- pollor.client/src/app/poll/poll.component.ts | 164 +++++++++++++----- 11 files changed, 198 insertions(+), 167 deletions(-) diff --git a/database/migration.sql b/database/migration.sql index 9509953..3d26177 100644 --- a/database/migration.sql +++ b/database/migration.sql @@ -57,8 +57,8 @@ GO CREATE TABLE [dbo].[votes]( [id] [int] IDENTITY(1,1) NOT NULL, [answer_id] [int] NOT NULL, - [ipv4_address] [varbinary](4) NULL, - [ipv6_address] [varbinary](16) NULL, + [ipv4_address] [varchar](15) NULL, + [ipv6_address] [varchar](45) NULL, [mac_address] [char](12) NULL, [voted_at] [datetime2](7) NOT NULL, [created_at] [datetime2](7) NOT NULL, diff --git a/pollor.Server/Controllers/VotesController.cs b/pollor.Server/Controllers/VotesController.cs index 46658e7..a9310f8 100644 --- a/pollor.Server/Controllers/VotesController.cs +++ b/pollor.Server/Controllers/VotesController.cs @@ -58,12 +58,29 @@ public IActionResult GetVoteById(int id) } [HttpPost("vote")] - [Authorize] public IActionResult AddVote(VoteModel vote) { try { using (var context = new PollorDbContext()) { - EntityEntry newVote = context.Votes.Add(vote); + AnswerModel? answer = context.Answers + .Where(a => a.id.Equals(vote.answer_id)) + .Where(a => a.Votes.Any(v => v.ipv4_address == vote.ipv4_address || v.ipv6_address == vote.ipv6_address)) + .FirstOrDefault(); + if (answer != null) { + return Conflict(new { message = "You have already voted on this poll" }); + } + } + + DateTime now = DateTime.Now; + using (var context = new PollorDbContext()) { + EntityEntry newVote = context.Votes.Add(new VoteModel() + { + answer_id = vote.answer_id, + ipv4_address = vote.ipv4_address ?? null, + ipv6_address = vote.ipv6_address ?? null, + created_at = now, + voted_at = now + }); context.SaveChanges(); if (newVote == null) { diff --git a/pollor.Server/Models/VoteModel.cs b/pollor.Server/Models/VoteModel.cs index 3f98f90..b452469 100644 --- a/pollor.Server/Models/VoteModel.cs +++ b/pollor.Server/Models/VoteModel.cs @@ -7,10 +7,10 @@ namespace pollor.Server.Models public partial class VoteModel : SuperModel { public int answer_id { get; set; } - [MaxLength(4)] - public byte[]? ipv4_address { get; set; } - [MaxLength(16)] - public byte[]? ipv6_address { get; set; } + [MaxLength(15)] + public string? ipv4_address { get; set; } + [MaxLength(45)] + public string? ipv6_address { get; set; } [MaxLength(12)] public char[]? mac_address { get; set; } public DateTime voted_at { get; set; } diff --git a/pollor.Server/Properties/launchSettings.json b/pollor.Server/Properties/launchSettings.json index 39eca2b..ebd4cc8 100644 --- a/pollor.Server/Properties/launchSettings.json +++ b/pollor.Server/Properties/launchSettings.json @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5010", + "applicationUrl": "https://localhost:5010", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" diff --git a/pollor.Server/pollor.Server.http b/pollor.Server/pollor.Server.http index 6c810c5..d1fc0ea 100644 --- a/pollor.Server/pollor.Server.http +++ b/pollor.Server/pollor.Server.http @@ -1,4 +1,4 @@ -@pollor.Server_HostAddress = http://localhost:5010 +@pollor.Server_HostAddress = https://localhost:5010 GET {{pollor.Server_HostAddress}}/weatherforecast/ Accept: application/json diff --git a/pollor.client/src/app/_auth/auth.interceptor.ts b/pollor.client/src/app/_auth/auth.interceptor.ts index 6c709de..ff3166a 100644 --- a/pollor.client/src/app/_auth/auth.interceptor.ts +++ b/pollor.client/src/app/_auth/auth.interceptor.ts @@ -1,4 +1,4 @@ -import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { HttpEvent, HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { AuthService } from './auth.service'; @@ -8,20 +8,19 @@ import { environment } from '../../environments/environment'; export class AuthInterceptor implements HttpInterceptor { private baseUrl = `${environment.API_URL}`; - constructor(private authService: AuthService) { } intercept(req: HttpRequest, next: HttpHandler): Observable> { - if (req.url.includes(this.baseUrl) && this.authService.getToken()) { - const headers = { - 'Content-Type': 'application/json; charset=utf-8', - 'Accept': 'application/json', - 'Authorization': `Bearer ${this.authService.getToken()}`, + if (req.url.includes(this.baseUrl)) { + let headers = new HttpHeaders(); + headers = headers.set('Content-Type', 'application/json; charset=utf-8'); + + if (this.authService.getToken()) { + headers = headers.set('Accept', 'application/json'); + headers = headers.set('Authorization', `Bearer ${this.authService.getToken()}`); }; - - req = req.clone({ - setHeaders: headers, - }); + + req = req.clone({ headers }); } return next.handle(req); diff --git a/pollor.client/src/app/_interfaces/answer.interface.ts b/pollor.client/src/app/_interfaces/answer.interface.ts index f3f3ee9..f77e051 100644 --- a/pollor.client/src/app/_interfaces/answer.interface.ts +++ b/pollor.client/src/app/_interfaces/answer.interface.ts @@ -2,7 +2,7 @@ import { IPoll } from "./poll.interface"; import { IVote } from "./vote.interface"; export interface IAnswer { - id: string; + id: number; poll_id: IPoll; poll_answer: string; created_at: Date; diff --git a/pollor.client/src/app/_interfaces/vote.interface.ts b/pollor.client/src/app/_interfaces/vote.interface.ts index 19e064c..3e625ed 100644 --- a/pollor.client/src/app/_interfaces/vote.interface.ts +++ b/pollor.client/src/app/_interfaces/vote.interface.ts @@ -1,18 +1,16 @@ -import { IAnswer } from "./answer.interface"; - export interface IVote { - id: string; - answer_id: IAnswer; - ipv4_address: ArrayBuffer; - ipv6_address: ArrayBuffer; - mac_address: ArrayBuffer; + id: number; + answer_id: number; + ipv4_address: string; + ipv6_address: string; + mac_address: string; voted_at: Date; created_at: Date; } export interface ICreateVote { - answer_id: IAnswer; - ipv4_address: ArrayBuffer; - ipv6_address: ArrayBuffer; - mac_address: ArrayBuffer; + answer_id: number; + ipv4_address: string | null| undefined; + ipv6_address: string | null | undefined; + mac_address: string | null | undefined ; } diff --git a/pollor.client/src/app/_services/ip.service.ts b/pollor.client/src/app/_services/ip.service.ts index 4dbd79f..75abed3 100644 --- a/pollor.client/src/app/_services/ip.service.ts +++ b/pollor.client/src/app/_services/ip.service.ts @@ -7,63 +7,24 @@ import { Observable, map } from 'rxjs'; }) export class IpService { + private ipv4_url: string = "https://api4.ipify.org/?format=json" + private ipv6_url: string = "https://api6.ipify.org/?format=json" + constructor(private http: HttpClient) { } - public getIPv4Address(): Observable { - return this.http.get("https://api4.ipify.org/?format=json").pipe( - map((res: any) => this.ipv4StringToVarBinary(res.ip)) + public getIPv4Address(): Observable { + return this.http.get(this.ipv4_url).pipe( + map((res: any) => { + return res.ip; + }) ); } - public getIPv6Address(): Observable { - return this.http.get("https://api6.ipify.org/?format=json").pipe( - map((res: any) => this.ipv6StringToVarBinary(res.ip)) + public getIPv6Address(): Observable { + return this.http.get(this.ipv6_url).pipe( + map((res: any) => { + return res.ip; + }) ); } - - public getMacAddress() { - - } - - // Convert ArrayBuffer to string assuming UTF-8 encoding - public arrayBufferToString(buffer: ArrayBuffer): string { - const decoder = new TextDecoder('utf-8'); - return decoder.decode(new Uint8Array(buffer)); - } - - // Function to convert IPv4 string to ArrayBuffer - private ipv4StringToVarBinary(ipv4String: string): ArrayBuffer { - const hexString = ipv4String.split('.').map(part => (+part).toString(16).padStart(2, '0')).join(''); - return this.hexStringToArrayBuffer(hexString); - } - - // Function to convert hexadecimal string to ArrayBuffer - private hexStringToArrayBuffer(hexString: string): ArrayBuffer { - const buffer = new ArrayBuffer(hexString.length / 2); - const view = new Uint8Array(buffer); - for (let i = 0; i < hexString.length; i += 2) { - view[i / 2] = parseInt(hexString.substring(i, i + 2), 16); - } - return buffer; - } - - private ipv6StringToVarBinary(ipv6String: string): ArrayBuffer { - // Remove colons and convert the IPv6 string to binary - const binaryString = ipv6String.split(':').map(part => parseInt(part, 16).toString(2).padStart(16, '0')).join(''); - - // Convert binary string to ArrayBuffer - return this.binaryStringToArrayBuffer(binaryString); - } - - private binaryStringToArrayBuffer(binaryString: string): ArrayBuffer { - const length = binaryString.length; - const buffer = new ArrayBuffer(length / 8); - const view = new Uint8Array(buffer); - - for (let i = 0; i < length; i += 8) { - view[i / 8] = parseInt(binaryString.substring(i, i + 8), 2); - } - - return buffer; - } } diff --git a/pollor.client/src/app/poll/poll.component.html b/pollor.client/src/app/poll/poll.component.html index 021fe39..4b0ac7f 100644 --- a/pollor.client/src/app/poll/poll.component.html +++ b/pollor.client/src/app/poll/poll.component.html @@ -1,5 +1,3 @@ -

This component demonstrates fetching data from the server.

-
Cast you vote
@@ -12,13 +10,12 @@
Cast you vote

{{ poll.question }}

- +
- + - @@ -26,41 +23,28 @@

{{ poll.question }}

-
AnswerAnswer Total votes Cast voteVoters Data
{{ answer.poll_answer }} {{ answer.votes.length }} - - - - - - - - - - - - - - - - - -
ipv4ipv6mac
{{ vote.ipv4_address }}{{ vote.ipv6_address }}{{ vote.mac_address }}
+
+ + +
+
+ You have voted! +
- - {{ voteMsg }} - - - - - - +
+ + {{ voteMsg }} + + + {{ voteMsg }} + + + {{ voteMsg }} + +
diff --git a/pollor.client/src/app/poll/poll.component.ts b/pollor.client/src/app/poll/poll.component.ts index bcb7cd9..962fd8a 100644 --- a/pollor.client/src/app/poll/poll.component.ts +++ b/pollor.client/src/app/poll/poll.component.ts @@ -1,13 +1,12 @@ import { Component } from '@angular/core'; -import { HttpParams } from '@angular/common/http'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { IPoll } from '../_interfaces/poll.interface'; import { ApiService } from '../_api/api.service'; import { AlertMessage } from '../alert-message/alert-message'; import { VoteService } from '../_services/vote.service'; import { IpService } from '../_services/ip.service'; -import { ICreateVote } from '../_interfaces/vote.interface'; +import { ICreateVote, IVote } from '../_interfaces/vote.interface'; import { IAnswer } from '../_interfaces/answer.interface'; @Component({ @@ -18,38 +17,65 @@ import { IAnswer } from '../_interfaces/answer.interface'; export class PollComponent { public poll: IPoll | null | undefined; + private pollId: string | undefined; public pollLoaded: boolean = false; public pollLoadingMsg: string = "Loading poll..."; public pollLoadingColor: string = ""; public voteCastLoading: boolean = false; + public voteCastSuccess: boolean = false; public voteMsg: string = ""; + private voteSucessMsg: string = "Your vote has been received, thank you for voting!"; + + public alreadyVoted: boolean = false; + + private ipv4: string | null = null; + private ipv6: string | null = null + private ipv4Loaded:boolean = false; + private ipv6Loaded:boolean = false; + + private getPollCallbackFunction: (() => void) | undefined = undefined; + private sendVoteCallbackFunction: ((answer: number) => void) | undefined = undefined; constructor( private apiService: ApiService, private alertMessage: AlertMessage, + private router: Router, private route: ActivatedRoute, private voteService: VoteService, private ipService: IpService ) { } ngOnInit() { - const id = this.route.snapshot.paramMap.get('id')!; + this.pollId = this.route.snapshot.paramMap.get('id')!; - this.getPoll(id); + this.getPollCallbackFunction = this.getPoll; + this.getIpv4(); + this.getIpv6(); } - getPoll(pollId: string) { - HttpParams - this.apiService.get(`api/poll/${ pollId }`) + getPoll() { + this.apiService.get(`api/poll/${ this.pollId }`) .subscribe({ next: (response) => { this.poll = response; + + this.poll.answers.forEach((a: IAnswer) => { + a.votes.forEach((v: IVote) => { + if (v.ipv4_address == this.ipv4 || v.ipv6_address == this.ipv6) { + this.alreadyVoted = true; + this.voteMsg = this.voteSucessMsg; + } else { + this.alreadyVoted = false; + } + }); + }); + this.pollLoaded = true; }, error: (err) => { - const msg = ((err.error && err.error.message) ? err.error.message : err.message); + const msg = ((err.error && err.error.message) ? err.error.message : (err.error.title) ? err.error.title : err.message); this.pollLoadingMsg = err.status + ' - ' + msg; this.pollLoadingColor = "red"; console.error(err); @@ -59,63 +85,109 @@ export class PollComponent { }); } - castVote(answer: IAnswer) { + castVote(answer: number) { this.voteCastLoading = true; + this.voteMsg = "Vote uploading..." + + this.sendVoteCallbackFunction = this.sendVote; + this.getIpv4(answer); + this.getIpv6(answer); + } + + private getIpv4(answer: number | null = null) { + if (!this.ipv4Loaded) { + this.ipService.getIPv4Address().subscribe({ + next: (response) => { + this.ipv4 = response; + this.ipv4Loaded = true; + this.handleCallback(answer); + }, + error: (error) => { + this.ipv4Loaded = true; + console.error(error.error); + //this.alertMessage.addErrorAlert("Failed to get ipv4", error.error); + this.handleCallback(answer); + } + }); + } else { + this.handleCallback(answer); + } + } + private getIpv6(answer: number | null = null) { // Example usage - this.ipService.getIPv4Address().subscribe({ - next: (response) => { - const ipv4 = response; - this.ipService.getIPv6Address().subscribe({ - next: (response) => { - const ipv6 = response; - this.sendVote(answer, ipv4, ipv6); - }, - error: (error) => { - this.voteMsg = "Failed to get ipv6, try again."; - console.error(error.error); - this.alertMessage.addErrorAlert("Failed to get ipv6", error.error); - } - }); - }, - error: (error) => { - this.voteMsg = "Failed to get ipv4, try again."; - console.error(error.error); - this.alertMessage.addErrorAlert("Failed to get ipv4", error.error); - } - }); + if (!this.ipv6Loaded) { + this.ipService.getIPv6Address().subscribe({ + next: (response) => { + this.ipv6 = response; + this.ipv6Loaded = true; + this.handleCallback(answer); + }, + error: (error) => { + this.ipv6Loaded = true; + console.error(error.error); + //this.alertMessage.addErrorAlert("Failed to get ipv6", error.error); + this.handleCallback(answer); + } + }); + } else { + this.handleCallback(answer); + } } - private sendVote(answer: IAnswer, ipv4: ArrayBuffer, ipv6: ArrayBuffer) { - const macAddress = this.ipService.getMacAddress(); + private handleCallback(answer: number | null = null) { + // console.log(`ipv4: ${this.ipv4Loaded}`); + // console.log(`ipv6: ${this.ipv6Loaded}`); + + if (this.ipv4Loaded && this.ipv6Loaded) { + if (this.getPollCallbackFunction) { + this.getPollCallbackFunction(); + this.getPollCallbackFunction = undefined; + } else if (this.sendVoteCallbackFunction && answer) { + this.sendVoteCallbackFunction(answer); + this.sendVoteCallbackFunction = undefined; + } + } + } - const vote: ICreateVote = { + private sendVote(answer: number) { + let vote: ICreateVote = { answer_id: answer, - ipv4_address: ipv4, - ipv6_address: ipv6, - mac_address: new ArrayBuffer(8) + ipv4_address: undefined, + ipv6_address: undefined, + mac_address: undefined }; + if (this.ipv4 || this.ipv6) { + if (this.ipv4) vote.ipv4_address = this.ipv4; + if (this.ipv6) vote.ipv6_address = this.ipv6; + } else { + this.voteMsg = "Failed to get IP Address, try again."; + this.alertMessage.addErrorAlert("Failed to get IP Address", `Ipv4(${ this.ipv4 }) and ipv6 (${ this.ipv6 })`); + } + console.log(vote); // Send the vote to the server - /*this.voteService.castVote(vote).subscribe({ + this.voteService.castVote(vote).subscribe({ next: (response) => { console.log(response); - this.voteMsg = "Vote was successfull !" + this.voteMsg = this.voteSucessMsg; this.voteCastLoading = false; - + this.voteCastSuccess = true; // reload page, so that cast vote buttons will be grayed out as anonymous user has voted + this.router.navigate(['poll/', { relativeTo: this.route }]); }, - error: (error) => { - console.error(error); - const msg = ((error.error && error.error.message) ? error.error.message : error.message); - this.voteMsg = error.status + ' - ' + msg; - console.error('Cast Vote Error:', error); + error: (err) => { + console.error(err); + const msg = ((err.error && err.error.message) ? err.error.message : (err.error.title) ? err.error.title : err.message); + this.voteMsg = err.status + ' - ' + msg; + console.error('Cast Vote Error:', err); this.alertMessage.addErrorAlert("Cast Vote Error", msg); this.voteCastLoading = false; + this.voteCastSuccess = false; }, //complete: () => {} - });*/ + }); } } From 93b6b7e7cc16e31022674489f73583f275e80456 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Mon, 12 Feb 2024 00:23:03 +0100 Subject: [PATCH 15/16] Comment out non usable button --- .../src/app/user-profile/user-profile.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pollor.client/src/app/user-profile/user-profile.component.html b/pollor.client/src/app/user-profile/user-profile.component.html index 00e5d78..437eed1 100644 --- a/pollor.client/src/app/user-profile/user-profile.component.html +++ b/pollor.client/src/app/user-profile/user-profile.component.html @@ -35,9 +35,9 @@
User profile

-
+