comments galore
All checks were successful
Build and Deploy Frontend / build-and-deploy (push) Successful in 6s
Build and Deploy API / build-and-deploy (push) Successful in 9s

This commit is contained in:
2026-04-23 00:15:49 -05:00
parent ef1e9aa759
commit 317a7bce9d
8 changed files with 62 additions and 44 deletions

View File

@@ -1,4 +1,5 @@
// system usings
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -7,23 +8,27 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Text; using System.Text;
// homeburger usings
using agologumApi.Models; using agologumApi.Models;
using agologumApi.Services; using agologumApi.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// make sure the jwt key exists or else abort, security issue
var key = builder.Configuration["Jwt:Key"]; var key = builder.Configuration["Jwt:Key"];
if(key == null) return; if(key == null) return;
// connect to the sql database
builder.Services.AddDbContext<AppDbContext>(options => builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddControllers(); builder.Services.AddControllers();
// services // add our services
builder.Services.AddScoped<UserService>(); builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<ItemService>(); builder.Services.AddScoped<ItemService>();
builder.Services.AddScoped<JwtService>(); builder.Services.AddScoped<JwtService>();
// if this grows sufficiently large we can put elsewhere
// configuration for jwt authentication // configuration for jwt authentication
builder.Services.AddIdentity<User, IdentityRole>() builder.Services.AddIdentity<User, IdentityRole>()
@@ -46,6 +51,8 @@ builder.Services.AddAuthentication(options => {
}; };
}); });
// authorization configurations; here's where we register our permissions to policies
// TODO: this suspiciously looks able to be automated through a for loop, only if we can have a static dictionary maybe though?
builder.Services.AddAuthorization(options => { builder.Services.AddAuthorization(options => {
options.AddPolicy(Permission.SensitiveData_Read, policy => options.AddPolicy(Permission.SensitiveData_Read, policy =>
@@ -70,6 +77,7 @@ builder.Services.Configure<ForwardedHeadersOptions>(options =>
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
// cors; scary needs to be fixed
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy("dev", options.AddPolicy("dev",
@@ -78,17 +86,14 @@ builder.Services.AddCors(options =>
policy.AllowAnyOrigin() policy.AllowAnyOrigin()
.AllowAnyHeader() .AllowAnyHeader()
.AllowAnyMethod(); .AllowAnyMethod();
}); }); // TODO: scary please fix this
}); });
// more middleware; probably uncessary at this stage
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
// https://www.reddit.com/r/dotnet/comments/1h7vzbs/how_do_you_guys_handle_authorization_on_a_web_api/ // build app
// add authorization here
// controllers will have endpoints based on authorization
// frontend is a different story
var app = builder.Build(); var app = builder.Build();
app.UseForwardedHeaders(); app.UseForwardedHeaders();
@@ -121,17 +126,6 @@ using (var scope = app.Services.CreateScope()) {
} }
} }
// TODO: abstract this away
// auto seed Identity roles
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
string[] roles = { "guest", "user", "dev", "mod", "admin", "superuser", "role1", "role2" };
foreach(string role in roles) {
if(!await roleManager.RoleExistsAsync(role)) {
await roleManager.CreateAsync(new IdentityRole(role));
}
}
} }
app.Run(); app.Run();

View File

