Merge pull request 'Major update to auth for MAuth' (#7) from working into main
Docker Build and Release Upload / build (push) Successful in 1m27s

Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
2025-07-30 05:16:24 +00:00
23 changed files with 107 additions and 905 deletions
+2 -2
View File
@@ -9,10 +9,10 @@
</a> </a>
<div *ngIf="auth.isLoggedIn" class="top-bar-buttons flex-right"> <div *ngIf="auth.isLoggedIn" class="top-bar-buttons flex-right">
<a class="nav-button nav-button-login" routerLink="/account/settings"><span>{{ auth.loggedInUser.userName.toUpperCase() }}</span></a> <a class="nav-button nav-button-login" routerLink="/account/settings"><span>{{ auth.loggedInUser.userName.toUpperCase() }}</span></a>
<a class="nav-button nav-button-login" routerLink="/account/logout"><span>LOGOUT</span></a> <a class="nav-button nav-button-login" href="/api/account/logout"><span>LOGOUT</span></a>
</div> </div>
<div *ngIf="!auth.isLoggedIn" class="top-bar-buttons flex-right"> <div *ngIf="!auth.isLoggedIn" class="top-bar-buttons flex-right">
<a class="nav-button nav-button-login" routerLink="/account/login"><span>LOGIN</span></a> <a class="nav-button nav-button-login" href="https://auth.mistox.com/account/login?returnURL=https://boredcareers.com/"><span>LOGIN</span></a>
<a class="nav-button nav-button-login" routerLink="/account/register"><span>REGISTER</span></a> <a class="nav-button nav-button-login" routerLink="/account/register"><span>REGISTER</span></a>
</div> </div>
</div> </div>
-16
View File
@@ -1,12 +1,5 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { ForgotPasswordComponent } from './pages/account/forgotpassword/forgotpassword.component';
import { LoginComponent } from './pages/account/login/login.component';
import { RegisterComponent } from './pages/account/register/register.component';
import { AboutComponent } from './pages/legal/about/about.component'; import { AboutComponent } from './pages/legal/about/about.component';
import { SettingsComponent } from './pages/account/settings/settings.component';
import { LogoutComponent } from './pages/account/logout/logout.component';
import { ResetPasswordComponent } from './pages/account/resetpassword/resetpassword.component';
import { VerifyEmailComponent } from './pages/account/verifyemail/verifyemail.component';
import { HomeComponent } from './pages/main/home/home.component'; import { HomeComponent } from './pages/main/home/home.component';
import { ContactComponent } from './pages/legal/contact/contact.component'; import { ContactComponent } from './pages/legal/contact/contact.component';
import { PrivacyComponent } from './pages/legal/privacy/privacy.component'; import { PrivacyComponent } from './pages/legal/privacy/privacy.component';
@@ -30,15 +23,6 @@ export const routes: Routes = [
// Company // Company
{ path: "company/connect", component: CompanyConnectComponent }, { path: "company/connect", component: CompanyConnectComponent },
// Account stuff
{ path: "account/forgotpassword", component: ForgotPasswordComponent },
{ path: "account/resetpassword", component: ResetPasswordComponent },
{ path: "account/verifyemail", component: VerifyEmailComponent },
{ path: "account/login", component: LoginComponent },
{ path: "account/logout", component: LogoutComponent },
{ path: "account/register", component: RegisterComponent },
{ path: "account/settings", component: SettingsComponent },
// Legal // Legal
{ path: "about", component: AboutComponent }, { path: "about", component: AboutComponent },
{ path: "contact", component: ContactComponent }, { path: "contact", component: ContactComponent },
+28 -3
View File
@@ -1,7 +1,8 @@
import { Component, ElementRef, ViewChild } from '@angular/core'; import { Component, ElementRef, ViewChild } from '@angular/core';
import { Router, RouterModule, RouterOutlet } from '@angular/router'; import { Router, RouterModule, RouterOutlet, ActivatedRoute } from '@angular/router';
import { Authentication } from './services/Authentication'; import { Authentication } from './services/Authentication';
import { CommonModule } from '@angular/common'; import { CommonModule, Location } from '@angular/common';
import { HttpClient } from '@angular/common/http';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -15,7 +16,31 @@ export class App {
@ViewChild('jobsLink') jobLink!: ElementRef<HTMLAnchorElement>; @ViewChild('jobsLink') jobLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('resumesLink') resumeLink!: ElementRef<HTMLAnchorElement>; @ViewChild('resumesLink') resumeLink!: ElementRef<HTMLAnchorElement>;
constructor(public auth: Authentication, private router: Router){} constructor( private http: HttpClient, public auth: Authentication, private router: Router, private route: ActivatedRoute, private location: Location){
this.route.queryParams.subscribe(params => {
const loginToken = params['LoginToken'];
console.log("LoginToken : " + loginToken);
if (loginToken){
this.http.post( "api/account/loginticket", JSON.stringify(loginToken), { headers: {'Content-Type': 'application/json'} } ).subscribe({
next: data => {
auth.getLoginState();
const pathWithoutQuery = this.location.path().split('?')[0];
this.location.replaceState(pathWithoutQuery);
},
error: err => {
auth.getLoginState();
const pathWithoutQuery = this.location.path().split('?')[0];
this.location.replaceState(pathWithoutQuery);
}
})
}else{
auth.getLoginState();
}
});
}
ngAfterViewInit(){ ngAfterViewInit(){
let ViewLinks = [ this.homeLink, this.resumeLink, this.jobLink ]; let ViewLinks = [ this.homeLink, this.resumeLink, this.jobLink ];
+1 -1
View File
@@ -1,5 +1,5 @@
export class Account { export class Account {
public id: number = 0; public id: number = -1;
public userName: string = ""; public userName: string = "";
public email: string = ""; public email: string = "";
public emailVerified: boolean = false; public emailVerified: boolean = false;
@@ -1,23 +0,0 @@
<div class="center">
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
<h3>Forgot Password</h3>
<div class="frame-item">
<input type="text" [(ngModel)]="email" name="email" placeholder=" " />
<label>Email</label>
</div>
<div>
<div class="flex-row">
<div class="frame-button">
<input class="submit" type="submit" value="Send Code" />
</div>
</div>
</div>
<ul *ngIf="errorMsgs.length > 0" >
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
</ul>
</form>
</div>
@@ -1,55 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
@Component({
selector: 'account-forgot',
templateUrl: './forgotpassword.component.html',
imports: [ FormsModule, CommonModule ]
})
export class ForgotPasswordComponent {
email: string = "";
errorMsgs: string[] = [];
returnURL: string = '/';
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title) {
this.title.setTitle("Forgot Password | Mistox");
this.route.queryParams.subscribe(params => {
this.returnURL = params['returnURL'] || '/';
});
}
sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
onSubmit() {
// Clear errors
this.errorMsgs = [];
// Send to server and wait for response
this.errorMsgs.push("Waiting for response from server");
const body = new HttpParams()
.set("Email", this.email)
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
});
this.http.post( "api/account/sendresetpassword", body, { headers, responseType: "text" } ).subscribe({
next: async (data) => {
if (data.trim() == "Success"){
this.errorMsgs = ["Reset-password sent"];
await this.sleep(3000);
this.router.navigate([this.returnURL]);
}else{
this.errorMsgs = [data];
}
},
error: err => {
console.log("HTTP Error Signing In: ", err);
}
});
}
}
@@ -1,35 +0,0 @@
<div class="center">
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
<h3>Login</h3>
<div class="frame-item">
<input type="text" [(ngModel)]="UserName" name="userName" placeholder=" " autocomplete="username" />
<label>UserName</label>
</div>
<div class="frame-item">
<input type="password" [(ngModel)]="Password" name="password" placeholder=" " autocomplete="current-password" />
<label>Password</label>
</div>
<div class="flex-row">
<div class="frame-button">
<input class="submit" type="submit" value="LOGIN" />
</div>
<div class="frame-forgot">
<div class="sub-frame">
Stay Logged In
<input type="checkbox" [(ngModel)]="StayLoggedIn" name="stayLoggedIn" />
</div>
<div class="sub-frame">
<a href="/account/forgotpassword">Forgot Password</a>
</div>
</div>
</div>
<ul *ngIf="errorMsgs.length > 0" >
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
</ul>
</form>
</div>
@@ -1,55 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { Authentication, SessionType } from '../../../services/Authentication';
@Component({
selector: 'account-login',
templateUrl: './login.component.html',
imports: [ FormsModule, CommonModule ],
standalone: true
})
export class LoginComponent {
UserName: string = "";
Password: string = "";
StayLoggedIn: boolean = false;
errorMsgs: string[] = [];
returnURL: string = '/';
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Login | Mistox");
this.route.queryParams.subscribe(params => {
this.returnURL = params['returnURL'] || '/';
});
}
onSubmit() {
this.errorMsgs = [];
if (!this.UserName) {
this.errorMsgs.push("The 'username' field is required");
}
if (!this.Password) {
this.errorMsgs.push("The 'password' field is required");
}
if (this.Password.length < 6) {
this.errorMsgs.push("Password must be at least 6 Characters long");
}
if (this.errorMsgs.length > 0) {
return;
}
this.errorMsgs.push("Waiting for response from server");
this.auth.Login(this.UserName, this.Password, this.StayLoggedIn).subscribe({
next: data => {
this.router.navigate([this.returnURL]);
},
error: err => {
this.errorMsgs = [ err.error ];
}
})
}
}
@@ -1,30 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { Authentication } from '../../../services/Authentication';
@Component({
selector: 'account-logout',
templateUrl: './logout.component.html',
imports: [ FormsModule, CommonModule ],
standalone: true
})
export class LogoutComponent {
errorMsgs: string[] = [];
returnURL: string = '/';
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Logout | Mistox");
}
ngAfterViewInit(){
this.auth.Logout().subscribe({
next: data => {
window.location.href = "";
}
});
}
}
@@ -1,35 +0,0 @@
<div class="center">
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
<h3>Register</h3>
<div class="frame-item">
<input type="text" [(ngModel)]="userName" name="userName" placeholder=" " autocomplete="username" />
<label>UserName</label>
</div>
<div class="frame-item">
<input type="email" [(ngModel)]="email" name="email" placeholder=" " autocomplete="current-password" />
<label>Email</label>
</div>
<div class="frame-item">
<input type="password" [(ngModel)]="passwordHash" name="password" placeholder=" " autocomplete="current-password" />
<label>Password</label>
</div>
<div class="frame-item">
<input type="password" [(ngModel)]="passwordHash2" name="repeat password" placeholder=" " autocomplete="current-password" />
<label>Repeat Password</label>
</div>
<div class="flex-row">
<div class="frame-button">
<input class="submit" type="submit" value="REGISTER" />
</div>
</div>
<ul *ngIf="errorMsgs.length > 0" >
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
</ul>
</form>
</div>
@@ -1,80 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { Account } from '../../../models/Account';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
@Component({
selector: 'account-register',
templateUrl: './register.component.html',
imports: [ FormsModule, CommonModule ]
})
export class RegisterComponent {
userName: string = ""
email: string = "";
passwordHash: string = "";
passwordHash2: string = "";
error: string = "";
errorMsgs: string[] = [];
returnURL: string = '/';
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
this.title.setTitle("Register | Mistox");
this.route.queryParams.subscribe(params => {
this.returnURL = params['returnURL'] || '/';
});
}
sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
onSubmit() {
// Clear errors
this.errorMsgs = [];
// Validate data
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!regex.test(this.email)){
this.errorMsgs.push("A valid email is required");
}
if (!this.userName) {
this.errorMsgs.push("The 'username' field is required");
}
if (!this.passwordHash) {
this.errorMsgs.push("The 'password' field is required");
}
if (this.passwordHash.length < 6) {
this.errorMsgs.push("Password must be at least 6 Characters long");
}
if (this.passwordHash !== this.passwordHash2){
this.errorMsgs.push("Passwords don't match");
}
if (this.errorMsgs.length > 0) {
return;
}
// Send to server and wait for response
this.errorMsgs.push("Waiting for response from server");
const body = new HttpParams()
.set("Email", this.email)
.set("UserName", this.userName)
.set("PasswordHash", this.passwordHash);
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
});
this.http.post<Account>( "api/account/register", body, { headers } ).subscribe({
next: async (data) => {
this.errorMsgs = ["Account Created"];
await this.sleep(3000);
this.router.navigate([this.returnURL]);
},
error: err => {
this.errorMsgs = [ err.error ]
}
});
}
}
@@ -1,29 +0,0 @@
<div class="center">
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
<h3>Reset Password</h3>
<h2>User: {{ UserName }}</h2>
<div class="frame-item">
<input type="password" [(ngModel)]="Password" name="Password" placeholder=" " />
<label>New Password</label>
</div>
<div class="frame-item">
<input type="password" [(ngModel)]="PassworR" name="PassworR" placeholder=" " />
<label>Repeat New Password</label>
</div>
<div>
<div class="flex-row">
<div class="frame-button">
<input class="submit" type="submit" value="Send Code" />
</div>
</div>
</div>
<ul *ngIf="errorMsgs.length > 0" >
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
</ul>
</form>
</div>
@@ -1,69 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
@Component({
selector: 'account-reset',
templateUrl: './resetpassword.component.html',
imports: [ FormsModule, CommonModule ]
})
export class ResetPasswordComponent {
UserName: string = "";
ResetPwd: string = "";
Password: string = "";
PassworR: string = "";
errorMsgs: string[] = [];
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
this.title.setTitle("Reset Password | Mistox");
this.route.queryParams.subscribe(params => {
this.UserName = params['UserName'] || '';
this.ResetPwd = params['ResetPwd'] || '';
});
}
sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
onSubmit() {
if (this.Password != this.PassworR){
this.errorMsgs.push("Passwords must match");
}
if (this.Password.length < 6){
this.errorMsgs.push("Password must be at least 6 Characters long");
}
if (this.errorMsgs.length == 0){
// Send to server and wait for response
this.errorMsgs.push("Waiting for response from server");
const body = new HttpParams()
.set("UserName", this.UserName)
.set("NewPassword", this.Password)
.set("ResetToken", this.ResetPwd);
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
});
this.http.post<boolean>( "api/account/resetpassword", body, { headers } ).subscribe({
next: async (data) => {
if (data == true){
this.errorMsgs = ["Password reset successfully"];
await this.sleep(3000);
this.router.navigate(["/account/login"]);
}else{
this.errorMsgs = ["An error has ocurred"];
await this.sleep(3000);
this.router.navigate(["/account/sendresetpassword"]);
}
},
error: err => {
console.log("HTTP Error Signing In: ", err);
}
});
}
}
}
@@ -1,29 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { Account } from '../../../models/Account';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
@Component({
selector: 'account-settings',
templateUrl: './settings.component.html',
imports: [ FormsModule, CommonModule ]
})
export class SettingsComponent {
user!: Account;
errorMsgs: string[] = [];
returnURL: string = '/';
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
this.title.setTitle("Settings | Mistox");
this.route.queryParams.subscribe(params => {
this.returnURL = params['returnURL'] || '/';
});
}
onSubmit() {
}
}
@@ -1,8 +0,0 @@
<div class="center">
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
<h3>Verifying Email</h3>
<h3 style="color: red;">{{ Result }}</h3>
</form>
</div>
@@ -1,54 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
@Component({
selector: 'account-verifyemail',
templateUrl: './verifyemail.component.html',
imports: [ FormsModule, CommonModule ]
})
export class VerifyEmailComponent {
UserName: string = "";
Guid: string = "";
Result: string = "";
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
this.title.setTitle("Verify Email | Mistox");
this.route.queryParams.subscribe(params => {
this.UserName = params['UserName'] || '';
this.Guid = params['Guid'] || '';
});
}
sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async onSubmit() {
// Send to server and wait for response
const body = new HttpParams()
.set("UserName", this.UserName)
.set("EmailToken", this.Guid);
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
});
this.http.post<boolean>( "api/account/verifyemail", body, { headers } ).subscribe({
next: async (data) => {
if (data == true){
this.Result = "Verified Email Successfully";
}else{
this.Result = "Email was not able to be verified please resend email";
}
await this.sleep(3000);
this.router.navigate(["/"]);
},
error: err => {
console.log("HTTP Error Signing In: ", err);
}
});
}
}
+4 -47
View File
@@ -6,30 +6,20 @@ import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class Authentication{ export class Authentication{
private _user = new BehaviorSubject<Account>(this.getUserFromStorage()); private _user = new BehaviorSubject<Account>( new Account );
user$ = this._user.asObservable(); user$ = this._user.asObservable();
constructor( private http: HttpClient){ } constructor( private http: HttpClient){ }
Login(UserName: string, Password: string, StayLoggedIn: boolean): Observable<Account> { getLoginState(): Observable<Account> {
let sub = this.http.post<Account>( "api/account/loginState", {}, {} );
const body = new HttpParams()
.set("UserName", UserName)
.set("PasswordHash", Password)
.set("StayLoggedIn", StayLoggedIn );
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
});
let sub = this.http.post<Account>( "api/account/login", body, { headers } );
sub.subscribe({ sub.subscribe({
next: data => { next: data => {
data.passwordHash = ""; data.passwordHash = "";
this._user.next(data); this._user.next(data);
this.setUserToStorage(data, StayLoggedIn == true ? SessionType.Forever : SessionType.Session);
}, },
error: err => { error: err => {
console.log("HTTP Error Signing In: ", err.error); console.log("No login state found: ", err.error);
} }
}); });
return sub; return sub;
@@ -37,7 +27,6 @@ export class Authentication{
Logout(){ Logout(){
this._user.next( new Account ); this._user.next( new Account );
this.delUserFromStorage();
return this.http.post<Account>( "api/account/logout", {}, { responseType: 'json' } ); return this.http.post<Account>( "api/account/logout", {}, { responseType: 'json' } );
} }
@@ -48,36 +37,4 @@ export class Authentication{
get loggedInUser(): Account { get loggedInUser(): Account {
return this._user.value; return this._user.value;
} }
private getUserFromStorage(): Account {
const foreverUser = localStorage.getItem('user');
const sessionUser = sessionStorage.getItem('user');
let user = null;
if (foreverUser != null){
user = JSON.parse(foreverUser)
} else if (sessionUser != null){
user = JSON.parse(sessionUser)
} else {
user = new Account();
user.id = -1;
}
return user;
}
private setUserToStorage(user: Account, session: SessionType): void {
if (session == SessionType.Forever){
localStorage.setItem('user', JSON.stringify(user));
}else if(session == SessionType.Session){
sessionStorage.setItem('user', JSON.stringify(user));
}
}
private delUserFromStorage(): void {
localStorage.removeItem('user');
sessionStorage.removeItem('user');
}
}
export enum SessionType {
Forever,
Session
} }
@@ -1,279 +1,51 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services;
using BoredCareers.Services.DatabaseService; using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities; using BoredCareers.Entities;
using System.Web.Http; using System.Web.Http;
using System.Text.Json;
using System.Text;
namespace BoredCareers.Controllers { namespace BoredCareers.Controllers {
[ApiController] [ApiController]
[Route("api/account/")] [Route("api/account/")]
public class AuthenticationController : MistoxControllerBase { public class AuthenticationController : MistoxControllerBase {
EmailService _emailContext; public AuthenticationController(DatabaseService db) : base(db) { }
public AuthenticationController(DatabaseService db, EmailService emailContext) : base(db) { [HttpPost("loginState")]
_emailContext = emailContext; public ActionResult<Account> LoginState() {
}
[Route("login")]
[HttpPost]
public async Task<ActionResult<Account>> Login([FromForm] string UserName, [FromForm] string PasswordHash, [FromForm] bool StayLoggedIn) {
try {
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null) {
if (test.EmailVerified == true) {
if (test.FailedPasswordLock) {
if (test.CurrentPasswordAttempts >= test.PasswordAttempts) {
return NotFound("Too many failed password attempts. Please reset your password");
}
}
if (BCrypt.Net.BCrypt.Verify(PasswordHash, test.PasswordHash)) {
test.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(test);
string jwt = BoredCareersJWT.GenereateJWTToken(test.ID, StayLoggedIn);
BoredCareersJWT.SignIn(Response, StayLoggedIn, jwt);
return Ok(test);
} else {
test.CurrentPasswordAttempts += 1;
await _databaseService.SetAccount(test);
return NotFound("Wrong Password");
}
} else {
await SendVerify(test.UserName);
return NotFound("A new verify email has been sent. \n Note only 1 email send every 5 mintes");
}
}
return NotFound("Account Not Found");
} catch (Exception ex) {
Console.WriteLine("Login Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
}
[Route("register")]
[HttpPost]
public async Task<ActionResult<Account>> Register([FromForm] string Email, [FromForm] string UserName, [FromForm] string PasswordHash) {
try {
if (await _databaseService.GetAccount(UserName.ToLower()) == null) {
if (await _databaseService.GetAccount(Email.ToLower()) == null) {
Account created = new Account() {
UserName = UserName.ToLower(),
Email = Email.ToLower(),
EmailVerified = false,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(PasswordHash),
};
await _databaseService.SetAccount(created);
Account? loadedAccount = await _databaseService.GetAccount(Email.ToLower());
if (loadedAccount != null) {
await SendVerify(loadedAccount.UserName);
return Ok(loadedAccount);
}
return NotFound("Unable to create the account");
} else {
return NotFound("Email is already in use");
}
} else {
return NotFound("UserName is taken");
}
} catch (Exception ex) {
Console.WriteLine("Register Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
}
[Route("changepassword")]
[HttpPost]
public async Task<ActionResult> ChangePassword([FromForm] string OldPassword, [FromForm] string NewPassword) {
try {
if (isLoggedIn()) { if (isLoggedIn()) {
Account user = await getLoggedInUser(); return Ok(getLoggedInUser());
if (BCrypt.Net.BCrypt.Verify(OldPassword, user.PasswordHash)) { }
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(NewPassword); return NotFound("Not logged in");
user.CurrentPasswordAttempts = 0; }
await _databaseService.SetAccount(user);
[HttpPost("loginticket")]
public async Task<ActionResult> LoginTicket([FromBody] string LoginToken) {
using (HttpClient client = new HttpClient()) {
var payload = new { ticket = LoginToken };
StringContent jsonPayload = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
HttpResponseMessage JWTResponse = await client.PostAsync("https://auth.mistox.com/api/auth/token", jsonPayload);
if (JWTResponse.IsSuccessStatusCode) {
string JWT = await JWTResponse.Content.ReadAsStringAsync();
signIn(JWT);
return Ok(); return Ok();
} else {
string error = await JWTResponse.Content.ReadAsStringAsync();
return NotFound(error);
} }
} }
return NotFound("Not logged in");
} catch (Exception ex) {
Console.WriteLine("ChangePassword Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
} }
[Route("toggleaccountlock")] [HttpGet("logout")]
[HttpPost]
public async Task<ActionResult<string>> ToggleAccountLock([FromForm] bool AccountLock) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
user.FailedPasswordLock = AccountLock;
user.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(user);
return Ok();
}
return NotFound("Not logged in");
} catch (Exception ex) {
Console.WriteLine("ToggleAccountLock Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
}
[Route("get")]
[HttpPost]
public async Task<ActionResult<Account>> Get() {
try {
if (isLoggedIn()) {
return Ok(await getLoggedInUser());
}
return NotFound("Not logged in");
} catch (Exception ex) {
Console.WriteLine("Get Error: " + ex);
return NotFound("An internal server error has occured");
}
}
[Route("logout")]
[HttpPost]
public ActionResult Logout() { public ActionResult Logout() {
if (isLoggedIn()) { if (isLoggedIn()) {
BoredCareersJWT.SignOut(Response); signOut();
return Ok(); return Redirect("/");
} }
return NotFound(); return NotFound();
} }
[Route("sendverifyemail")]
[HttpPost]
public async Task<ActionResult<string>> SendVerify([FromForm] string UserName) {
try {
string key = "v" + UserName;
// Stop from sending multiple emails quickly
if (_emailContext._SentEmails.ContainsKey(key)) {
DateTime PreviousSentTime = _emailContext._SentEmails.GetValueOrDefault(key);
if (PreviousSentTime.AddMinutes(5) > DateTime.Now) {
return NotFound("Cannot sent another verify email until 5 minutes has elapsed");
}
else {
_emailContext._SentEmails.Remove(key);
}
}
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null) {
test.EmailToken = Guid.NewGuid().ToString();
await _databaseService.SetAccount(test);
string EmailContents = EmailService.VerifyEmailEmail;
EmailContents = Substitue(EmailContents, "@UserName", UserName);
EmailContents = Substitue(EmailContents, "@UserName", UserName);
EmailContents = Substitue(EmailContents, "@VerifyPassword", test.EmailToken);
string result = _emailContext.Send(test.Email, EmailService.VerifyEmailSubject, EmailContents);
_emailContext._SentEmails.Add(key, DateTime.Now);
return Ok(result);
}
return NotFound("Account not found");
} catch (Exception) {
return NotFound("An internal server error has occured");
}
}
[Route("verifyemail")]
[HttpPost]
public async Task<ActionResult<bool>> VerifyEmail([FromForm] string UserName, [FromForm] string EmailToken) {
try {
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null) {
if (!string.IsNullOrEmpty(test.EmailToken) && test.EmailToken == EmailToken) {
test.EmailToken = "";
test.EmailVerified = true;
await _databaseService.SetAccount(test);
return Ok(true);
}
}
return NotFound("Account not found or token is invalid");;
} catch {
return NotFound("An internal server error has occured");
}
}
[Route("sendresetpassword")]
[HttpPost]
public async Task<ActionResult<string>> ResetPassword([FromForm] string Email) {
try {
string key = "p" + Email.ToLower();
// Stop from sending multiple emails quickly
if (_emailContext._SentEmails.ContainsKey(key)) {
DateTime PreviousSentTime = _emailContext._SentEmails.GetValueOrDefault(key);
if (PreviousSentTime.AddMinutes(5) > DateTime.Now) {
return NotFound("Cannot sent another reset requests until 5 minutes has elapsed");
}
else {
_emailContext._SentEmails.Remove(key);
}
}
Account? test = await _databaseService.GetAccount(Email.ToLower());
if (test != null) {
test.EmailToken = Guid.NewGuid().ToString();
await _databaseService.SetAccount(test);
string EmailContents = EmailService.ResetPasswordEmail;
EmailContents = Substitue(EmailContents, "@UserName", test.UserName);
EmailContents = Substitue(EmailContents, "@UserName", test.UserName);
EmailContents = Substitue(EmailContents, "@ResetPassWord", test.EmailToken);
string result = _emailContext.Send(test.Email, EmailService.VerifyEmailSubject, EmailContents);
_emailContext._SentEmails.Add(key, DateTime.Now);
return Ok(result);
}
return NotFound("Account Not Found");
} catch (Exception e) {
Console.WriteLine("EmailService Error: " + e.ToString());
return NotFound("An internal server error has occured");
} }
} }
[Route("resetpassword")]
[HttpPost]
public async Task<ActionResult<bool>> ResetPwdVerify([FromForm] string UserName, [FromForm] string NewPassword, [FromForm] string ResetToken) {
try {
Account? test = await _databaseService.GetAccount(UserName.ToLower());
if (test != null && !string.IsNullOrEmpty(test.EmailToken)) {
if (!string.IsNullOrEmpty(test.EmailToken) && test.EmailToken == ResetToken) {
test.CurrentPasswordAttempts = 0;
test.EmailToken = "";
test.PasswordHash = BCrypt.Net.BCrypt.HashPassword(NewPassword);
await _databaseService.SetAccount(test);
return Ok(true);
}
}
return NotFound("Account not found or reset token is bad");
} catch {
return NotFound("An internal server error has occured");
}
}
[Route("delete")]
[HttpPost]
public async Task<ActionResult> delete([FromForm] string Password) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
if (BCrypt.Net.BCrypt.Verify(Password, user.PasswordHash)) {
await _databaseService.DeleteAccount(user.ID);
return Ok();
}
}
return NotFound("User is not logged in");
} catch (Exception ex) {
Console.WriteLine("Delete Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
}
}
}
+22 -6
View File
@@ -13,6 +13,19 @@ namespace BoredCareers.Controllers {
_databaseService = databaseService; _databaseService = databaseService;
} }
public void signIn(string JWT) {
Response.Cookies.Append("mistox_session", JWT, new CookieOptions {
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddDays(7)
});
}
public void signOut() {
Response.Cookies.Delete("mistox_session");
}
public bool isLoggedIn() { public bool isLoggedIn() {
if (User.Identity != null && User.Identity.IsAuthenticated) { if (User.Identity != null && User.Identity.IsAuthenticated) {
return true; return true;
@@ -24,13 +37,16 @@ namespace BoredCareers.Controllers {
return Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier)); return Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier));
} }
public async Task<Account> getLoggedInUser() { public Account getLoggedInUser() {
try { try {
Account? test = await _databaseService.GetAccount(getLoggedInUserID()); Account building = new Account {
if (test != null) { ID = Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier)),
return test; UserName = User.FindFirstValue(ClaimTypes.Name)!.ToString(),
} Email = User.FindFirstValue(ClaimTypes.Email)!.ToString(),
return new Account(); Role = User.FindFirstValue(ClaimTypes.Role)!.ToString(),
DataServer = User.FindFirstValue(ClaimTypes.UserData)!.ToString()
};
return building;
} catch { } catch {
return new Account(); return new Account();
} }
+26 -19
View File
@@ -7,7 +7,7 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Text; using System.Security.Cryptography;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -39,15 +39,6 @@ string dbPass = !string.IsNullOrEmpty(_dbpass) ? _dbpass : "oasv34$8gpv023dd";
DatabaseService databaseService = new DatabaseService(connectionString: "server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;"); DatabaseService databaseService = new DatabaseService(connectionString: "server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;");
builder.Services.Add( new ServiceDescriptor( typeof( DatabaseService ), databaseService ) ); builder.Services.Add( new ServiceDescriptor( typeof( DatabaseService ), databaseService ) );
////////////////////////////////
////////// Auth Service ////////
////////////////////////////////
// Address
string? _jwtSecret = Environment.GetEnvironmentVariable("JWTsecret");
string JWTsecret = !string.IsNullOrEmpty(_jwtSecret) ? _jwtSecret : "v0Ftluhdh7Nht8^2b5eaiC^IS^VS1ku0VBs3j*B2";
BoredCareersJWT.TokenSecretKey = JWTsecret;
//////////////////////////////// ////////////////////////////////
///////// Email Service //////// ///////// Email Service ////////
//////////////////////////////// ////////////////////////////////
@@ -92,7 +83,26 @@ if (IPayment._PaymentType == PaymentType.StripeIntent) {
IPayment._EndpointSecret = string.IsNullOrEmpty(StripeEndpointKey) ? "" : StripeEndpointKey; IPayment._EndpointSecret = string.IsNullOrEmpty(StripeEndpointKey) ? "" : StripeEndpointKey;
} }
// Authentication Service ////////////////////////////////
/////// Auth Service ////////
////////////////////////////////
RsaSecurityKey? PublicKey = null;
using (HttpClient client = new HttpClient()) {
HttpResponseMessage PublicKeyResponse = await client.GetAsync("https://auth.mistox.com/api/auth/publickey");
if (PublicKeyResponse.IsSuccessStatusCode) {
string publicKey = await PublicKeyResponse.Content.ReadAsStringAsync();
RSA rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
PublicKey = new RsaSecurityKey(rsa);
}
}
if (PublicKey == null) {
Console.WriteLine("Unable to load RSA PubKey Shutting Down");
Environment.Exit(100);
}
builder.Services.AddAuthentication(options => { builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
@@ -102,14 +112,14 @@ builder.Services.AddAuthentication(options => {
ValidateAudience = true, ValidateAudience = true,
ValidateLifetime = true, ValidateLifetime = true,
ValidateIssuerSigningKey = true, ValidateIssuerSigningKey = true,
ValidIssuer = BoredCareersJWT.TokenIssuer, ValidIssuer = "https://auth.mistox.com",
ValidAudience = BoredCareersJWT.TokenAudience, ValidAudience = "mistox-llc-auth-token",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(BoredCareersJWT.TokenSecretKey)), IssuerSigningKey = PublicKey,
ClockSkew = TimeSpan.FromMinutes(1) ClockSkew = TimeSpan.FromMinutes(1)
}; };
options.Events = new JwtBearerEvents { options.Events = new JwtBearerEvents {
OnMessageReceived = context => { OnMessageReceived = context => {
context.Token = context.Request.Cookies[BoredCareersJWT.TokenName]; context.Token = context.Request.Cookies["mistox_session"];
return Task.CompletedTask; return Task.CompletedTask;
}, },
OnTokenValidated = context => { OnTokenValidated = context => {
@@ -118,10 +128,7 @@ builder.Services.AddAuthentication(options => {
var exp = jwtToken.ValidTo; var exp = jwtToken.ValidTo;
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
if ((exp - now) < TimeSpan.FromDays(3)) { if ((exp - now) < TimeSpan.FromDays(3)) {
int accountID = Convert.ToInt32(context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value); // Impliment token refresh
bool isPersistent = bool.Parse(context.Principal?.FindFirst(ClaimTypes.IsPersistent)?.Value);
var newJWT = BoredCareersJWT.GenereateJWTToken(accountID, isPersistent);
BoredCareersJWT.SignIn(context.HttpContext.Response, isPersistent, newJWT);
} }
} }
return Task.CompletedTask; return Task.CompletedTask;
-57
View File
@@ -1,57 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace BoredCareers.Services {
public class BoredCareersJWT {
public static string TokenAudience = "mistox-llc-auth-token";
public static string TokenIssuer = "https://auth.mistox.com";
public static string TokenSecretKey = "";
public static string TokenName = "mistox_session";
public static string GenereateJWTToken(int accountID, bool StayLoggedIn) {
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(TokenSecretKey);
var tokenDiscriptor = new SecurityTokenDescriptor {
Subject = new ClaimsIdentity([
new Claim(ClaimTypes.NameIdentifier, accountID.ToString()),
new Claim(ClaimTypes.IsPersistent, StayLoggedIn.ToString())
]),
Expires = DateTime.UtcNow.AddDays(7),
IssuedAt = DateTime.UtcNow,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256),
Audience = TokenAudience,
Issuer = TokenIssuer
};
var token = tokenHandler.CreateToken(tokenDiscriptor);
return tokenHandler.WriteToken(token);
}
public static void SignIn(HttpResponse Response, bool StayLoggedIn, string jwt) {
if (StayLoggedIn) {
// Stay logged in cookie
Response.Cookies.Append(TokenName, jwt, new CookieOptions {
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddDays(7)
});
} else {
// Session cookie
Response.Cookies.Append(TokenName, jwt, new CookieOptions {
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
});
}
}
public static void SignOut(HttpResponse Response) {
Response.Cookies.Delete(TokenName);
}
}
}