From b4a3f50ecbe841350fc5a366cc64485ca940c857 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 02:19:21 +0100 Subject: [PATCH 01/28] Add JWT authentication --- database/migration.sql | 8 +- pollor.Server/.env.example | 3 +- pollor.Server/Controllers/AuthController.cs | 117 ++++++++++++++++++ pollor.Server/Models/AuthModel.cs | 33 +++++ pollor.Server/Models/AuthenticatedResponse.cs | 8 ++ pollor.Server/Models/SuperModel.cs | 4 +- pollor.Server/Models/UserModel.cs | 20 ++- pollor.Server/Program.cs | 26 ++++ pollor.Server/Properties/launchSettings.json | 2 +- pollor.Server/Services/PollorDbContext.cs | 1 + pollor.Server/pollor.Server.csproj | 1 + pollor.client/.env.example | 2 +- 12 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 pollor.Server/Controllers/AuthController.cs create mode 100644 pollor.Server/Models/AuthModel.cs create mode 100644 pollor.Server/Models/AuthenticatedResponse.cs diff --git a/database/migration.sql b/database/migration.sql index 240b194..2aaadd9 100644 --- a/database/migration.sql +++ b/database/migration.sql @@ -14,11 +14,13 @@ GO CREATE TABLE [dbo].[users]( [id] [int] IDENTITY(1,1) NOT NULL, [emailaddress] [nvarchar](256) NOT NULL, - [first_name] [nvarchar](64) NOT NULL, - [last_name] [nvarchar](64) NOT NULL, - [profile_username] [nvarchar](64) NOT NULL, + [username] [nvarchar](64) NOT NULL, + [password] [nvarchar](128) NOT NULL, + [first_name] [nvarchar](64) NULL, + [last_name] [nvarchar](64) NULL, [created_at] [datetime] NOT NULL, CONSTRAINT PK_users PRIMARY KEY NONCLUSTERED (id) + CONSTRAINT UC_Users UNIQUE (id,emailaddress,username) ) ON [PRIMARY] GO diff --git a/pollor.Server/.env.example b/pollor.Server/.env.example index 1c5ea20..bdf5c4b 100644 --- a/pollor.Server/.env.example +++ b/pollor.Server/.env.example @@ -3,4 +3,5 @@ DB_SERVER=localhost\MSSQLSERVERx DB_NAME=name DB_UID= DB_PASSWORD= -ASPNETCORE_ENVIRONMENT=Development \ No newline at end of file +ASPNETCORE_ENVIRONMENT=Development +SECRET_JWT_KEY=secret-hash-key \ No newline at end of file diff --git a/pollor.Server/Controllers/AuthController.cs b/pollor.Server/Controllers/AuthController.cs new file mode 100644 index 0000000..897ad13 --- /dev/null +++ b/pollor.Server/Controllers/AuthController.cs @@ -0,0 +1,117 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Identity; +using pollor.Server.Models; +using pollor.Server.Services; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +[Route("api/[controller]")] +[ApiController] +public class AuthController : ControllerBase +{ + [HttpPost("register")] + public IActionResult Register([FromBody] RegisterModel registerUser) + { + if (registerUser is null) + { + return BadRequest("Invalid client request"); + } + + if (registerUser.password!.Length < 8) { + return BadRequest("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("Username is already taken, please login or use another username."); + } + + bool isEmailAvailable = new PollorDbContext().UserAuthModel.Where(u => u.emailaddress!.ToLower().Equals(registerUser.emailaddress!.ToLower())).IsNullOrEmpty(); + if (isEmailAvailable == false) { + return BadRequest("Emailaddress is already taken, please login or use another emailaddress."); + } + + var hasher = new PasswordHasher(); + var hashedPass = hasher.HashPassword(registerUser, registerUser.password!); + UserAuthModel newUser = new UserAuthModel() { + username = registerUser.username, + password = hashedPass, + emailaddress = registerUser.emailaddress, + Created_at = DateTime.Now, + }; + + EntityEntry createdUser; + using (var pollorContext = new PollorDbContext()) { + createdUser = pollorContext.UserAuthModel.Add(newUser); + pollorContext.SaveChanges(); + } + + var tokeOptions = GetJwtTokenOptions(1, createdUser.Entity); + var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions); + UserModel currentUser = new PollorDbContext().Users.Where(u => u.Id.Equals(createdUser.Entity.Id)).FirstOrDefault()!; + return Ok(new AuthenticatedResponse { Token = tokenString, User = currentUser}); + } + + + [HttpPost("login")] + public IActionResult Login([FromBody] LoginModel loginUser) + { + if (loginUser is null) + { + return BadRequest("Invalid client request"); + } + + var authUser = new PollorDbContext().UserAuthModel.Where(u => u.username!.ToLower().Equals(loginUser.username!.ToLower())).FirstOrDefault(); + if (authUser == null) { + return Unauthorized("Username or password is wrong!"); + } + + var hasher = new PasswordHasher(); + PasswordVerificationResult passwordIsOk = hasher.VerifyHashedPassword(loginUser, authUser.password!, loginUser.password!); + + if (passwordIsOk == PasswordVerificationResult.Failed) { + return Unauthorized("Username or password is wrong!"); + } + + if (authUser.username == loginUser.username && (passwordIsOk == PasswordVerificationResult.Success || passwordIsOk == PasswordVerificationResult.SuccessRehashNeeded)) + { + if (passwordIsOk == PasswordVerificationResult.SuccessRehashNeeded) { + // rehash password and save to DB + } + + int tokenLongerValid = (bool)loginUser.tokenLongerValid ? 31 : 1;// true = 31, false = 1 + + var currentUserWithPass = new PollorDbContext().UserAuthModel.Where(u => u.username!.ToLower().Equals(authUser.username!.ToLower())).FirstOrDefault(); + int daysTokenIsValid = tokenLongerValid; + var tokenOptions = GetJwtTokenOptions(daysTokenIsValid, currentUserWithPass!); + var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions); + + var currentUser = new PollorDbContext().Users.Where(u => u.username!.ToLower().Equals(authUser.username!.ToLower())).FirstOrDefault(); + return Ok(new AuthenticatedResponse { Token = tokenString, User = currentUser}); + } + + return Unauthorized(); + } + + private JwtSecurityToken GetJwtTokenOptions (int tokenValidForXDays, UserAuthModel user) { + var jwtClaims = new List + { + new Claim(ClaimTypes.Name, user.username!), + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) + }; + + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET_JWT_KEY")!)); + var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); + var tokeOptions = new JwtSecurityToken( + issuer: "https://localhost:5001", + audience: "https://localhost:5001", + claims: jwtClaims, + expires: DateTime.Now.AddDays(tokenValidForXDays), + signingCredentials: signinCredentials + ); + return tokeOptions; + } +} \ No newline at end of file diff --git a/pollor.Server/Models/AuthModel.cs b/pollor.Server/Models/AuthModel.cs new file mode 100644 index 0000000..f604307 --- /dev/null +++ b/pollor.Server/Models/AuthModel.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace pollor.Server.Models +{ + public class RegisterModel + { + [Required] + public string? emailaddress { get; set; } + [Required] + public string? username { get; set; } + [Required] + public string? password { get; set; } + } + + public class LoginModel + { + [Required] + public string? username { get; set; } + [Required] + public string? password { get; set; } + public bool tokenLongerValid { get; set; } = false; + } + + public class ChangePasswordModel + { + [Required] + public int? id { get; set; } + [Required] + public string? newpassword { get; set; } + [Required] + public string? confirmPassword { get; set; } + } +} \ No newline at end of file diff --git a/pollor.Server/Models/AuthenticatedResponse.cs b/pollor.Server/Models/AuthenticatedResponse.cs new file mode 100644 index 0000000..f432fa8 --- /dev/null +++ b/pollor.Server/Models/AuthenticatedResponse.cs @@ -0,0 +1,8 @@ +namespace pollor.Server.Models +{ + public class AuthenticatedResponse + { + public string? Token { get; set; } + public UserModel? User { get; set; } + } +} \ No newline at end of file diff --git a/pollor.Server/Models/SuperModel.cs b/pollor.Server/Models/SuperModel.cs index 48805c3..2ebf491 100644 --- a/pollor.Server/Models/SuperModel.cs +++ b/pollor.Server/Models/SuperModel.cs @@ -1,11 +1,11 @@ +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace pollor.Server.Models { public class SuperModel { - - [Column("id")] + [Key, Column("id")] public int Id { get; set; } public DateTime Created_at { get; set; } diff --git a/pollor.Server/Models/UserModel.cs b/pollor.Server/Models/UserModel.cs index a88b8ce..fa671eb 100644 --- a/pollor.Server/Models/UserModel.cs +++ b/pollor.Server/Models/UserModel.cs @@ -1,19 +1,33 @@ +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace pollor.Server.Models { + public class BaseUserModel : SuperModel + { + public string? username { get; set; } + public string? emailaddress { get; set; } + } + [Table("users")] - public class UserModel : SuperModel + public class UserModel: BaseUserModel { public UserModel() { Polls = new List(); } - public string? first_name { get; set; } public string? last_name { get; set; } - public string? profile_username { get; set; } [ForeignKey("user_id")] // ForeignKey attribute in the PollModel public virtual ICollection Polls { get; set; } } + + [Table("users", Schema = "dbo")] + public class UserAuthModel : BaseUserModel + { + [DataType(DataType.Password)] + public string? password { get; set; } + [NotMapped] + public string? confirmPassword { get; set; } + } } \ No newline at end of file diff --git a/pollor.Server/Program.cs b/pollor.Server/Program.cs index 07fbb8e..84af820 100644 --- a/pollor.Server/Program.cs +++ b/pollor.Server/Program.cs @@ -1,4 +1,7 @@ +using System.Text; using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; using pollor.Server.Services; var builder = WebApplication.CreateBuilder(args); @@ -27,6 +30,28 @@ }); }); +/* get secret private jwt key value */ +String secretJwtKey = Environment.GetEnvironmentVariable("SECRET_JWT_KEY")!; + +/* Add JWT authentication */ +builder.Services.AddAuthentication(opt => { + opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = "https://localhost:5001", + ValidAudience = "https://localhost:5001", + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretJwtKey)) + }; + }); + // Add services to the container. builder.Services.AddControllers() @@ -55,6 +80,7 @@ app.UseHttpsRedirection(); app.UseAuthorization(); +app.UseAuthentication(); app.MapControllers().RequireCors(); diff --git a/pollor.Server/Properties/launchSettings.json b/pollor.Server/Properties/launchSettings.json index 7ca1d1e..39eca2b 100644 --- a/pollor.Server/Properties/launchSettings.json +++ b/pollor.Server/Properties/launchSettings.json @@ -25,7 +25,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7267;http://localhost:5010", + "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" diff --git a/pollor.Server/Services/PollorDbContext.cs b/pollor.Server/Services/PollorDbContext.cs index 855482e..f37372b 100644 --- a/pollor.Server/Services/PollorDbContext.cs +++ b/pollor.Server/Services/PollorDbContext.cs @@ -9,6 +9,7 @@ public class PollorDbContext : DbContext //entities public DbSet Users { get; set; } + public DbSet UserAuthModel { get; set; } public DbSet Polls { get; set; } public DbSet Votes { get; set; } public DbSet Answers { get; set; } diff --git a/pollor.Server/pollor.Server.csproj b/pollor.Server/pollor.Server.csproj index e9f73ad..f169e06 100644 --- a/pollor.Server/pollor.Server.csproj +++ b/pollor.Server/pollor.Server.csproj @@ -12,6 +12,7 @@ + 8.*-* diff --git a/pollor.client/.env.example b/pollor.client/.env.example index a975e12..aa02a07 100644 --- a/pollor.client/.env.example +++ b/pollor.client/.env.example @@ -1 +1 @@ -API_URL='https://localhost:7267' +API_URL='https://localhost:5001' From 242754fdd6925e6772a4401730e73f1bab914505 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 02:35:18 +0100 Subject: [PATCH 02/28] Add Authorize to Swagger --- pollor.Server/Program.cs | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/pollor.Server/Program.cs b/pollor.Server/Program.cs index 84af820..956677d 100644 --- a/pollor.Server/Program.cs +++ b/pollor.Server/Program.cs @@ -2,6 +2,7 @@ using System.Text.Json.Serialization; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using pollor.Server.Services; var builder = WebApplication.CreateBuilder(args); @@ -61,13 +62,36 @@ });; // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(option => +{ + option.SwaggerDoc("v1", new OpenApiInfo { Title = "Pollor API", Version = "v1" }); + option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + option.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type=ReferenceType.SecurityScheme, + Id="Bearer" + } + }, + new string[]{} + } + }); +}); var app = builder.Build(); -app.UseDefaultFiles(); -app.UseStaticFiles(); - app.UseCors(); // Configure the HTTP request pipeline. From e734016872540a2dd583cd500eee19f9e92211c3 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 02:35:56 +0100 Subject: [PATCH 03/28] Add secure AddPoll api call --- pollor.Server/Controllers/PollsController.cs | 28 +++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/pollor.Server/Controllers/PollsController.cs b/pollor.Server/Controllers/PollsController.cs index bef93d0..02d3901 100644 --- a/pollor.Server/Controllers/PollsController.cs +++ b/pollor.Server/Controllers/PollsController.cs @@ -1,5 +1,7 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using pollor.Server.Models; @@ -8,7 +10,7 @@ namespace pollor.Server.Controllers { [ApiController] - [Route("[controller]")] + [Route("api/")] public class PollsController : ControllerBase { private readonly ILogger _logger; @@ -18,7 +20,7 @@ public PollsController(ILogger logger) _logger = logger; } - [HttpGet(Name = "GetPollsController")] + [HttpGet("polls")] public IActionResult GetAllPolls() { try { @@ -39,7 +41,7 @@ public IActionResult GetAllPolls() } } - [HttpGet("{id}")] + [HttpGet("poll/{id}")] public IActionResult GetPollById(int id) { try { @@ -60,5 +62,25 @@ public IActionResult GetPollById(int id) return StatusCode(500, new { message = ex.Message}); } } + + [HttpPost("poll")] + [Authorize] + public IActionResult AddPoll(PollModel newPoll) + { + try { + using (var context = new PollorDbContext()) { + EntityEntry? poll = context.Polls + .Add(newPoll); + if (poll == null) { + return NotFound(); + } + return Ok(poll); + } + } + catch (Exception ex) { + _logger.LogError(ex.Message); + return StatusCode(500, new { message = ex.Message}); + } + } } } From 9cc274eceffdc0b622c62d505a1582acc6d78999 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 02:36:53 +0100 Subject: [PATCH 04/28] Update api naming --- pollor.Server/Controllers/AnswersController.cs | 6 +++--- pollor.Server/Controllers/AuthController.cs | 2 +- pollor.Server/Controllers/UsersController.cs | 6 +++--- pollor.Server/Controllers/VotesController.cs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pollor.Server/Controllers/AnswersController.cs b/pollor.Server/Controllers/AnswersController.cs index 162dc08..e89f232 100644 --- a/pollor.Server/Controllers/AnswersController.cs +++ b/pollor.Server/Controllers/AnswersController.cs @@ -7,7 +7,7 @@ namespace answeror.Server.Controllers { [ApiController] - [Route("[controller]")] + [Route("api/")] public class AnswersController : ControllerBase { private readonly ILogger _logger; @@ -17,7 +17,7 @@ public AnswersController(ILogger logger) _logger = logger; } - [HttpGet(Name = "GetAnswersController")] + [HttpGet("answers")] public IActionResult GetAllAnswers() { try { @@ -37,7 +37,7 @@ public IActionResult GetAllAnswers() } } - [HttpGet("{id}")] + [HttpGet("answer/{id}")] public IActionResult GetAnswerById(int id) { try { diff --git a/pollor.Server/Controllers/AuthController.cs b/pollor.Server/Controllers/AuthController.cs index 897ad13..e472539 100644 --- a/pollor.Server/Controllers/AuthController.cs +++ b/pollor.Server/Controllers/AuthController.cs @@ -8,7 +8,7 @@ using pollor.Server.Services; using Microsoft.EntityFrameworkCore.ChangeTracking; -[Route("api/[controller]")] +[Route("api/auth")] [ApiController] public class AuthController : ControllerBase { diff --git a/pollor.Server/Controllers/UsersController.cs b/pollor.Server/Controllers/UsersController.cs index 8fcd45b..70e11c5 100644 --- a/pollor.Server/Controllers/UsersController.cs +++ b/pollor.Server/Controllers/UsersController.cs @@ -7,7 +7,7 @@ namespace pollor.Server.Controllers { [ApiController] - [Route("[controller]")] + [Route("api/")] public class UsersController : ControllerBase { private readonly ILogger _logger; @@ -17,7 +17,7 @@ public UsersController(ILogger logger) _logger = logger; } - [HttpGet(Name = "GetUsersController")] + [HttpGet("users")] public IActionResult GetAllUsers() { try { @@ -39,7 +39,7 @@ public IActionResult GetAllUsers() } } - [HttpGet("{id}")] + [HttpGet("user/{id}")] public IActionResult GetUserById(int id) { try { diff --git a/pollor.Server/Controllers/VotesController.cs b/pollor.Server/Controllers/VotesController.cs index 615fbba..f72ae5e 100644 --- a/pollor.Server/Controllers/VotesController.cs +++ b/pollor.Server/Controllers/VotesController.cs @@ -7,7 +7,7 @@ namespace pollor.Server.Controllers { [ApiController] - [Route("[controller]")] + [Route("api/")] public class VotesController : ControllerBase { private readonly ILogger _logger; @@ -17,7 +17,7 @@ public VotesController(ILogger logger) _logger = logger; } - [HttpGet(Name = "GetVotesController")] + [HttpGet("votes")] public IActionResult GetAllVotes() { try { @@ -35,7 +35,7 @@ public IActionResult GetAllVotes() } } - [HttpGet("{id}")] + [HttpGet("vote/{id}")] public IActionResult GetVoteById(int id) { try { From 48c0dc1e5f6afd2cb264ea7cf553a5447aa2b071 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 03:16:53 +0100 Subject: [PATCH 05/28] Change Angular file structure --- pollor.client/src/app/app-routing.module.ts | 2 +- pollor.client/src/app/app.module.ts | 2 +- pollor.client/src/app/home/home.component.ts | 2 +- .../src/app/polls/{component => }/polls.component.css | 0 .../src/app/polls/{component => }/polls.component.html | 0 .../src/app/polls/{component => }/polls.component.spec.ts | 0 .../src/app/polls/{component => }/polls.component.ts | 8 ++++---- .../src/{app/polls => interfaces}/answers.interface.ts | 0 .../src/{app/polls => interfaces}/polls.interface.ts | 0 .../src/{app/polls => interfaces}/votes.interface.ts | 0 pollor.client/src/{app => }/services/api.service.ts | 2 +- 11 files changed, 8 insertions(+), 8 deletions(-) rename pollor.client/src/app/polls/{component => }/polls.component.css (100%) rename pollor.client/src/app/polls/{component => }/polls.component.html (100%) rename pollor.client/src/app/polls/{component => }/polls.component.spec.ts (100%) rename pollor.client/src/app/polls/{component => }/polls.component.ts (74%) rename pollor.client/src/{app/polls => interfaces}/answers.interface.ts (100%) rename pollor.client/src/{app/polls => interfaces}/polls.interface.ts (100%) rename pollor.client/src/{app/polls => interfaces}/votes.interface.ts (100%) rename pollor.client/src/{app => }/services/api.service.ts (92%) diff --git a/pollor.client/src/app/app-routing.module.ts b/pollor.client/src/app/app-routing.module.ts index 49b6658..5bd9e70 100644 --- a/pollor.client/src/app/app-routing.module.ts +++ b/pollor.client/src/app/app-routing.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; -import { PollsComponent } from './polls/component/polls.component'; +import { PollsComponent } from './polls/polls.component'; const routes: Routes = [ { path: '', component: HomeComponent }, diff --git a/pollor.client/src/app/app.module.ts b/pollor.client/src/app/app.module.ts index baa9450..991ee97 100644 --- a/pollor.client/src/app/app.module.ts +++ b/pollor.client/src/app/app.module.ts @@ -11,7 +11,7 @@ import { FooterComponent } from './footer/footer.component'; import { HomeComponent } from './home/home.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; -import { PollsComponent } from './polls/component/polls.component'; +import { PollsComponent } from './polls/polls.component'; @NgModule({ declarations: [ diff --git a/pollor.client/src/app/home/home.component.ts b/pollor.client/src/app/home/home.component.ts index 152dafe..fe23cc3 100644 --- a/pollor.client/src/app/home/home.component.ts +++ b/pollor.client/src/app/home/home.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ApiService } from '../services/api.service'; +import { ApiService } from '../../services/api.service'; @Component({ selector: 'app-home', diff --git a/pollor.client/src/app/polls/component/polls.component.css b/pollor.client/src/app/polls/polls.component.css similarity index 100% rename from pollor.client/src/app/polls/component/polls.component.css rename to pollor.client/src/app/polls/polls.component.css diff --git a/pollor.client/src/app/polls/component/polls.component.html b/pollor.client/src/app/polls/polls.component.html similarity index 100% rename from pollor.client/src/app/polls/component/polls.component.html rename to pollor.client/src/app/polls/polls.component.html diff --git a/pollor.client/src/app/polls/component/polls.component.spec.ts b/pollor.client/src/app/polls/polls.component.spec.ts similarity index 100% rename from pollor.client/src/app/polls/component/polls.component.spec.ts rename to pollor.client/src/app/polls/polls.component.spec.ts diff --git a/pollor.client/src/app/polls/component/polls.component.ts b/pollor.client/src/app/polls/polls.component.ts similarity index 74% rename from pollor.client/src/app/polls/component/polls.component.ts rename to pollor.client/src/app/polls/polls.component.ts index 229291e..57a90ea 100644 --- a/pollor.client/src/app/polls/component/polls.component.ts +++ b/pollor.client/src/app/polls/polls.component.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core'; import { ApiService } from '../../services/api.service'; -import { IPolls } from '../polls.interface'; -import { IAnswers } from '../answers.interface'; -import { IVotes } from '../votes.interface'; +import { IPolls } from '../../interfaces/polls.interface'; +import { IAnswers } from '../../interfaces/answers.interface'; +import { IVotes } from '../../interfaces/votes.interface'; @Component({ @@ -22,7 +22,7 @@ export class PollsComponent { } getPolls() { - this.apiService.get('polls') + this.apiService.get('api/polls') .subscribe({ next: (response) => { this.polls = response; diff --git a/pollor.client/src/app/polls/answers.interface.ts b/pollor.client/src/interfaces/answers.interface.ts similarity index 100% rename from pollor.client/src/app/polls/answers.interface.ts rename to pollor.client/src/interfaces/answers.interface.ts diff --git a/pollor.client/src/app/polls/polls.interface.ts b/pollor.client/src/interfaces/polls.interface.ts similarity index 100% rename from pollor.client/src/app/polls/polls.interface.ts rename to pollor.client/src/interfaces/polls.interface.ts diff --git a/pollor.client/src/app/polls/votes.interface.ts b/pollor.client/src/interfaces/votes.interface.ts similarity index 100% rename from pollor.client/src/app/polls/votes.interface.ts rename to pollor.client/src/interfaces/votes.interface.ts diff --git a/pollor.client/src/app/services/api.service.ts b/pollor.client/src/services/api.service.ts similarity index 92% rename from pollor.client/src/app/services/api.service.ts rename to pollor.client/src/services/api.service.ts index ee8c4b5..ae3b523 100644 --- a/pollor.client/src/app/services/api.service.ts +++ b/pollor.client/src/services/api.service.ts @@ -1,7 +1,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; -import { environment } from '../../environments/environment'; +import { environment } from '../environments/environment'; @Injectable({ providedIn: 'root' From 5f8eb1197d6fea93d11b94c7b580abe07a2b8d55 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 03:46:41 +0100 Subject: [PATCH 06/28] Fix Jwt Authorize --- .../Controllers/AnswersController.cs | 2 +- pollor.Server/Controllers/AuthController.cs | 10 +++++----- pollor.Server/Controllers/PollsController.cs | 19 +++++++++++-------- pollor.Server/Controllers/UsersController.cs | 2 +- pollor.Server/Controllers/VotesController.cs | 2 +- pollor.Server/Models/AnswerModel.cs | 2 +- pollor.Server/Models/AuthenticatedResponse.cs | 4 ++-- pollor.Server/Models/PollModel.cs | 8 ++++---- pollor.Server/Models/SuperModel.cs | 4 ++-- pollor.Server/Models/VoteModel.cs | 2 +- pollor.Server/Program.cs | 17 ++++++++++------- 11 files changed, 39 insertions(+), 33 deletions(-) diff --git a/pollor.Server/Controllers/AnswersController.cs b/pollor.Server/Controllers/AnswersController.cs index e89f232..3183263 100644 --- a/pollor.Server/Controllers/AnswersController.cs +++ b/pollor.Server/Controllers/AnswersController.cs @@ -43,7 +43,7 @@ public IActionResult GetAnswerById(int id) try { using (var context = new PollorDbContext()) { AnswerModel? answer = context.Answers - .Where(p => p.Id.Equals(id)) + .Where(p => p.id.Equals(id)) .Include(a => a.Votes) .FirstOrDefault(); if (answer == null) { diff --git a/pollor.Server/Controllers/AuthController.cs b/pollor.Server/Controllers/AuthController.cs index e472539..84be035 100644 --- a/pollor.Server/Controllers/AuthController.cs +++ b/pollor.Server/Controllers/AuthController.cs @@ -40,7 +40,7 @@ public IActionResult Register([FromBody] RegisterModel registerUser) username = registerUser.username, password = hashedPass, emailaddress = registerUser.emailaddress, - Created_at = DateTime.Now, + created_at = DateTime.Now, }; EntityEntry createdUser; @@ -51,8 +51,8 @@ public IActionResult Register([FromBody] RegisterModel registerUser) var tokeOptions = GetJwtTokenOptions(1, createdUser.Entity); var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions); - UserModel currentUser = new PollorDbContext().Users.Where(u => u.Id.Equals(createdUser.Entity.Id)).FirstOrDefault()!; - return Ok(new AuthenticatedResponse { Token = tokenString, User = currentUser}); + UserModel currentUser = new PollorDbContext().Users.Where(u => u.id.Equals(createdUser.Entity.id)).FirstOrDefault()!; + return Created("user/" + currentUser.id, new AuthenticatedResponse { token = tokenString, user = currentUser}); } @@ -90,7 +90,7 @@ public IActionResult Login([FromBody] LoginModel loginUser) var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions); var currentUser = new PollorDbContext().Users.Where(u => u.username!.ToLower().Equals(authUser.username!.ToLower())).FirstOrDefault(); - return Ok(new AuthenticatedResponse { Token = tokenString, User = currentUser}); + return Ok(new AuthenticatedResponse { token = tokenString, user = currentUser}); } return Unauthorized(); @@ -100,7 +100,7 @@ private JwtSecurityToken GetJwtTokenOptions (int tokenValidForXDays, UserAuthMod var jwtClaims = new List { new Claim(ClaimTypes.Name, user.username!), - new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) + new Claim(ClaimTypes.NameIdentifier, user.id.ToString()) }; var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET_JWT_KEY")!)); diff --git a/pollor.Server/Controllers/PollsController.cs b/pollor.Server/Controllers/PollsController.cs index 02d3901..1579691 100644 --- a/pollor.Server/Controllers/PollsController.cs +++ b/pollor.Server/Controllers/PollsController.cs @@ -47,7 +47,7 @@ public IActionResult GetPollById(int id) try { using (var context = new PollorDbContext()) { PollModel? poll = context.Polls - .Where(p => p.Id.Equals(id)) + .Where(p => p.id.Equals(id)) .Include(p => p.Answers) .ThenInclude(a => a.Votes) .FirstOrDefault(); @@ -65,20 +65,23 @@ public IActionResult GetPollById(int id) [HttpPost("poll")] [Authorize] - public IActionResult AddPoll(PollModel newPoll) + public IActionResult AddPoll(PollModel poll) { try { using (var context = new PollorDbContext()) { - EntityEntry? poll = context.Polls - .Add(newPoll); - if (poll == null) { - return NotFound(); + EntityEntry newPoll = context.Polls.Add(poll); + context.SaveChanges(); + + Console.WriteLine("newPoll: " + newPoll.Entity); + + if (newPoll == null) { + return NotFound(newPoll); } - return Ok(poll); + return Created("poll/" + newPoll.Entity.id.ToString(), newPoll.Entity); } } catch (Exception ex) { - _logger.LogError(ex.Message); + _logger.LogError(ex, ex.Message); return StatusCode(500, new { message = ex.Message}); } } diff --git a/pollor.Server/Controllers/UsersController.cs b/pollor.Server/Controllers/UsersController.cs index 70e11c5..87e68d3 100644 --- a/pollor.Server/Controllers/UsersController.cs +++ b/pollor.Server/Controllers/UsersController.cs @@ -45,7 +45,7 @@ public IActionResult GetUserById(int id) try { using (var context = new PollorDbContext()) { UserModel? user = context.Users - .Where(u => u.Id.Equals(id)) + .Where(u => u.id.Equals(id)) .Include(u => u.Polls) .ThenInclude(p => p.Answers) .ThenInclude(a => a.Votes) diff --git a/pollor.Server/Controllers/VotesController.cs b/pollor.Server/Controllers/VotesController.cs index f72ae5e..69fe426 100644 --- a/pollor.Server/Controllers/VotesController.cs +++ b/pollor.Server/Controllers/VotesController.cs @@ -41,7 +41,7 @@ public IActionResult GetVoteById(int id) try { using (var context = new PollorDbContext()) { VoteModel? vote = context.Votes - .Where(v => v.Id.Equals(id)) + .Where(v => v.id.Equals(id)) .FirstOrDefault(); if (vote == null) { return NotFound(); diff --git a/pollor.Server/Models/AnswerModel.cs b/pollor.Server/Models/AnswerModel.cs index 99f99ba..fc8f3a1 100644 --- a/pollor.Server/Models/AnswerModel.cs +++ b/pollor.Server/Models/AnswerModel.cs @@ -9,7 +9,7 @@ public AnswerModel() { Votes = new List(); } - public virtual int Poll_id { get; set; } + public int poll_id { get; set; } public string? poll_answer { get; set; } [ForeignKey("answer_id")] // ForeignKey attribute in the VoteModel diff --git a/pollor.Server/Models/AuthenticatedResponse.cs b/pollor.Server/Models/AuthenticatedResponse.cs index f432fa8..6855ec9 100644 --- a/pollor.Server/Models/AuthenticatedResponse.cs +++ b/pollor.Server/Models/AuthenticatedResponse.cs @@ -2,7 +2,7 @@ namespace pollor.Server.Models { public class AuthenticatedResponse { - public string? Token { get; set; } - public UserModel? User { get; set; } + public string? token { get; set; } + public UserModel? user { get; set; } } } \ No newline at end of file diff --git a/pollor.Server/Models/PollModel.cs b/pollor.Server/Models/PollModel.cs index 435022b..16e0f90 100644 --- a/pollor.Server/Models/PollModel.cs +++ b/pollor.Server/Models/PollModel.cs @@ -9,11 +9,11 @@ public PollModel() { Answers = new List(); } - public virtual int User_id { get; set; } - public string? Question { get; set; } - public DateTime Ending_date { get; set; } + public int user_id { get; set; } + public string? question { get; set; } + public DateTime ending_date { get; set; } - [ForeignKey("Poll_id")] // ForeignKey attribute in the AnswerModel + [ForeignKey("poll_id")] // ForeignKey attribute in the AnswerModel public virtual ICollection Answers { get; set; } } } \ No newline at end of file diff --git a/pollor.Server/Models/SuperModel.cs b/pollor.Server/Models/SuperModel.cs index 2ebf491..f4e2754 100644 --- a/pollor.Server/Models/SuperModel.cs +++ b/pollor.Server/Models/SuperModel.cs @@ -6,8 +6,8 @@ namespace pollor.Server.Models public class SuperModel { [Key, Column("id")] - public int Id { get; set; } - public DateTime Created_at { get; set; } + public int id { get; set; } + public DateTime created_at { get; set; } } } diff --git a/pollor.Server/Models/VoteModel.cs b/pollor.Server/Models/VoteModel.cs index cf0907e..082befe 100644 --- a/pollor.Server/Models/VoteModel.cs +++ b/pollor.Server/Models/VoteModel.cs @@ -5,7 +5,7 @@ namespace pollor.Server.Models [Table("votes")] public partial class VoteModel : SuperModel { - public int Answer_id { get; set; } + public int answer_id { get; set; } public byte[]? ipv4_address { get; set; } public byte[]? ipv6_address { get; set; } public char[]? mac_address { get; set; } diff --git a/pollor.Server/Program.cs b/pollor.Server/Program.cs index 956677d..4925091 100644 --- a/pollor.Server/Program.cs +++ b/pollor.Server/Program.cs @@ -36,9 +36,9 @@ /* Add JWT authentication */ builder.Services.AddAuthentication(opt => { - opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -}) + opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters @@ -83,9 +83,11 @@ { Type=ReferenceType.SecurityScheme, Id="Bearer" - } + }, + Name = "Bearer", + In = ParameterLocation.Header, }, - new string[]{} + new List() } }); }); @@ -102,9 +104,10 @@ } app.UseHttpsRedirection(); - -app.UseAuthorization(); app.UseAuthentication(); +app.UseAuthorization(); + +app.UseStatusCodePages(); app.MapControllers().RequireCors(); From 660d62b8a09ed911bca5fcc3086861edf6a4bc19 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 04:17:32 +0100 Subject: [PATCH 07/28] Fix footer overlapping content --- pollor.client/src/app/app.component.css | 11 ++++++++++- pollor.client/src/app/app.component.html | 12 +++++++----- pollor.client/src/app/footer/footer.component.css | 5 ++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/pollor.client/src/app/app.component.css b/pollor.client/src/app/app.component.css index 7a4fe7e..0078b1c 100644 --- a/pollor.client/src/app/app.component.css +++ b/pollor.client/src/app/app.component.css @@ -1,6 +1,5 @@ :host { max-width: 1280px; - padding: 2rem; text-align: center; } @@ -16,3 +15,13 @@ th, td { padding-left: 1rem; padding-right: 1rem; } + +#page-container { + position: relative; + min-height: 100vh; + } + +#content-wrap { + padding: 2.5rem; + padding-bottom: 12.5rem; /* Footer height, must be same or higher as in footer.component.css */ +} \ No newline at end of file diff --git a/pollor.client/src/app/app.component.html b/pollor.client/src/app/app.component.html index 20c39af..51ee6ff 100644 --- a/pollor.client/src/app/app.component.html +++ b/pollor.client/src/app/app.component.html @@ -1,5 +1,7 @@ - - - - - +
+ +
+ +
+ +
\ No newline at end of file diff --git a/pollor.client/src/app/footer/footer.component.css b/pollor.client/src/app/footer/footer.component.css index 82fd693..3f67524 100644 --- a/pollor.client/src/app/footer/footer.component.css +++ b/pollor.client/src/app/footer/footer.component.css @@ -2,6 +2,5 @@ position: absolute; bottom: 0; width: 100%; - /* Set the fixed height of the footer here */ - background-color: #f5f5f5; -} + height: 10rem; /* Footer height, must be same or lower as in app.component.css */ +} \ No newline at end of file From ed8dbdf9236bea09c72ba8bd0ebcdbf12115f209 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 04:24:30 +0100 Subject: [PATCH 08/28] Add length to model attributes --- pollor.Server/Models/AnswerModel.cs | 2 ++ pollor.Server/Models/AuthModel.cs | 14 +++++++------- pollor.Server/Models/PollModel.cs | 2 ++ pollor.Server/Models/UserModel.cs | 8 ++++++-- pollor.Server/Models/VoteModel.cs | 4 ++++ 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/pollor.Server/Models/AnswerModel.cs b/pollor.Server/Models/AnswerModel.cs index fc8f3a1..8ac0ba6 100644 --- a/pollor.Server/Models/AnswerModel.cs +++ b/pollor.Server/Models/AnswerModel.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace pollor.Server.Models @@ -10,6 +11,7 @@ public AnswerModel() { } public int poll_id { get; set; } + [StringLength(256)] public string? poll_answer { get; set; } [ForeignKey("answer_id")] // ForeignKey attribute in the VoteModel diff --git a/pollor.Server/Models/AuthModel.cs b/pollor.Server/Models/AuthModel.cs index f604307..1399d31 100644 --- a/pollor.Server/Models/AuthModel.cs +++ b/pollor.Server/Models/AuthModel.cs @@ -4,19 +4,19 @@ namespace pollor.Server.Models { public class RegisterModel { - [Required] + [Required, StringLength(256)] public string? emailaddress { get; set; } - [Required] + [Required, StringLength(64)] public string? username { get; set; } - [Required] + [Required, StringLength(128)] public string? password { get; set; } } public class LoginModel { - [Required] + [Required, StringLength(64)] public string? username { get; set; } - [Required] + [Required, StringLength(128)] public string? password { get; set; } public bool tokenLongerValid { get; set; } = false; } @@ -25,9 +25,9 @@ public class ChangePasswordModel { [Required] public int? id { get; set; } - [Required] + [Required, StringLength(128)] public string? newpassword { get; set; } - [Required] + [Required, StringLength(128)] public string? confirmPassword { get; set; } } } \ No newline at end of file diff --git a/pollor.Server/Models/PollModel.cs b/pollor.Server/Models/PollModel.cs index 16e0f90..953d2b3 100644 --- a/pollor.Server/Models/PollModel.cs +++ b/pollor.Server/Models/PollModel.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace pollor.Server.Models @@ -10,6 +11,7 @@ public PollModel() { } public int user_id { get; set; } + [StringLength(512)] public string? question { get; set; } public DateTime ending_date { get; set; } diff --git a/pollor.Server/Models/UserModel.cs b/pollor.Server/Models/UserModel.cs index fa671eb..f9184a8 100644 --- a/pollor.Server/Models/UserModel.cs +++ b/pollor.Server/Models/UserModel.cs @@ -5,7 +5,9 @@ namespace pollor.Server.Models { public class BaseUserModel : SuperModel { + [StringLength(64)] public string? username { get; set; } + [StringLength(256)] public string? emailaddress { get; set; } } @@ -15,7 +17,9 @@ public class UserModel: BaseUserModel public UserModel() { Polls = new List(); } + [StringLength(64)] public string? first_name { get; set; } + [StringLength(64)] public string? last_name { get; set; } [ForeignKey("user_id")] // ForeignKey attribute in the PollModel @@ -25,9 +29,9 @@ public UserModel() { [Table("users", Schema = "dbo")] public class UserAuthModel : BaseUserModel { - [DataType(DataType.Password)] + [DataType(DataType.Password), StringLength(128)] public string? password { get; set; } - [NotMapped] + [NotMapped, StringLength(128)] public string? confirmPassword { get; set; } } } \ No newline at end of file diff --git a/pollor.Server/Models/VoteModel.cs b/pollor.Server/Models/VoteModel.cs index 082befe..3f98f90 100644 --- a/pollor.Server/Models/VoteModel.cs +++ b/pollor.Server/Models/VoteModel.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace pollor.Server.Models @@ -6,8 +7,11 @@ 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(12)] public char[]? mac_address { get; set; } public DateTime voted_at { get; set; } } From 065d30a2649c253d7d44284861de7c0eeb6cc93e Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 04:30:39 +0100 Subject: [PATCH 09/28] Add post answer --- .../Controllers/AnswersController.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pollor.Server/Controllers/AnswersController.cs b/pollor.Server/Controllers/AnswersController.cs index 3183263..6a20282 100644 --- a/pollor.Server/Controllers/AnswersController.cs +++ b/pollor.Server/Controllers/AnswersController.cs @@ -3,6 +3,8 @@ using pollor.Server.Models; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore.ChangeTracking; namespace answeror.Server.Controllers { @@ -57,5 +59,26 @@ public IActionResult GetAnswerById(int id) return StatusCode(500, new { message = ex.Message}); } } + + [HttpPost("answer")] + [Authorize] + public IActionResult AddAnswer(AnswerModel answer) + { + try { + using (var context = new PollorDbContext()) { + EntityEntry newAnswer = context.Answers.Add(answer); + context.SaveChanges(); + + if (newAnswer == null) { + return NotFound(newAnswer); + } + return Created("answer/" + newAnswer.Entity.id.ToString(), newAnswer.Entity); + } + } + catch (Exception ex) { + _logger.LogError(ex, ex.Message); + return StatusCode(500, new { message = ex.Message}); + } + } } } From ad987fa06b302641816a743c41195949caae0686 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 04:31:03 +0100 Subject: [PATCH 10/28] Add post vote --- pollor.Server/Controllers/VotesController.cs | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pollor.Server/Controllers/VotesController.cs b/pollor.Server/Controllers/VotesController.cs index 69fe426..a89e1fd 100644 --- a/pollor.Server/Controllers/VotesController.cs +++ b/pollor.Server/Controllers/VotesController.cs @@ -1,5 +1,7 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.IdentityModel.Tokens; using pollor.Server.Models; using pollor.Server.Services; @@ -54,5 +56,26 @@ public IActionResult GetVoteById(int id) return StatusCode(500, new { message = ex.Message}); } } + + [HttpPost("vote")] + [Authorize] + public IActionResult AddVote(VoteModel vote) + { + try { + using (var context = new PollorDbContext()) { + EntityEntry newVote = context.Votes.Add(vote); + context.SaveChanges(); + + if (newVote == null) { + return NotFound(newVote); + } + return Created("vote/" + newVote.Entity.id.ToString(), newVote.Entity); + } + } + catch (Exception ex) { + _logger.LogError(ex, ex.Message); + return StatusCode(500, new { message = ex.Message}); + } + } } } From ba719be96129babe6a8e14fbee3d3cced0fb8487 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 04:31:39 +0100 Subject: [PATCH 11/28] Remove some LOC --- pollor.Server/Controllers/PollsController.cs | 2 -- pollor.Server/Models/AnswerModel.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pollor.Server/Controllers/PollsController.cs b/pollor.Server/Controllers/PollsController.cs index 1579691..e78ae55 100644 --- a/pollor.Server/Controllers/PollsController.cs +++ b/pollor.Server/Controllers/PollsController.cs @@ -72,8 +72,6 @@ public IActionResult AddPoll(PollModel poll) EntityEntry newPoll = context.Polls.Add(poll); context.SaveChanges(); - Console.WriteLine("newPoll: " + newPoll.Entity); - if (newPoll == null) { return NotFound(newPoll); } diff --git a/pollor.Server/Models/AnswerModel.cs b/pollor.Server/Models/AnswerModel.cs index 8ac0ba6..2f062b7 100644 --- a/pollor.Server/Models/AnswerModel.cs +++ b/pollor.Server/Models/AnswerModel.cs @@ -9,7 +9,7 @@ public partial class AnswerModel : SuperModel public AnswerModel() { Votes = new List(); } - + public int poll_id { get; set; } [StringLength(256)] public string? poll_answer { get; set; } From 939f222df8d0b0475b0bf84a79231183e8ec2a2a Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 05:05:13 +0100 Subject: [PATCH 12/28] Add user pages --- pollor.client/angular.json | 13 +++++---- pollor.client/package.json | 6 +--- pollor.client/src/app/app-routing.module.ts | 6 ++++ pollor.client/src/app/app.module.ts | 10 +++++-- .../src/app/footer/footer.component.html | 6 ++-- .../src/app/footer/footer.component.ts | 7 ----- .../src/app/header/header.component.html | 29 +++++++++++++++++++ .../src/app/header/header.component.ts | 1 - .../app/user-login/user-login.component.css | 0 .../app/user-login/user-login.component.html | 1 + .../user-login/user-login.component.spec.ts | 23 +++++++++++++++ .../app/user-login/user-login.component.ts | 10 +++++++ .../user-profile/user-profile.component.css | 0 .../user-profile/user-profile.component.html | 1 + .../user-profile.component.spec.ts | 23 +++++++++++++++ .../user-profile/user-profile.component.ts | 10 +++++++ .../user-register/user-register.component.css | 0 .../user-register.component.html | 1 + .../user-register.component.spec.ts | 23 +++++++++++++++ .../user-register/user-register.component.ts | 10 +++++++ 20 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 pollor.client/src/app/user-login/user-login.component.css create mode 100644 pollor.client/src/app/user-login/user-login.component.html create mode 100644 pollor.client/src/app/user-login/user-login.component.spec.ts create mode 100644 pollor.client/src/app/user-login/user-login.component.ts create mode 100644 pollor.client/src/app/user-profile/user-profile.component.css create mode 100644 pollor.client/src/app/user-profile/user-profile.component.html create mode 100644 pollor.client/src/app/user-profile/user-profile.component.spec.ts create mode 100644 pollor.client/src/app/user-profile/user-profile.component.ts create mode 100644 pollor.client/src/app/user-register/user-register.component.css create mode 100644 pollor.client/src/app/user-register/user-register.component.html create mode 100644 pollor.client/src/app/user-register/user-register.component.spec.ts create mode 100644 pollor.client/src/app/user-register/user-register.component.ts diff --git a/pollor.client/angular.json b/pollor.client/angular.json index 153c524..50aaee6 100644 --- a/pollor.client/angular.json +++ b/pollor.client/angular.json @@ -35,10 +35,12 @@ ], "styles": [ "src/styles.css", - "./node_modules/bootstrap/dist/css/bootstrap.min.css" + "./node_modules/bootstrap/dist/css/bootstrap.min.css", + "./node_modules/bootstrap-icons/font/bootstrap-icons.css" + ], "scripts": [ - "./node_modules/bootstrap/dist/js/bootstrap.min.js", + "./node_modules/bootstrap/dist/js/bootstrap.min.js" ] }, "configurations": { @@ -55,7 +57,7 @@ "maximumError": "4kb" } ], - "outputHashing": "all", + "outputHashing": "all" }, "development": { "optimization": false, @@ -96,10 +98,11 @@ ], "styles": [ "src/styles.css", - "node_modules/bootstrap/dist/css/bootstrap.min.css" + "node_modules/bootstrap/dist/css/bootstrap.min.css", + "./node_modules/bootstrap-icons/font/bootstrap-icons.css" ], "scripts": [ - "./node_modules/bootstrap/dist/js/bootstrap.min.js", + "./node_modules/bootstrap/dist/js/bootstrap.min.js" ], "karmaConfig": "karma.conf.js" } diff --git a/pollor.client/package.json b/pollor.client/package.json index 79cc7a3..6e9e205 100644 --- a/pollor.client/package.json +++ b/pollor.client/package.json @@ -23,14 +23,10 @@ "@angular/platform-browser": "^17.0.7", "@angular/platform-browser-dynamic": "^17.0.7", "@angular/router": "^17.0.7", - "@fortawesome/angular-fontawesome": "^0.14.1", - "@fortawesome/fontawesome-svg-core": "^6.4.2", - "@fortawesome/free-brands-svg-icons": "^6.4.2", - "@fortawesome/free-regular-svg-icons": "^6.4.2", - "@fortawesome/free-solid-svg-icons": "^6.4.2", "@ng-bootstrap/ng-bootstrap": "^16.0.0", "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.2", + "bootstrap-icons": "^1.11.3", "jest-editor-support": "*", "run-script-os": "*", "rxjs": "~7.8.0", diff --git a/pollor.client/src/app/app-routing.module.ts b/pollor.client/src/app/app-routing.module.ts index 5bd9e70..a622e0d 100644 --- a/pollor.client/src/app/app-routing.module.ts +++ b/pollor.client/src/app/app-routing.module.ts @@ -3,9 +3,15 @@ import { RouterModule, Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { PollsComponent } from './polls/polls.component'; +import { UserLoginComponent } from './user-login/user-login.component'; +import { UserRegisterComponent } from './user-register/user-register.component'; +import { UserProfileComponent } from './user-profile/user-profile.component'; const routes: Routes = [ { path: '', component: HomeComponent }, + { path: 'login', component: UserLoginComponent }, + { path: 'register', component: UserRegisterComponent }, + { path: 'profile', component: UserProfileComponent }, { path: 'polls', component: PollsComponent }, { path: '**', component: PageNotFoundComponent }, // route for 404 page ]; diff --git a/pollor.client/src/app/app.module.ts b/pollor.client/src/app/app.module.ts index 991ee97..9d6e1e8 100644 --- a/pollor.client/src/app/app.module.ts +++ b/pollor.client/src/app/app.module.ts @@ -10,8 +10,10 @@ import { HeaderComponent } from './header/header.component'; import { FooterComponent } from './footer/footer.component'; import { HomeComponent } from './home/home.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; -import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { PollsComponent } from './polls/polls.component'; +import { UserProfileComponent } from './user-profile/user-profile.component'; +import { UserLoginComponent } from './user-login/user-login.component'; +import { UserRegisterComponent } from './user-register/user-register.component'; @NgModule({ declarations: [ @@ -19,7 +21,10 @@ import { PollsComponent } from './polls/polls.component'; HeaderComponent, HomeComponent, PageNotFoundComponent, - PollsComponent + PollsComponent, + UserProfileComponent, + UserLoginComponent, + UserRegisterComponent ], imports: [ BrowserModule, HttpClientModule, @@ -28,7 +33,6 @@ import { PollsComponent } from './polls/polls.component'; RouterOutlet, RouterLink, RouterLinkActive, - FontAwesomeModule, FooterComponent ], providers: [], diff --git a/pollor.client/src/app/footer/footer.component.html b/pollor.client/src/app/footer/footer.component.html index 383f6df..a70c55a 100644 --- a/pollor.client/src/app/footer/footer.component.html +++ b/pollor.client/src/app/footer/footer.component.html @@ -10,8 +10,8 @@ href="#!" role="button" data-mdb-ripple-color="dark"> - - + + - + diff --git a/pollor.client/src/app/footer/footer.component.ts b/pollor.client/src/app/footer/footer.component.ts index 1e04a22..f043e96 100644 --- a/pollor.client/src/app/footer/footer.component.ts +++ b/pollor.client/src/app/footer/footer.component.ts @@ -1,18 +1,11 @@ import { Component } from '@angular/core'; import { environment } from './../../environments/environment'; -import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; -import { faLinkedin, faGithub } from '@fortawesome/free-brands-svg-icons'; - @Component({ selector: 'app-footer', standalone: true, - imports: [FontAwesomeModule], templateUrl: './footer.component.html', styleUrl: './footer.component.css' }) export class FooterComponent { public appVersion: String = environment.appVersion; - - faLinkedin = faLinkedin - faGithub = faGithub } diff --git a/pollor.client/src/app/header/header.component.html b/pollor.client/src/app/header/header.component.html index f2f37b3..7f99a9b 100644 --- a/pollor.client/src/app/header/header.component.html +++ b/pollor.client/src/app/header/header.component.html @@ -23,6 +23,35 @@

+ diff --git a/pollor.client/src/app/header/header.component.ts b/pollor.client/src/app/header/header.component.ts index 5051cf8..371e298 100644 --- a/pollor.client/src/app/header/header.component.ts +++ b/pollor.client/src/app/header/header.component.ts @@ -6,5 +6,4 @@ import { Component } from '@angular/core'; styleUrl: './header.component.css' }) export class HeaderComponent { - } diff --git a/pollor.client/src/app/user-login/user-login.component.css b/pollor.client/src/app/user-login/user-login.component.css new file mode 100644 index 0000000..e69de29 diff --git a/pollor.client/src/app/user-login/user-login.component.html b/pollor.client/src/app/user-login/user-login.component.html new file mode 100644 index 0000000..33b7de5 --- /dev/null +++ b/pollor.client/src/app/user-login/user-login.component.html @@ -0,0 +1 @@ +

user-login works!

diff --git a/pollor.client/src/app/user-login/user-login.component.spec.ts b/pollor.client/src/app/user-login/user-login.component.spec.ts new file mode 100644 index 0000000..900c4d9 --- /dev/null +++ b/pollor.client/src/app/user-login/user-login.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserLoginComponent } from './user-login.component'; + +describe('UserLoginComponent', () => { + let component: UserLoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UserLoginComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UserLoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pollor.client/src/app/user-login/user-login.component.ts b/pollor.client/src/app/user-login/user-login.component.ts new file mode 100644 index 0000000..a6e981a --- /dev/null +++ b/pollor.client/src/app/user-login/user-login.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-user-login', + templateUrl: './user-login.component.html', + styleUrl: './user-login.component.css' +}) +export class UserLoginComponent { + +} diff --git a/pollor.client/src/app/user-profile/user-profile.component.css b/pollor.client/src/app/user-profile/user-profile.component.css new file mode 100644 index 0000000..e69de29 diff --git a/pollor.client/src/app/user-profile/user-profile.component.html b/pollor.client/src/app/user-profile/user-profile.component.html new file mode 100644 index 0000000..fedcb8b --- /dev/null +++ b/pollor.client/src/app/user-profile/user-profile.component.html @@ -0,0 +1 @@ +

user-profile works!

diff --git a/pollor.client/src/app/user-profile/user-profile.component.spec.ts b/pollor.client/src/app/user-profile/user-profile.component.spec.ts new file mode 100644 index 0000000..9a8110b --- /dev/null +++ b/pollor.client/src/app/user-profile/user-profile.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserProfileComponent } from './user-profile.component'; + +describe('UserProfileComponent', () => { + let component: UserProfileComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UserProfileComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UserProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pollor.client/src/app/user-profile/user-profile.component.ts b/pollor.client/src/app/user-profile/user-profile.component.ts new file mode 100644 index 0000000..888fbf2 --- /dev/null +++ b/pollor.client/src/app/user-profile/user-profile.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-user-profile', + templateUrl: './user-profile.component.html', + styleUrl: './user-profile.component.css' +}) +export class UserProfileComponent { + +} diff --git a/pollor.client/src/app/user-register/user-register.component.css b/pollor.client/src/app/user-register/user-register.component.css new file mode 100644 index 0000000..e69de29 diff --git a/pollor.client/src/app/user-register/user-register.component.html b/pollor.client/src/app/user-register/user-register.component.html new file mode 100644 index 0000000..9f0da31 --- /dev/null +++ b/pollor.client/src/app/user-register/user-register.component.html @@ -0,0 +1 @@ +

user-register works!

diff --git a/pollor.client/src/app/user-register/user-register.component.spec.ts b/pollor.client/src/app/user-register/user-register.component.spec.ts new file mode 100644 index 0000000..8ba536d --- /dev/null +++ b/pollor.client/src/app/user-register/user-register.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserRegisterComponent } from './user-register.component'; + +describe('UserRegisterComponent', () => { + let component: UserRegisterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UserRegisterComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UserRegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pollor.client/src/app/user-register/user-register.component.ts b/pollor.client/src/app/user-register/user-register.component.ts new file mode 100644 index 0000000..3ff5c66 --- /dev/null +++ b/pollor.client/src/app/user-register/user-register.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-user-register', + templateUrl: './user-register.component.html', + styleUrl: './user-register.component.css' +}) +export class UserRegisterComponent { + +} From cb94d96bfe31865bfc995676ff65eb0ea0fea5f5 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 14:37:59 +0100 Subject: [PATCH 13/28] Add alert message --- .../src/app/alert-message/alert-message.css | 7 +++ .../src/app/alert-message/alert-message.html | 5 ++ .../src/app/alert-message/alert-message.ts | 59 +++++++++++++++++++ pollor.client/src/app/app.component.html | 1 + pollor.client/src/app/app.module.ts | 4 +- .../src/app/footer/footer.component.css | 1 + .../src/app/polls/polls.component.html | 2 +- .../src/app/polls/polls.component.ts | 12 +++- 8 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 pollor.client/src/app/alert-message/alert-message.css create mode 100644 pollor.client/src/app/alert-message/alert-message.html create mode 100644 pollor.client/src/app/alert-message/alert-message.ts diff --git a/pollor.client/src/app/alert-message/alert-message.css b/pollor.client/src/app/alert-message/alert-message.css new file mode 100644 index 0000000..ee8a674 --- /dev/null +++ b/pollor.client/src/app/alert-message/alert-message.css @@ -0,0 +1,7 @@ +.alert-messages { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 30%; + z-index: 120; +} \ No newline at end of file diff --git a/pollor.client/src/app/alert-message/alert-message.html b/pollor.client/src/app/alert-message/alert-message.html new file mode 100644 index 0000000..0311900 --- /dev/null +++ b/pollor.client/src/app/alert-message/alert-message.html @@ -0,0 +1,5 @@ +
+ @for (alert of getAlertMessages(); track alert) { +
+ } +
\ No newline at end of file diff --git a/pollor.client/src/app/alert-message/alert-message.ts b/pollor.client/src/app/alert-message/alert-message.ts new file mode 100644 index 0000000..cc2847a --- /dev/null +++ b/pollor.client/src/app/alert-message/alert-message.ts @@ -0,0 +1,59 @@ +import { Component } from '@angular/core'; +import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; + +interface Alert { + type: string; + message: string; +} + +const ALERTS: Alert[] = [ + // { + // type: 'info', + // message: 'This is an test alert', + // } +]; + +@Component({ + selector: 'ngbd-alert-message', + standalone: true, + imports: [NgbAlertModule], + templateUrl: './alert-message.html', + styleUrl: './alert-message.css' + +}) +export class AlertMessage { + static alerts: Alert[] = ALERTS; + + constructor() { + this.reset(); + } + + close(alert: Alert) { + AlertMessage.alerts.splice(AlertMessage.alerts.indexOf(alert), 1); + } + + reset() { + AlertMessage.alerts = []; + } + + static addAlert(alertType: string, alertMessage: string, timeout: number = 10000) { + const newAlert : Alert = { + type: alertType, + message: alertMessage + }; + + AlertMessage.alerts.unshift(newAlert); + + setTimeout(() => { + AlertMessage.alerts.splice(AlertMessage.alerts.indexOf(newAlert), 1); + }, timeout); // automatic close the alert after x miliseconds based + } + + static addErrorAlert(alertMessage: string) { + AlertMessage.addAlert("danger", "An error occured:
" + alertMessage); + } + + getAlertMessages() { + return AlertMessage.alerts; + } +} diff --git a/pollor.client/src/app/app.component.html b/pollor.client/src/app/app.component.html index 51ee6ff..54f0477 100644 --- a/pollor.client/src/app/app.component.html +++ b/pollor.client/src/app/app.component.html @@ -3,5 +3,6 @@
+ \ No newline at end of file diff --git a/pollor.client/src/app/app.module.ts b/pollor.client/src/app/app.module.ts index 9d6e1e8..ef6792b 100644 --- a/pollor.client/src/app/app.module.ts +++ b/pollor.client/src/app/app.module.ts @@ -14,6 +14,7 @@ import { PollsComponent } from './polls/polls.component'; import { UserProfileComponent } from './user-profile/user-profile.component'; import { UserLoginComponent } from './user-login/user-login.component'; import { UserRegisterComponent } from './user-register/user-register.component'; +import { AlertMessage } from './alert-message/alert-message'; @NgModule({ declarations: [ @@ -33,7 +34,8 @@ import { UserRegisterComponent } from './user-register/user-register.component'; RouterOutlet, RouterLink, RouterLinkActive, - FooterComponent + FooterComponent, + AlertMessage ], providers: [], bootstrap: [AppComponent] diff --git a/pollor.client/src/app/footer/footer.component.css b/pollor.client/src/app/footer/footer.component.css index 3f67524..041b5d0 100644 --- a/pollor.client/src/app/footer/footer.component.css +++ b/pollor.client/src/app/footer/footer.component.css @@ -3,4 +3,5 @@ bottom: 0; width: 100%; height: 10rem; /* Footer height, must be same or lower as in app.component.css */ + z-index: 50; } \ No newline at end of file diff --git a/pollor.client/src/app/polls/polls.component.html b/pollor.client/src/app/polls/polls.component.html index 5f6d060..a263d66 100644 --- a/pollor.client/src/app/polls/polls.component.html +++ b/pollor.client/src/app/polls/polls.component.html @@ -2,7 +2,7 @@

Polls

-

Loading POLLS...

+

{{ pollLoadingMsg }}

no poll data found

diff --git a/pollor.client/src/app/polls/polls.component.ts b/pollor.client/src/app/polls/polls.component.ts index 57a90ea..3d5dfba 100644 --- a/pollor.client/src/app/polls/polls.component.ts +++ b/pollor.client/src/app/polls/polls.component.ts @@ -1,9 +1,8 @@ import { Component } from '@angular/core'; import { ApiService } from '../../services/api.service'; import { IPolls } from '../../interfaces/polls.interface'; -import { IAnswers } from '../../interfaces/answers.interface'; -import { IVotes } from '../../interfaces/votes.interface'; +import { AlertMessage } from '../alert-message/alert-message'; @Component({ selector: 'app-polls', @@ -14,6 +13,8 @@ export class PollsComponent { public polls: IPolls[] = []; public pollsLoaded: boolean = false; + public pollLoadingMsg: string = "Loading polls..."; + public pollLoadingColor: string = ""; constructor(private apiService: ApiService) { } @@ -28,7 +29,12 @@ export class PollsComponent { this.polls = response; this.pollsLoaded = true; }, - error: (error) => console.error(error), + error: (err) => { + this.pollLoadingMsg = err.status + ' - ' + err.message; + this.pollLoadingColor = "red"; + console.error(err); + AlertMessage.addErrorAlert(err.error.message); + }, //complete: () => { } }); } From 71cb6d9afc3852bcd3bed316ad3c0b0fad8f75b9 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 14:44:42 +0100 Subject: [PATCH 14/28] Add title to alert --- pollor.client/src/app/alert-message/alert-message.html | 6 +++++- pollor.client/src/app/alert-message/alert-message.ts | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pollor.client/src/app/alert-message/alert-message.html b/pollor.client/src/app/alert-message/alert-message.html index 0311900..0cc3abf 100644 --- a/pollor.client/src/app/alert-message/alert-message.html +++ b/pollor.client/src/app/alert-message/alert-message.html @@ -1,5 +1,9 @@
@for (alert of getAlertMessages(); track alert) { -
+ +

+
+

+
}
\ No newline at end of file diff --git a/pollor.client/src/app/alert-message/alert-message.ts b/pollor.client/src/app/alert-message/alert-message.ts index cc2847a..34a7d80 100644 --- a/pollor.client/src/app/alert-message/alert-message.ts +++ b/pollor.client/src/app/alert-message/alert-message.ts @@ -3,12 +3,14 @@ import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'; interface Alert { type: string; + title: string; message: string; } const ALERTS: Alert[] = [ // { // type: 'info', + // title: 'Test alert', // message: 'This is an test alert', // } ]; @@ -36,9 +38,10 @@ export class AlertMessage { AlertMessage.alerts = []; } - static addAlert(alertType: string, alertMessage: string, timeout: number = 10000) { + static addAlert(alertType: string, alertTitle: string, alertMessage: string, timeout: number = 10000) { const newAlert : Alert = { type: alertType, + title: alertTitle, message: alertMessage }; @@ -50,7 +53,7 @@ export class AlertMessage { } static addErrorAlert(alertMessage: string) { - AlertMessage.addAlert("danger", "An error occured:
" + alertMessage); + AlertMessage.addAlert("danger", "An error occured", alertMessage); } getAlertMessages() { From 09d4cb80b12dfadb6e277ac9581c9a77a34444cf Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 16:12:31 +0100 Subject: [PATCH 15/28] Add Login form --- pollor.client/src/app/app-routing.module.ts | 6 +- pollor.client/src/app/app.module.ts | 2 + .../src/app/header/header.component.html | 6 +- .../app/user-login/user-login.component.css | 4 ++ .../app/user-login/user-login.component.html | 34 +++++++++- .../app/user-login/user-login.component.ts | 64 +++++++++++++++++++ pollor.client/src/services/auth.service.ts | 23 +++++++ 7 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 pollor.client/src/services/auth.service.ts diff --git a/pollor.client/src/app/app-routing.module.ts b/pollor.client/src/app/app-routing.module.ts index a622e0d..8c78486 100644 --- a/pollor.client/src/app/app-routing.module.ts +++ b/pollor.client/src/app/app-routing.module.ts @@ -9,9 +9,9 @@ import { UserProfileComponent } from './user-profile/user-profile.component'; const routes: Routes = [ { path: '', component: HomeComponent }, - { path: 'login', component: UserLoginComponent }, - { path: 'register', component: UserRegisterComponent }, - { path: 'profile', component: UserProfileComponent }, + { path: 'account/login', component: UserLoginComponent }, + { path: 'account/register', component: UserRegisterComponent }, + { path: 'account/profile', component: UserProfileComponent }, { path: 'polls', component: PollsComponent }, { path: '**', component: PageNotFoundComponent }, // route for 404 page ]; diff --git a/pollor.client/src/app/app.module.ts b/pollor.client/src/app/app.module.ts index ef6792b..2129a41 100644 --- a/pollor.client/src/app/app.module.ts +++ b/pollor.client/src/app/app.module.ts @@ -2,6 +2,7 @@ import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -29,6 +30,7 @@ import { AlertMessage } from './alert-message/alert-message'; ], imports: [ BrowserModule, HttpClientModule, + FormsModule, ReactiveFormsModule, AppRoutingModule, NgbModule, RouterOutlet, diff --git a/pollor.client/src/app/header/header.component.html b/pollor.client/src/app/header/header.component.html index 7f99a9b..9855025 100644 --- a/pollor.client/src/app/header/header.component.html +++ b/pollor.client/src/app/header/header.component.html @@ -28,7 +28,7 @@

@@ -37,7 +37,7 @@

@@ -46,7 +46,7 @@

diff --git a/pollor.client/src/app/user-login/user-login.component.css b/pollor.client/src/app/user-login/user-login.component.css index e69de29..07e9ae1 100644 --- a/pollor.client/src/app/user-login/user-login.component.css +++ b/pollor.client/src/app/user-login/user-login.component.css @@ -0,0 +1,4 @@ +.card { + max-width: 500px; + margin: 0 auto; +} \ No newline at end of file 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 33b7de5..4d1f2ec 100644 --- a/pollor.client/src/app/user-login/user-login.component.html +++ b/pollor.client/src/app/user-login/user-login.component.html @@ -1 +1,33 @@ -

user-login works!

+
+
Login to POLLOR
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+ {{ loginError }} +
+
\ No newline at end of file diff --git a/pollor.client/src/app/user-login/user-login.component.ts b/pollor.client/src/app/user-login/user-login.component.ts index a6e981a..80f0a89 100644 --- a/pollor.client/src/app/user-login/user-login.component.ts +++ b/pollor.client/src/app/user-login/user-login.component.ts @@ -1,4 +1,9 @@ import { Component } from '@angular/core'; +import { AuthService } from '../../services/auth.service'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { finalize } from 'rxjs/operators'; +import { AlertMessage } from '../alert-message/alert-message'; @Component({ selector: 'app-user-login', @@ -6,5 +11,64 @@ import { Component } from '@angular/core'; styleUrl: './user-login.component.css' }) export class UserLoginComponent { + loginError: string = ''; + loading: boolean = false; + loginForm: FormGroup; + constructor( + private formBuilder: FormBuilder, + private authService: AuthService, + private router: Router + ) { + this.loginForm = formBuilder.group({ + username: ["", Validators.required], + password: ["", Validators.required], + stayLoggedIn: [false, Validators.required] + }); + } + + sendLogin(): void { + if (this.loginForm.valid) { + console.log('this.loginForm.valid', this.loginForm.valid); + this.loading = true; // Start the loading spinner + + console.log('this.loginForm.value', this.loginForm.value); + + this.authService + .login(this.loginForm.value) + .pipe( + finalize(() => { + this.loading = false; //Stop the loading spinner + }) + ) + .subscribe({ + next: (res: any) => { + console.log('Response:', res); + if (res.success) { + console.log('Show me the success - ', res.role, res) + localStorage.setItem('token', res.token); + localStorage.setItem('role', res.role); + this.loginError = ''; + this.loginForm.reset(); + this.navigateDashboard(res.role); + } else { + this.loginError = 'Invalid email and password combination!'; + } + + }, + error: (error: any) => { + this.loginError = 'An error occurred during login.'; + console.error('Login Error:', error); + AlertMessage.addErrorAlert(error.message); + }, + }); + } + } + + navigateDashboard(role: string): void { + const dashboardRoute = + role === 'admin' ? '/admindashboard' : '/userdashboard'; + this.router.navigate([dashboardRoute]); + console.log(`${role} dashboard route`); + } } diff --git a/pollor.client/src/services/auth.service.ts b/pollor.client/src/services/auth.service.ts new file mode 100644 index 0000000..cabda54 --- /dev/null +++ b/pollor.client/src/services/auth.service.ts @@ -0,0 +1,23 @@ +import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { ApiService } from './api.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + constructor(private apiService: ApiService) {} + + login(body: any): Observable { + return this.apiService.post('api/auth/login', body); + } + + register(body: any): Observable { + return this.apiService.post('api/auth/register', body); + } + + logout(body: any): Observable { + return this.apiService.post('api/auth/logout', body); + } + +} From 943356e4dcb738a4a77f489835fa27869ca3efb2 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 16:20:32 +0100 Subject: [PATCH 16/28] Add files to .sln --- pollor.client/package-lock.json | 50 ++++----------------------------- pollor.sln | 9 ++++++ 2 files changed, 14 insertions(+), 45 deletions(-) diff --git a/pollor.client/package-lock.json b/pollor.client/package-lock.json index 9ea2a10..9e4681d 100644 --- a/pollor.client/package-lock.json +++ b/pollor.client/package-lock.json @@ -1929,51 +1929,6 @@ "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", "dev": true }, - "@fortawesome/angular-fontawesome": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.14.1.tgz", - "integrity": "sha512-Yb5HLiEOAxjSLEcaOM51CKIrzdfvoDafXVJERm9vufxfZkVZPZJgrZRgqwLVpejgq4/Ez6TqHZ6SqmJwdtRF6g==", - "requires": { - "tslib": "^2.6.2" - } - }, - "@fortawesome/fontawesome-common-types": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", - "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==" - }, - "@fortawesome/fontawesome-svg-core": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", - "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", - "requires": { - "@fortawesome/fontawesome-common-types": "6.5.1" - } - }, - "@fortawesome/free-brands-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.1.tgz", - "integrity": "sha512-093l7DAkx0aEtBq66Sf19MgoZewv1zeY9/4C7vSKPO4qMwEsW/2VYTUTpBtLwfb9T2R73tXaRDPmE4UqLCYHfg==", - "requires": { - "@fortawesome/fontawesome-common-types": "6.5.1" - } - }, - "@fortawesome/free-regular-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.1.tgz", - "integrity": "sha512-m6ShXn+wvqEU69wSP84coxLbNl7sGVZb+Ca+XZq6k30SzuP3X4TfPqtycgUh9ASwlNh5OfQCd8pDIWxl+O+LlQ==", - "requires": { - "@fortawesome/fontawesome-common-types": "6.5.1" - } - }, - "@fortawesome/free-solid-svg-icons": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", - "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", - "requires": { - "@fortawesome/fontawesome-common-types": "6.5.1" - } - }, "@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -3452,6 +3407,11 @@ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz", "integrity": "sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==" }, + "bootstrap-icons": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz", + "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/pollor.sln b/pollor.sln index 51f6912..aed91dc 100644 --- a/pollor.sln +++ b/pollor.sln @@ -11,9 +11,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "database", "database", "{98 ProjectSection(SolutionItems) = preProject database\drop-tables.sql = database\drop-tables.sql database\migration.sql = database\migration.sql + database\README.md = database\README.md database\seed.sql = database\seed.sql EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B9096682-FD32-481C-ACA9-8F8AE88BD0D6}" + ProjectSection(SolutionItems) = preProject + .gitattributes = .gitattributes + .gitignore = .gitignore + LICENSE.txt = LICENSE.txt + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From b8c80fd73fab95480176b6022d3d0277a35e6832 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 16:33:01 +0100 Subject: [PATCH 17/28] Fix files for creation of database --- database/README.md | 2 +- database/migration.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/database/README.md b/database/README.md index 7950470..adfdf56 100644 --- a/database/README.md +++ b/database/README.md @@ -18,7 +18,7 @@ MS SQL database ``` ``` - docker run -d --name POLLOR_DATABASE -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=myPassw0rd' -p 1433:1433 mcr.microsoft.com/mssql/server:2022-latest + docker run -d --name POLLOR_DATABASE -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=myPassw0rd" -p 1433:1433 mcr.microsoft.com/mssql/server:2022-latest ``` ``` diff --git a/database/migration.sql b/database/migration.sql index 2aaadd9..6722671 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, [created_at] [datetime] NOT NULL, - CONSTRAINT PK_users PRIMARY KEY NONCLUSTERED (id) + CONSTRAINT PK_users PRIMARY KEY NONCLUSTERED (id), CONSTRAINT UC_Users UNIQUE (id,emailaddress,username) ) ON [PRIMARY] GO From bff52a032d7586d897dcec6fd80a68eeec8ce3eb Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 16:35:07 +0100 Subject: [PATCH 18/28] Add jwt .env value empty exception --- pollor.Server/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pollor.Server/Program.cs b/pollor.Server/Program.cs index 4925091..6bf5444 100644 --- a/pollor.Server/Program.cs +++ b/pollor.Server/Program.cs @@ -34,6 +34,13 @@ /* get secret private jwt key value */ String secretJwtKey = Environment.GetEnvironmentVariable("SECRET_JWT_KEY")!; +/* If values are null or empty, give error message that values are missing in .env */ +if (secretJwtKey == null) +{ + throw new InvalidOperationException("secretJwtKey contains no value. Make sure SECRET_JWT_KEY is set in .env"); +} + + /* Add JWT authentication */ builder.Services.AddAuthentication(opt => { opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; From f979a987fe0a4388da5c4459b5faab4305dd111c Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 16:56:54 +0100 Subject: [PATCH 19/28] Fix POST to backend --- database/migration.sql | 1 + database/seed.sql | 4 ++-- pollor.Server/Program.cs | 5 ++-- .../app/user-login/user-login.component.html | 6 ++--- .../app/user-login/user-login.component.ts | 23 ++++++++----------- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/database/migration.sql b/database/migration.sql index 6722671..295171e 100644 --- a/database/migration.sql +++ b/database/migration.sql @@ -18,6 +18,7 @@ CREATE TABLE [dbo].[users]( [password] [nvarchar](128) NOT NULL, [first_name] [nvarchar](64) NULL, [last_name] [nvarchar](64) NULL, + [role] [nvarchar](32) NULL DEFAULT 'user', [created_at] [datetime] NOT NULL, CONSTRAINT PK_users PRIMARY KEY NONCLUSTERED (id), CONSTRAINT UC_Users UNIQUE (id,emailaddress,username) diff --git a/database/seed.sql b/database/seed.sql index f6ed6d2..34c4451 100644 --- a/database/seed.sql +++ b/database/seed.sql @@ -1,8 +1,8 @@ USE [pollor_db] GO -INSERT INTO users (emailaddress, first_name, last_name, profile_username, created_at) -VALUES ('test@test.nl', 'Tester', 'Test', 'Testing', '1970-01-01T00:00:01'); +INSERT INTO users (emailaddress, first_name, last_name, username, password, created_at) +VALUES ('test@test.nl', 'Tester', 'Test', 'Testing', '', '1970-01-01T00:00:01'); GO INSERT INTO polls (user_id, question, ending_date, created_at) diff --git a/pollor.Server/Program.cs b/pollor.Server/Program.cs index 6bf5444..2fbd217 100644 --- a/pollor.Server/Program.cs +++ b/pollor.Server/Program.cs @@ -27,7 +27,9 @@ options.AddDefaultPolicy( policy => { - policy.WithOrigins(corsDomains); // loading array of .env values as allowed CORS domains + policy.WithOrigins(corsDomains) // loading array of .env values as allowed CORS domains + .AllowAnyHeader() + .AllowAnyMethod(); }); }); @@ -40,7 +42,6 @@ throw new InvalidOperationException("secretJwtKey contains no value. Make sure SECRET_JWT_KEY is set in .env"); } - /* Add JWT authentication */ builder.Services.AddAuthentication(opt => { opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 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 4d1f2ec..48fb835 100644 --- a/pollor.client/src/app/user-login/user-login.component.html +++ b/pollor.client/src/app/user-login/user-login.component.html @@ -4,7 +4,7 @@
Login to POLLOR

- +
@@ -12,7 +12,7 @@
Login to POLLOR
- + @@ -30,4 +30,4 @@
Login to POLLOR
{{ loginError }}
- \ No newline at end of file + diff --git a/pollor.client/src/app/user-login/user-login.component.ts b/pollor.client/src/app/user-login/user-login.component.ts index 80f0a89..e5fef71 100644 --- a/pollor.client/src/app/user-login/user-login.component.ts +++ b/pollor.client/src/app/user-login/user-login.component.ts @@ -21,9 +21,9 @@ export class UserLoginComponent { private router: Router ) { this.loginForm = formBuilder.group({ - username: ["", Validators.required], - password: ["", Validators.required], - stayLoggedIn: [false, Validators.required] + username: ["", Validators.required], + password: ["", Validators.required], + tokenLongerValid: [false, Validators.required] }); } @@ -44,17 +44,12 @@ export class UserLoginComponent { .subscribe({ next: (res: any) => { console.log('Response:', res); - if (res.success) { - console.log('Show me the success - ', res.role, res) - localStorage.setItem('token', res.token); - localStorage.setItem('role', res.role); - this.loginError = ''; - this.loginForm.reset(); - this.navigateDashboard(res.role); - } else { - this.loginError = 'Invalid email and password combination!'; - } - + console.log('Show me the success - ', res.role, res) + localStorage.setItem('token', res.token); + localStorage.setItem('role', res.role); + this.loginError = ''; + this.loginForm.reset(); + this.navigateDashboard(res.role); }, error: (error: any) => { this.loginError = 'An error occurred during login.'; From c35957267e34a1aee2ce145bba3baec2c5792324 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 17:09:10 +0100 Subject: [PATCH 20/28] Add role and an extra user page --- pollor.Server/Models/UserModel.cs | 1 + pollor.client/src/app/app-routing.module.ts | 2 ++ pollor.client/src/app/app.module.ts | 4 +++- .../user-admin-profile.component.css | 0 .../user-admin-profile.component.html | 1 + .../user-admin-profile.component.spec.ts | 23 +++++++++++++++++++ .../user-admin-profile.component.ts | 10 ++++++++ .../app/user-login/user-login.component.ts | 20 +++++++++------- 8 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 pollor.client/src/app/user-admin-profile/user-admin-profile.component.css create mode 100644 pollor.client/src/app/user-admin-profile/user-admin-profile.component.html create mode 100644 pollor.client/src/app/user-admin-profile/user-admin-profile.component.spec.ts create mode 100644 pollor.client/src/app/user-admin-profile/user-admin-profile.component.ts diff --git a/pollor.Server/Models/UserModel.cs b/pollor.Server/Models/UserModel.cs index f9184a8..0f9c96b 100644 --- a/pollor.Server/Models/UserModel.cs +++ b/pollor.Server/Models/UserModel.cs @@ -21,6 +21,7 @@ public UserModel() { public string? first_name { get; set; } [StringLength(64)] public string? last_name { get; set; } + public string? role { get; set; } [ForeignKey("user_id")] // ForeignKey attribute in the PollModel public virtual ICollection Polls { get; set; } diff --git a/pollor.client/src/app/app-routing.module.ts b/pollor.client/src/app/app-routing.module.ts index 8c78486..67b49c5 100644 --- a/pollor.client/src/app/app-routing.module.ts +++ b/pollor.client/src/app/app-routing.module.ts @@ -6,12 +6,14 @@ import { PollsComponent } from './polls/polls.component'; import { UserLoginComponent } from './user-login/user-login.component'; 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'; const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'account/login', component: UserLoginComponent }, { path: 'account/register', component: UserRegisterComponent }, { path: 'account/profile', component: UserProfileComponent }, + { path: 'account/admin-profile', component: UserAdminProfileComponent }, { path: 'polls', component: PollsComponent }, { path: '**', component: PageNotFoundComponent }, // route for 404 page ]; diff --git a/pollor.client/src/app/app.module.ts b/pollor.client/src/app/app.module.ts index 2129a41..eb1d2d3 100644 --- a/pollor.client/src/app/app.module.ts +++ b/pollor.client/src/app/app.module.ts @@ -16,6 +16,7 @@ import { UserProfileComponent } from './user-profile/user-profile.component'; import { UserLoginComponent } from './user-login/user-login.component'; 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'; @NgModule({ declarations: [ @@ -26,7 +27,8 @@ import { AlertMessage } from './alert-message/alert-message'; PollsComponent, UserProfileComponent, UserLoginComponent, - UserRegisterComponent + UserRegisterComponent, + UserAdminProfileComponent ], imports: [ BrowserModule, HttpClientModule, diff --git a/pollor.client/src/app/user-admin-profile/user-admin-profile.component.css b/pollor.client/src/app/user-admin-profile/user-admin-profile.component.css new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..b9ae287 --- /dev/null +++ b/pollor.client/src/app/user-admin-profile/user-admin-profile.component.html @@ -0,0 +1 @@ +

user-admin-profile works!

diff --git a/pollor.client/src/app/user-admin-profile/user-admin-profile.component.spec.ts b/pollor.client/src/app/user-admin-profile/user-admin-profile.component.spec.ts new file mode 100644 index 0000000..1bfea6d --- /dev/null +++ b/pollor.client/src/app/user-admin-profile/user-admin-profile.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserAdminProfileComponent } from './user-admin-profile.component'; + +describe('UserAdminProfileComponent', () => { + let component: UserAdminProfileComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UserAdminProfileComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UserAdminProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pollor.client/src/app/user-admin-profile/user-admin-profile.component.ts b/pollor.client/src/app/user-admin-profile/user-admin-profile.component.ts new file mode 100644 index 0000000..caf64a0 --- /dev/null +++ b/pollor.client/src/app/user-admin-profile/user-admin-profile.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-user-admin-profile', + templateUrl: './user-admin-profile.component.html', + styleUrl: './user-admin-profile.component.css' +}) +export class UserAdminProfileComponent { + +} diff --git a/pollor.client/src/app/user-login/user-login.component.ts b/pollor.client/src/app/user-login/user-login.component.ts index e5fef71..4172874 100644 --- a/pollor.client/src/app/user-login/user-login.component.ts +++ b/pollor.client/src/app/user-login/user-login.component.ts @@ -25,15 +25,20 @@ export class UserLoginComponent { password: ["", Validators.required], tokenLongerValid: [false, Validators.required] }); + + const token = localStorage.getItem('token'); + const role = localStorage.getItem('role'); + + if (token && role) { + // todo: check if token if OK + // navigate to role profile page + this.navigateDashboard(role); + } } sendLogin(): void { if (this.loginForm.valid) { - console.log('this.loginForm.valid', this.loginForm.valid); this.loading = true; // Start the loading spinner - - console.log('this.loginForm.value', this.loginForm.value); - this.authService .login(this.loginForm.value) .pipe( @@ -44,12 +49,11 @@ export class UserLoginComponent { .subscribe({ next: (res: any) => { console.log('Response:', res); - console.log('Show me the success - ', res.role, res) localStorage.setItem('token', res.token); - localStorage.setItem('role', res.role); + localStorage.setItem('role', res.user.role); this.loginError = ''; this.loginForm.reset(); - this.navigateDashboard(res.role); + this.navigateDashboard(res.user.role); }, error: (error: any) => { this.loginError = 'An error occurred during login.'; @@ -62,7 +66,7 @@ export class UserLoginComponent { navigateDashboard(role: string): void { const dashboardRoute = - role === 'admin' ? '/admindashboard' : '/userdashboard'; + role === 'admin' ? '/account/admin-profile' : '/account/profile'; this.router.navigate([dashboardRoute]); console.log(`${role} dashboard route`); } From abad357735f95685aee47a48729182523f86a604 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Fri, 2 Feb 2024 21:47:03 +0100 Subject: [PATCH 21/28] Add validation api route --- pollor.Server/Controllers/AuthController.cs | 77 ++++++++++++++++++- pollor.Server/Models/AuthModel.cs | 7 ++ pollor.Server/Models/UserModel.cs | 4 +- pollor.client/src/app/app.module.ts | 18 ++++- .../app/user-login/user-login.component.ts | 27 ++++++- .../src/interfaces/auth.interface.ts | 4 + .../src/services/auth.interceptor.ts | 25 ++++++ pollor.client/src/services/auth.service.ts | 14 ++++ 8 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 pollor.client/src/interfaces/auth.interface.ts create mode 100644 pollor.client/src/services/auth.interceptor.ts diff --git a/pollor.Server/Controllers/AuthController.cs b/pollor.Server/Controllers/AuthController.cs index 84be035..e92f55a 100644 --- a/pollor.Server/Controllers/AuthController.cs +++ b/pollor.Server/Controllers/AuthController.cs @@ -7,11 +7,24 @@ using pollor.Server.Models; using pollor.Server.Services; using Microsoft.EntityFrameworkCore.ChangeTracking; +using System.Security.Principal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.AspNetCore.Authorization; +using pollor.Server.Controllers; +using System.Data; [Route("api/auth")] [ApiController] public class AuthController : ControllerBase { + private readonly ILogger _logger; + + public AuthController(ILogger logger) + { + _logger = logger; + } + + [HttpPost("register")] public IActionResult Register([FromBody] RegisterModel registerUser) { @@ -96,11 +109,58 @@ public IActionResult Login([FromBody] LoginModel loginUser) return Unauthorized(); } + [HttpPost("validate")] + [Authorize] + public IActionResult Validate([FromBody] ValidateTokenModel validateTokenModel) + { + // Retrieve the JWT token from the Authorization header + var token = HttpContext.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + + if (string.IsNullOrEmpty(token)) + { + return BadRequest(new { message = "Token is missing" }); + } + + SecurityToken validatedToken; + IPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(token, GetValidationParameters(), out validatedToken); + + // The user is authenticated, and you can access user information + var userClaims = HttpContext.User; + // Perform additional validation or return success response + + var username = userClaims.Claims.Where(e => e.Type.EndsWith("identity/claims/name")).Select(e => e.Value).SingleOrDefault(); + var userId = userClaims.Claims.Where(e => e.Type.EndsWith("identity/claims/nameidentifier")).Select(e => e.Value).SingleOrDefault(); + var userRole = userClaims.Claims.Where(e => e.Type.EndsWith("identity/claims/role")).Select(e => e.Value).SingleOrDefault(); + + try + { + using (var context = new PollorDbContext()) + { + UserModel? user = context.Users + .Where(u => u.id.Equals(userId)) + .Where(u => u.username.Equals(username)) + .Where(u => u.role.Equals(userRole)) + .FirstOrDefault(); + if (user == null) + { + return NotFound("User not found"); + } + return Ok(new AuthenticatedResponse { token = validateTokenModel.token, user = user }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, new { message = ex.Message }); + } + } + private JwtSecurityToken GetJwtTokenOptions (int tokenValidForXDays, UserAuthModel user) { var jwtClaims = new List { new Claim(ClaimTypes.Name, user.username!), - new Claim(ClaimTypes.NameIdentifier, user.id.ToString()) + new Claim(ClaimTypes.NameIdentifier, user.id.ToString()), + new Claim(ClaimTypes.Role, user.role!) }; var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET_JWT_KEY")!)); @@ -114,4 +174,19 @@ private JwtSecurityToken GetJwtTokenOptions (int tokenValidForXDays, UserAuthMod ); return tokeOptions; } + + private static TokenValidationParameters GetValidationParameters() + { + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET_JWT_KEY")!)); + + return new TokenValidationParameters() + { + ValidateLifetime = true, + ValidateAudience = true, + ValidateIssuer = true, + ValidIssuer = "https://localhost:5001", + ValidAudience = "https://localhost:5001", + IssuerSigningKey = secretKey // The same key as the one that generate the token + }; + } } \ No newline at end of file diff --git a/pollor.Server/Models/AuthModel.cs b/pollor.Server/Models/AuthModel.cs index 1399d31..8105c6b 100644 --- a/pollor.Server/Models/AuthModel.cs +++ b/pollor.Server/Models/AuthModel.cs @@ -30,4 +30,11 @@ public class ChangePasswordModel [Required, StringLength(128)] public string? confirmPassword { get; set; } } + + public class ValidateTokenModel + { + public string? token { get; set; } + [Required, StringLength(32)] + public string? role { get; set; } + } } \ No newline at end of file diff --git a/pollor.Server/Models/UserModel.cs b/pollor.Server/Models/UserModel.cs index 0f9c96b..2eaf912 100644 --- a/pollor.Server/Models/UserModel.cs +++ b/pollor.Server/Models/UserModel.cs @@ -9,6 +9,9 @@ public class BaseUserModel : SuperModel public string? username { get; set; } [StringLength(256)] public string? emailaddress { get; set; } + [StringLength(32)] + public string? role { get; set; } + } [Table("users")] @@ -21,7 +24,6 @@ public UserModel() { public string? first_name { get; set; } [StringLength(64)] public string? last_name { get; set; } - public string? role { get; set; } [ForeignKey("user_id")] // ForeignKey attribute in the PollModel public virtual ICollection Polls { get; set; } diff --git a/pollor.client/src/app/app.module.ts b/pollor.client/src/app/app.module.ts index eb1d2d3..a2014e4 100644 --- a/pollor.client/src/app/app.module.ts +++ b/pollor.client/src/app/app.module.ts @@ -1,4 +1,4 @@ -import { HttpClientModule } from '@angular/common/http'; +import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; @@ -7,6 +7,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { AuthInterceptor } from '../services/auth.interceptor'; import { HeaderComponent } from './header/header.component'; import { FooterComponent } from './footer/footer.component'; import { HomeComponent } from './home/home.component'; @@ -31,8 +33,10 @@ import { UserAdminProfileComponent } from './user-admin-profile/user-admin-profi UserAdminProfileComponent ], imports: [ - BrowserModule, HttpClientModule, - FormsModule, ReactiveFormsModule, + BrowserModule, + HttpClientModule, + FormsModule, + ReactiveFormsModule, AppRoutingModule, NgbModule, RouterOutlet, @@ -41,7 +45,13 @@ import { UserAdminProfileComponent } from './user-admin-profile/user-admin-profi FooterComponent, AlertMessage ], - providers: [], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true, + } + ], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/pollor.client/src/app/user-login/user-login.component.ts b/pollor.client/src/app/user-login/user-login.component.ts index 4172874..33cf71f 100644 --- a/pollor.client/src/app/user-login/user-login.component.ts +++ b/pollor.client/src/app/user-login/user-login.component.ts @@ -30,9 +30,8 @@ export class UserLoginComponent { const role = localStorage.getItem('role'); if (token && role) { - // todo: check if token if OK - // navigate to role profile page - this.navigateDashboard(role); + console.log("validate token and role"); + this.validateUser(); // validate and navigate to role profile page } } @@ -64,6 +63,28 @@ export class UserLoginComponent { } } + validateUser(): any { + this.loading = true; // Start the loading spinner + this.authService + .validateToken() + .pipe( + finalize(() => { + this.loading = false; //Stop the loading spinner + }) + ) + .subscribe({ + next: (res: any) => { + console.log('Response:', res); + this.navigateDashboard(res.user.role); + }, + error: (error: any) => { + this.loginError = 'An error occurred during token validation.'; + console.error('Token validation Error:', error); + AlertMessage.addErrorAlert(error.message); + }, + }); + } + navigateDashboard(role: string): void { const dashboardRoute = role === 'admin' ? '/account/admin-profile' : '/account/profile'; diff --git a/pollor.client/src/interfaces/auth.interface.ts b/pollor.client/src/interfaces/auth.interface.ts new file mode 100644 index 0000000..a357980 --- /dev/null +++ b/pollor.client/src/interfaces/auth.interface.ts @@ -0,0 +1,4 @@ +export interface IAuth { + token: string; + role: string; +} diff --git a/pollor.client/src/services/auth.interceptor.ts b/pollor.client/src/services/auth.interceptor.ts new file mode 100644 index 0000000..74c2c39 --- /dev/null +++ b/pollor.client/src/services/auth.interceptor.ts @@ -0,0 +1,25 @@ +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { AuthService } from './auth.service'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + intercept(req: HttpRequest, next: HttpHandler): Observable> { + if (AuthService.getToken()) { + const headers = { + 'Content-Type': 'application/json; charset=utf-8', + 'Accept': 'application/json', + 'Authorization': `Bearer ${AuthService.getToken()}`, + }; + + req = req.clone({ + setHeaders: headers, + }); + + console.log(headers); + } + + return next.handle(req); + } +} diff --git a/pollor.client/src/services/auth.service.ts b/pollor.client/src/services/auth.service.ts index cabda54..fc2760b 100644 --- a/pollor.client/src/services/auth.service.ts +++ b/pollor.client/src/services/auth.service.ts @@ -1,6 +1,8 @@ import { Observable } from 'rxjs'; import { Injectable } from '@angular/core'; import { ApiService } from './api.service'; +import { HttpHeaders } from '@angular/common/http'; +import { IAuth } from '../interfaces/auth.interface'; @Injectable({ providedIn: 'root' @@ -16,8 +18,20 @@ export class AuthService { return this.apiService.post('api/auth/register', body); } + validateToken(): Observable { + const auth: IAuth = { + token: AuthService.getToken(), + role: localStorage.getItem("role")! + } + return this.apiService.post('api/auth/validate', auth); + } + logout(body: any): Observable { return this.apiService.post('api/auth/logout', body); } + static getToken() { + return localStorage.getItem("token")!; + } + } From cb14ee11136c40e99eccdcc28c548ff3fe1a7ba8 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Sat, 3 Feb 2024 00:14:14 +0100 Subject: [PATCH 22/28] Fix backend and add getCurrentUser through token --- pollor.Server/Controllers/AuthController.cs | 31 ++++--------- pollor.Server/Controllers/UsersController.cs | 49 ++++++++++++++++++++ pollor.Server/Services/AuthService.cs | 24 ++++++++++ 3 files changed, 82 insertions(+), 22 deletions(-) create mode 100644 pollor.Server/Services/AuthService.cs diff --git a/pollor.Server/Controllers/AuthController.cs b/pollor.Server/Controllers/AuthController.cs index e92f55a..832279e 100644 --- a/pollor.Server/Controllers/AuthController.cs +++ b/pollor.Server/Controllers/AuthController.cs @@ -122,7 +122,7 @@ public IActionResult Validate([FromBody] ValidateTokenModel validateTokenModel) } SecurityToken validatedToken; - IPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(token, GetValidationParameters(), out validatedToken); + IPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(token, AuthService.GetValidationParameters(), out validatedToken); // The user is authenticated, and you can access user information var userClaims = HttpContext.User; @@ -137,13 +137,13 @@ public IActionResult Validate([FromBody] ValidateTokenModel validateTokenModel) using (var context = new PollorDbContext()) { UserModel? user = context.Users - .Where(u => u.id.Equals(userId)) - .Where(u => u.username.Equals(username)) - .Where(u => u.role.Equals(userRole)) + .Where(u => u.id.ToString().Equals(userId) && + u.username.Equals(username) && + u.role.Equals(userRole)) .FirstOrDefault(); if (user == null) { - return NotFound("User not found"); + return NotFound("User not found..."); } return Ok(new AuthenticatedResponse { token = validateTokenModel.token, user = user }); } @@ -163,30 +163,17 @@ private JwtSecurityToken GetJwtTokenOptions (int tokenValidForXDays, UserAuthMod new Claim(ClaimTypes.Role, user.role!) }; + String jwtTokenDomain = Environment.GetEnvironmentVariable("JWT_TOKEN_DOMAIN")!.Split(',').FirstOrDefault()!; + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET_JWT_KEY")!)); var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); var tokeOptions = new JwtSecurityToken( - issuer: "https://localhost:5001", - audience: "https://localhost:5001", + issuer: jwtTokenDomain, + audience: jwtTokenDomain, claims: jwtClaims, expires: DateTime.Now.AddDays(tokenValidForXDays), signingCredentials: signinCredentials ); return tokeOptions; } - - private static TokenValidationParameters GetValidationParameters() - { - var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET_JWT_KEY")!)); - - return new TokenValidationParameters() - { - ValidateLifetime = true, - ValidateAudience = true, - ValidateIssuer = true, - ValidIssuer = "https://localhost:5001", - ValidAudience = "https://localhost:5001", - IssuerSigningKey = secretKey // The same key as the one that generate the token - }; - } } \ No newline at end of file diff --git a/pollor.Server/Controllers/UsersController.cs b/pollor.Server/Controllers/UsersController.cs index 87e68d3..1136b95 100644 --- a/pollor.Server/Controllers/UsersController.cs +++ b/pollor.Server/Controllers/UsersController.cs @@ -1,8 +1,11 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using pollor.Server.Models; using pollor.Server.Services; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Principal; namespace pollor.Server.Controllers { @@ -39,6 +42,52 @@ public IActionResult GetAllUsers() } } + [HttpGet("user")] + [Authorize] + public IActionResult GetCurrentUser() + { + // Retrieve the JWT token from the Authorization header + var token = HttpContext.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + + if (string.IsNullOrEmpty(token)) + { + return BadRequest(new { message = "Token is missing" }); + } + + SecurityToken validatedToken; + IPrincipal principal = new JwtSecurityTokenHandler().ValidateToken(token, AuthService.GetValidationParameters(), out validatedToken); + + // The user is authenticated, and you can access user information + var userClaims = HttpContext.User; + // Perform additional validation or return success response + + var username = userClaims.Claims.Where(e => e.Type.EndsWith("identity/claims/name")).Select(e => e.Value).SingleOrDefault(); + var userId = userClaims.Claims.Where(e => e.Type.EndsWith("identity/claims/nameidentifier")).Select(e => e.Value).SingleOrDefault(); + var userRole = userClaims.Claims.Where(e => e.Type.EndsWith("identity/claims/role")).Select(e => e.Value).SingleOrDefault(); + + try + { + using (var context = new PollorDbContext()) + { + UserModel? user = context.Users + .Where(u => u.id.ToString().Equals(userId) && + u.username.Equals(username) && + u.role.Equals(userRole)) + .FirstOrDefault(); + if (user == null) + { + return NotFound("User not found..."); + } + return Ok(user); + } + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, new { message = ex.Message }); + } + } + [HttpGet("user/{id}")] public IActionResult GetUserById(int id) { diff --git a/pollor.Server/Services/AuthService.cs b/pollor.Server/Services/AuthService.cs new file mode 100644 index 0000000..97b7261 --- /dev/null +++ b/pollor.Server/Services/AuthService.cs @@ -0,0 +1,24 @@ +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace pollor.Server.Services +{ + public class AuthService + { + public static TokenValidationParameters GetValidationParameters() + { + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET_JWT_KEY")!)); + String jwtTokenDomain = Environment.GetEnvironmentVariable("JWT_TOKEN_DOMAIN")!.Split(',').FirstOrDefault()!; + + return new TokenValidationParameters() + { + ValidateLifetime = true, + ValidateAudience = true, + ValidateIssuer = true, + ValidIssuer = jwtTokenDomain, + ValidAudience = jwtTokenDomain, + IssuerSigningKey = secretKey // The same key as the one that generate the token + }; + } + } +} From 1f1a6419c7c98516ab1184c0cb50d05f5dfeaea5 Mon Sep 17 00:00:00 2001 From: devdanielsun Date: Sat, 3 Feb 2024 00:19:52 +0100 Subject: [PATCH 23/28] Change profiles and header --- pollor.client/angular.json | 4 +- .../src/app/alert-message/alert-message.ts | 13 +- pollor.client/src/app/app.module.ts | 2 + .../src/app/header/header.component.css | 23 ++++ .../src/app/header/header.component.html | 122 ++++++++++++------ .../src/app/header/header.component.ts | 9 ++ .../user-admin-profile.component.html | 7 +- .../app/user-login/user-login.component.ts | 3 +- .../user-profile/user-profile.component.html | 53 +++++++- .../user-profile/user-profile.component.ts | 60 +++++++++ .../src/interfaces/user.interface.ts | 19 +++ .../src/services/auth.interceptor.ts | 2 - pollor.client/src/services/auth.service.ts | 14 +- 13 files changed, 279 insertions(+), 52 deletions(-) create mode 100644 pollor.client/src/interfaces/user.interface.ts diff --git a/pollor.client/angular.json b/pollor.client/angular.json index 50aaee6..b307ff4 100644 --- a/pollor.client/angular.json +++ b/pollor.client/angular.json @@ -40,7 +40,7 @@ ], "scripts": [ - "./node_modules/bootstrap/dist/js/bootstrap.min.js" + "./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" ] }, "configurations": { @@ -102,7 +102,7 @@ "./node_modules/bootstrap-icons/font/bootstrap-icons.css" ], "scripts": [ - "./node_modules/bootstrap/dist/js/bootstrap.min.js" + "./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" ], "karmaConfig": "karma.conf.js" } diff --git a/pollor.client/src/app/alert-message/alert-message.ts b/pollor.client/src/app/alert-message/alert-message.ts index 34a7d80..c141241 100644 --- a/pollor.client/src/app/alert-message/alert-message.ts +++ b/pollor.client/src/app/alert-message/alert-message.ts @@ -47,10 +47,15 @@ export class AlertMessage { AlertMessage.alerts.unshift(newAlert); - setTimeout(() => { - AlertMessage.alerts.splice(AlertMessage.alerts.indexOf(newAlert), 1); - }, timeout); // automatic close the alert after x miliseconds based - } + setTimeout(() => { + AlertMessage.alerts.splice(AlertMessage.alerts.indexOf(newAlert), 1); + }, timeout); // automatic close the alert after x miliseconds based + } + + + static addSuccessAlert(alertMessage: string) { + AlertMessage.addAlert("success", "Success!", alertMessage); + } static addErrorAlert(alertMessage: string) { AlertMessage.addAlert("danger", "An error occured", alertMessage); diff --git a/pollor.client/src/app/app.module.ts b/pollor.client/src/app/app.module.ts index a2014e4..42312b5 100644 --- a/pollor.client/src/app/app.module.ts +++ b/pollor.client/src/app/app.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -34,6 +35,7 @@ import { UserAdminProfileComponent } from './user-admin-profile/user-admin-profi ], imports: [ BrowserModule, + RouterModule, HttpClientModule, FormsModule, ReactiveFormsModule, diff --git a/pollor.client/src/app/header/header.component.css b/pollor.client/src/app/header/header.component.css index e69de29..e817705 100644 --- a/pollor.client/src/app/header/header.component.css +++ b/pollor.client/src/app/header/header.component.css @@ -0,0 +1,23 @@ +nav { + background-color: seagreen; +} + +a.nav-link.active { + background-color: white; + color: black !important; +} + +a.nav-link:hover { + background-color: white; + color: black !important; +} + +@media screen and (max-width:767px) { + .nav-pills > li { + width: 100%; + } + + .btn-group { + margin-left: auto; + } +} diff --git a/pollor.client/src/app/header/header.component.html b/pollor.client/src/app/header/header.component.html index 9855025..694308c 100644 --- a/pollor.client/src/app/header/header.component.html +++ b/pollor.client/src/app/header/header.component.html @@ -1,56 +1,100 @@ -