@@ -13,10 +13,11 @@ public class AuthController : ControllerBase {
// identity things // identity things
private readonly UserManager<User> userManager_; private readonly UserManager<User> userManager_;
private readonly SignInManager<User> signInManager_; private readonly SignInManager<User> signInManager_;
// services
private readonly JwtService jwt_; private readonly JwtService jwt_;
private readonly UserService userService_; private readonly UserService userService_;
// class constructor (where are my initializer lists man)
public AuthController(UserManager<User> userManager, SignInManager<User> signInManager, JwtService jwt, UserService userService) { public AuthController(UserManager<User> userManager, SignInManager<User> signInManager, JwtService jwt, UserService userService) {
userManager_ = userManager; userManager_ = userManager;
@@ -25,43 +26,49 @@ public class AuthController : ControllerBase {
userService_ = userService; userService_ = userService;
} }
// register endpoint
[HttpPost("register")] [HttpPost("register")]
public async Task<ActionResult> Register(RegisterDto dto) { public async Task<ActionResult> Register(RegisterDto dto) {
// create a new user out of the dto from the request
User user = new User { User user = new User {
UserName = dto.UserName, UserName = dto.UserName,
Email = dto.Email, Email = dto.Email,
CreatedAt = DateTime.UtcNow // yeah why not utc CreatedAt = DateTime.UtcNow // yeah why not utc
}; };
// assigning roles to user // assigning roles to user. create a user starting with x to give it permissions to read sensitive data
if(dto.UserName.StartsWith("x")) { if(dto.UserName.StartsWith("x")) {
user.Permissions = new List<string> { Permission.SensitiveData_Read }; user.Permissions = new List<string> { Permission.SensitiveData_Read };
} else if(dto.UserName == "bard") {
user.Permissions = new List<string> { Permission.SensitiveData_Read, Permission.SensitiveData_Modify };
} }
// use Identity's user manager to add to db; error check if failed
var result = await userManager_.CreateAsync(user, dto.Password); var result = await userManager_.CreateAsync(user, dto.Password);
if(!result.Succeeded) return BadRequest(result.Errors); if(!result.Succeeded) return BadRequest(result.Errors);
// respond to post as necessary
return CreatedAtAction( return CreatedAtAction(
nameof(Register), nameof(Register),
new { id = user.Id } new { id = user.Id }
); );
} }
// login endpoint
[HttpPost("login")] [HttpPost("login")]
public async Task<ActionResult> Login(LoginDto dto) public async Task<ActionResult> Login(LoginDto dto)
{ {
// get the user from the database given the username
var user = await userManager_.FindByNameAsync(dto.UserName); var user = await userManager_.FindByNameAsync(dto.UserName);
// user not found with that name
if (user == null) return Unauthorized(); // unauthorized instead of not found to not give away info
if (user == null) return Unauthorized(); // use identity's password validation
var result = await signInManager_.CheckPasswordSignInAsync(user, dto.Password, false); var result = await signInManager_.CheckPasswordSignInAsync(user, dto.Password, false);
// if failed then youre not real !
if(!result.Succeeded) return Unauthorized(); if(!result.Succeeded) return Unauthorized();
// login sucess, give you an authentication token
var accessToken = await jwt_.GenerateJwt(user); var accessToken = await jwt_.GenerateJwt(user);
var refreshToken = jwt_.GenerateRefreshToken(); var refreshToken = jwt_.GenerateRefreshToken(); // the refresh token is good enough to refresh your access token
RefreshToken newTokenObject = new RefreshToken { RefreshToken newTokenObject = new RefreshToken {
Token = refreshToken, Token = refreshToken,
UserId = user.Id, UserId = user.Id,
@@ -70,43 +77,44 @@ public class AuthController : ControllerBase {
IsRevoked = false IsRevoked = false
}; };
await jwt_.AddRefreshToken(newTokenObject); await jwt_.AddRefreshToken(newTokenObject);
// the jwt says we trust who you are and can substitute it for login
// contains permissions claims too
if(user.UserName == "bard") { // tempory because im silly // return both access and refresh token
await userManager_.AddToRoleAsync(user, "superuser");
} // eventually ill have an endpoint for adding/removing roles
if(dto.UserName == "bard") {
user.Permissions = new List<string> { Permission.SensitiveData_Read, Permission.SensitiveData_Modify };
await userService_.Update(user.Id, user);
}
return Ok(new { accessToken, refreshToken }); return Ok(new { accessToken, refreshToken });
} }
// logout endpoint
[Authorize] // authorize is handled by middleware [Authorize] // authorize is handled by middleware
[HttpPost("logout")] [HttpPost("logout")]
public async Task<ActionResult> Logout(string refreshTokenString) { public async Task<ActionResult> Logout(string refreshTokenString) {
// revoke refresh token // revoke refresh token
bool success = await jwt_.RevokeRefreshToken(refreshTokenString); bool success = await jwt_.RevokeRefreshToken(refreshTokenString);
if(!success) return NotFound(); if(!success) return NotFound();
// frontend refreshes page and detects logout
return Ok(); return Ok();
} }
// refresh token endpoint
[HttpPost("refresh")] // allow-anonymous by default [HttpPost("refresh")] // allow-anonymous by default
public async Task<ActionResult> Refresh(TokenDto request) { public async Task<ActionResult> Refresh(TokenDto request) {
// reached when the frontend gets an unauthorized response and autoattempts to refresh if available
// get token from request and check if its valid
RefreshToken? storedToken = await jwt_.GetRefreshToken(request.RefreshToken); RefreshToken? storedToken = await jwt_.GetRefreshToken(request.RefreshToken);
if (storedToken == null) return Unauthorized(); if (storedToken == null) return Unauthorized();
bool valid = (storedToken.IsRevoked) || bool valid = (storedToken.IsRevoked) ||
(storedToken.ExpiresAt < DateTime.UtcNow); (storedToken.ExpiresAt < DateTime.UtcNow);
if(!valid) return Unauthorized(); // TODO: delete the invalid token if(!valid) return Unauthorized(); // TODO: delete the invalid token
// get user from the token and give them new tokens
User? user = await jwt_.GetUser(storedToken.UserId); User? user = await jwt_.GetUser(storedToken.UserId);
if(user == null) return NotFound(); if(user == null) return NotFound();
string? newAccessToken = await jwt_.GenerateJwt(user); string? newAccessToken = await jwt_.GenerateJwt(user);
if(newAccessToken == null) return NotFound(); if(newAccessToken == null) return NotFound();
string newRefreshToken = jwt_.GenerateRefreshToken(); string newRefreshToken = jwt_.GenerateRefreshToken();
// construct new token
storedToken.IsRevoked = true; storedToken.IsRevoked = true;
RefreshToken newTokenObject = new RefreshToken { RefreshToken newTokenObject = new RefreshToken {
Token = newRefreshToken, Token = newRefreshToken,
@@ -117,10 +125,9 @@ public class AuthController : ControllerBase {
}; };
await jwt_.AddRefreshToken(newTokenObject); await jwt_.AddRefreshToken(newTokenObject);
// return new tokens
return Ok(new { accessToken = newAccessToken, refreshToken = newRefreshToken }); return Ok(new { accessToken = newAccessToken, refreshToken = newRefreshToken });
} }
// TODO // TODO

View File

@@ -68,6 +68,7 @@ public class UsersController : ControllerBase {
[HttpDelete("{id}/{permission}")] [HttpDelete("{id}/{permission}")]
public async Task<ActionResult> removePermission(string id, string permission) { public async Task<ActionResult> removePermission(string id, string permission) {
// get the user this request comes from. since it passed identity auth we can trust it
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if(permission == Permission.SensitiveData_Modify && userId == id) return BadRequest(); // dont allow permission removal of whats allowing us to re-add premissions if(permission == Permission.SensitiveData_Modify && userId == id) return BadRequest(); // dont allow permission removal of whats allowing us to re-add premissions

View File

@@ -10,7 +10,7 @@ public class User : IdentityUser {
// TODO: make this a list of UserPermissions // TODO: make this a list of UserPermissions
// where a userpermission has an Id, Permission (string), and userId string // where a userpermission has an Id, Permission (string), and userId string
// then we can do something like: get all users with this permission // then we can do something like: get all users with this permission
public List<string>? Permissions { get; set; } = new(); public List<string>? Permissions { get; set; } = new(); // because this isnt very relational database happy
// properties inherited from IdentityUser: // properties inherited from IdentityUser:
/* /*
@@ -54,12 +54,14 @@ public class LoginDto {
public class UserDto { public class UserDto {
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // gets compressed to a string' public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // datetimes get compressed to a string
public List<string>? Permissions { get; set; } = []; public List<string>? Permissions { get; set; } = [];
public string? Email { get; set; } = ""; public string? Email { get; set; } = "";
public string Id { get; set; } = ""; public string Id { get; set; } = "";
public string? UserName { get; set; } = ""; public string? UserName { get; set; } = "";
// constructor out of a full User object
// REMEMBER: when adding fields to UserDto they must also be set in this constructor or else stuff breaks
public UserDto(User user) { public UserDto(User user) {
CreatedAt = user.CreatedAt; CreatedAt = user.CreatedAt;
Email = user.Email; Email = user.Email;

View File

@@ -5,6 +5,7 @@ using agologumApi.Models;
namespace agologumApi.Services; namespace agologumApi.Services;
// basic CRUD operations for items in the database
public class ItemService { public class ItemService {
private readonly AppDbContext db_; private readonly AppDbContext db_;

View File

@@ -21,14 +21,16 @@ public class JwtService {
userManager_ = userManager; userManager_ = userManager;
} }
// create a jwt string given a user (user contains permissions which go into claims)
public async Task<string?> GenerateJwt(User user) { public async Task<string?> GenerateJwt(User user) {
// security stuff
string? jwtKey = config_["Jwt:Key"]; string? jwtKey = config_["Jwt:Key"];
if(jwtKey == null) return null; if(jwtKey == null) return null;
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
// make sure the user is real
if(user.UserName == null) return null; if(user.UserName == null) return null;
// not too sure // not too sure
@@ -37,13 +39,15 @@ public class JwtService {
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
}; };
// add each permission that the user has into the claims
List<string>? permissions = user.Permissions; List<string>? permissions = user.Permissions;
if(permissions != null) { if(permissions != null) {
foreach(var perm in permissions) { foreach(string perm in permissions) {
claims.Add(new Claim("permission", perm)); claims.Add(new Claim("permission", perm));
} }
} }
// construct that token
var token = new JwtSecurityToken( var token = new JwtSecurityToken(
issuer: "agologum", issuer: "agologum",
audience: "agologum", audience: "agologum",
@@ -56,6 +60,7 @@ public class JwtService {
} }
// generating a refresh token is just like a long random password
public string GenerateRefreshToken() { public string GenerateRefreshToken() {
byte[] randomBytes = new byte[64]; byte[] randomBytes = new byte[64];
@@ -64,10 +69,12 @@ public class JwtService {
} }
// we store refresh tokens on our side to check against when a user requests a refresh
public async Task<RefreshToken?> GetRefreshToken(string refreshTokenString) { public async Task<RefreshToken?> GetRefreshToken(string refreshTokenString) {
return await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString); return await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString);
} }
// add a refresh token to the token db store
public async Task<RefreshToken> AddRefreshToken(RefreshToken refreshToken) { public async Task<RefreshToken> AddRefreshToken(RefreshToken refreshToken) {
db_.RefreshTokens.Add(refreshToken); db_.RefreshTokens.Add(refreshToken);
await db_.SaveChangesAsync(); await db_.SaveChangesAsync();
@@ -77,8 +84,9 @@ public class JwtService {
// helper to get the User from the id that exists in a refresh token object // helper to get the User from the id that exists in a refresh token object
public async Task<User?> GetUser(string id) { public async Task<User?> GetUser(string id) {
return await db_.Users.FindAsync(id); return await db_.Users.FindAsync(id);
} } // since other places aren't good for having references to db contexts
// remove refresh token from our store; called when user logs out
public async Task<bool> RevokeRefreshToken(string refreshTokenString) { public async Task<bool> RevokeRefreshToken(string refreshTokenString) {
var refreshToken = await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString); var refreshToken = await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString);
if(refreshToken == null) return false; if(refreshToken == null) return false;

View File

@@ -13,18 +13,22 @@ public class UserService {
db_ = db; db_ = db;
} }
// get all users
public async Task<List<User>> GetAll() { public async Task<List<User>> GetAll() {
return await db_.Users.ToListAsync(); return await db_.Users.ToListAsync();
} }
// get one user with id of id
public async Task<User?> GetById(string id) { public async Task<User?> GetById(string id) {
return await db_.Users.FindAsync(id); return await db_.Users.FindAsync(id);
} }
// get one user with username of name
public async Task<User?> GetByName(string name) { public async Task<User?> GetByName(string name) {
return await db_.Users.FirstOrDefaultAsync(u => u.UserName == name); return await db_.Users.FirstOrDefaultAsync(u => u.UserName == name);
} }
// delete one user with id of id
public async Task<bool> Delete(string id) { public async Task<bool> Delete(string id) {
User? User = await db_.Users.FindAsync(id); User? User = await db_.Users.FindAsync(id);
if(User != null) { if(User != null) {
@@ -36,6 +40,7 @@ public class UserService {
} }
} }
// update user of id with user
public async Task<User?> Update(string id, User user) { public async Task<User?> Update(string id, User user) {
User? oldUser = await db_.Users.FindAsync(id); User? oldUser = await db_.Users.FindAsync(id);

View File

@@ -88,6 +88,6 @@ api.interceptors.response.use(response => response, async error => { // mainly f
}) })
// TODO: if you get a 403 while navigating then redirect to the last authenticated page // TODO: if you get a 403 while navigating then redirect to the last authenticated page
// if you gert a 403 on a form submissio nthen do like an unauthorized popup // if you gert a 403 on a form submissio nthen do like an unauthorized popup (message: stale session <login link>) (or redirect to login like i said elsewhere)
export default api; export default api;