Init commit
Docker Build and Release Upload / build (push) Has been cancelled

This commit is contained in:
2025-07-24 17:10:03 -07:00
commit d8bc76cf0b
57 changed files with 11280 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"Mistox-Frontend": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"polyfills": ["zone.js" ],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
],
"aot": true,
"outputMode": "static",
"outputPath": {
"base": "../../debug/wwwroot",
"browser": ""
},
"deleteOutputPath": false
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "Mistox-Frontend:build:production"
},
"development": {
"buildTarget": "Mistox-Frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
}
}
}
}
},
"cli": {
"analytics": false
}
}
+8931
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
{
"name": "mistox-frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"@stripe/stripe-js": "^7.4.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.2",
"@angular/cli": "^20.0.2",
"@angular/compiler-cli": "^20.0.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.7.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.8.2"
}
}
+13
View File
@@ -0,0 +1,13 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi())
]
};
+105
View File
@@ -0,0 +1,105 @@
.top-bar {
display: flex;
width: 100%;
height: 200px;
background: linear-gradient(0deg,#99999988, #000000FF);
backdrop-filter: blur(2px);
}
.top-bar-logo {
width: 180px;
height: 180px;
margin: 10px
}
.top-bar-buttons {
display: flex;
width: calc(50% - 110px);
height: 180px;
margin: 10px;
}
.flex-right {
justify-content: end;
}
.nav-button {
height: 15px;
width: 150px;
border-radius: 5px;
margin: 10px;
text-align: center;
padding: 15px 0;
transition: 0.5s;
background-color: #00000000;
border: 1px solid var(--Mistox-White);
color: var(--Mistox-White);
text-decoration: none;
}
.nav-button:hover {
background-color: #00000044;
color: var(--Mistox-Light);
}
.active {
background-color: #00000010;
color: var(--Mistox-Frame);
}
.nav-button-login {
width: 100px;
padding: 10px 0;
}
.content {
width: 100%;
min-height: calc(100vh - 300px);
}
.bottom-bar {
display: flex;
width: 100%;
height: 100px;
background: linear-gradient(180deg,#99999988, #000000FF);
backdrop-filter: blur(2px);
}
.bottom-bar-logo {
width: 80px;
height: 80px;
margin: 10px 50px;
}
.bottom-bar-logo img {
width: 80px;
height: 80px;
}
.bottom-bar-buttons {
display: flex;
width: calc(50% - 110px);
height: 80px;
margin: 10px;
}
.nav-button-bottom {
height: 15px;
width: 150px;
border-radius: 5px;
text-align: center;
padding: 5px 0;
transition: 0.5s;
background-color: #00000000;
text-decoration: none;
}
.bottom-bar-float {
align-items: end;
color: var(--Mistox-White);
}
.bottom-bar-padding {
color: var(--Mistox-White);
margin: 20px;
}
+36
View File
@@ -0,0 +1,36 @@
<div class="top-bar">
<div class="top-bar-buttons">
<a #homeLink class="nav-button" routerLink="">HOME</a>
<a #jobsLink class="nav-button" routerLink="/jobs">JOB BOARD</a>
<a #resumesLink class="nav-button" routerLink="/resumes">RESUMES</a>
</div>
<a class="top-bar-logo" routerLink="">
<img class="top-bar-logo" style="margin: 0;" src="img/logo-full.png" />
</a>
<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/logout"><span>LOGOUT</span></a>
</div>
<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" routerLink="/account/register"><span>REGISTER</span></a>
</div>
</div>
<div class="content">
<router-outlet></router-outlet>
</div>
<div class="bottom-bar">
<div class="bottom-bar-buttons bottom-bar-float">
<a class="nav-button-bottom bottom-bar-padding" routerLink="/contact">CONTACT</a>
<a class="nav-button-bottom bottom-bar-padding" routerLink="/privacy">PRIVACY</a>
<a class="nav-button-bottom bottom-bar-padding" routerLink="/about">ABOUT</a>
</div>
<a class="bottom-bar-logo" href="https://mistox.com">
<img src="img/mistox-logo.png" />
</a>
<div class="bottom-bar-buttons flex-right bottom-bar-float">
<div class="bottom-bar-padding">
<span>Mistox LLC</span>
</div>
</div>
</div>
+20
View File
@@ -0,0 +1,20 @@
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 { 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';
export const routes: Routes = [
{ 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 },
]
+29
View File
@@ -0,0 +1,29 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { Router, RouterModule, RouterOutlet } from '@angular/router';
import { Authentication } from './services/Authentication';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
imports: [RouterOutlet, CommonModule, RouterModule],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
@ViewChild('homeLink') homeLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('jobsLink') jobLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('resumesLink') resumeLink!: ElementRef<HTMLAnchorElement>;
constructor(public auth: Authentication, private router: Router){}
ngAfterViewInit(){
let ViewLinks = [ this.homeLink, this.resumeLink, this.jobLink ];
ViewLinks.forEach(link => {
if (new URL(link.nativeElement.href).pathname === new URL(window.location.href).pathname){
link.nativeElement.classList.add("active");
}
});
}
}
+13
View File
@@ -0,0 +1,13 @@
export class Account {
public id: number = 0;
public userName: string = "";
public email: string = "";
public emailVerified: boolean = false;
public passwordHash: string = "";
public failedPasswordLock: boolean = false;
public passwordAttempts: number = 5;
public currentPasswordAttempts: number = 0;
public role: string = "Generic";
public emailToken: string = "";
public dataServer: string = "";
}
@@ -0,0 +1,23 @@
<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>
@@ -0,0 +1,55 @@
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);
}
});
}
}
@@ -0,0 +1,35 @@
<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>
@@ -0,0 +1,55 @@
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 ];
}
})
}
}
@@ -0,0 +1,30 @@
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 = "";
}
});
}
}
@@ -0,0 +1,35 @@
<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>
@@ -0,0 +1,80 @@
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 ]
}
});
}
}
@@ -0,0 +1,29 @@
<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>
@@ -0,0 +1,69 @@
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);
}
});
}
}
}
@@ -0,0 +1,29 @@
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() {
}
}
@@ -0,0 +1,8 @@
<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>
@@ -0,0 +1,54 @@
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);
}
});
}
}
@@ -0,0 +1,83 @@
import { Injectable } from "@angular/core";
import { Account } from "../models/Account";
import { BehaviorSubject, Observable } from "rxjs";
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
@Injectable({ providedIn: 'root' })
export class Authentication{
private _user = new BehaviorSubject<Account>(this.getUserFromStorage());
user$ = this._user.asObservable();
constructor( private http: HttpClient){ }
Login(UserName: string, Password: string, StayLoggedIn: boolean): Observable<Account> {
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({
next: data => {
data.passwordHash = "";
this._user.next(data);
this.setUserToStorage(data, StayLoggedIn == true ? SessionType.Forever : SessionType.Session);
},
error: err => {
console.log("HTTP Error Signing In: ", err.error);
}
});
return sub;
}
Logout(){
this._user.next( new Account );
this.delUserFromStorage();
return this.http.post<Account>( "api/account/logout", {}, { responseType: 'json' } );
}
get isLoggedIn(): boolean {
return this._user.value.id != -1 ? true : false;
}
get loggedInUser(): Account {
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
}
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Auth | Mistox</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="img/logo-full.png">
</head>
<body style="border: 0; margin: 0; padding: 0;">
<app-root></app-root>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
+23
View File
@@ -0,0 +1,23 @@
/* You can add global styles to this file, and also import other style files */
:root {
--Mistox-Dark: #2C0703;
--Mistox-Medium: #890620;
--Mistox-Light: #B6465F;
--Mistox-Bright: #FC440F;
--Mistox-Frame: #FF5A00CC;
--Mistox-Button: #ff9999;
--Mistox-Button-Hover: #ff999977;
--Mistox-White: #FFF;
--Mistox-Black: #000;
font-family: Arial, Helvetica, sans-serif;
}
html {
background-color: #000;
}
body {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cg fill-rule='evenodd'%3E%3Cg id='hexagons' fill='%23999999' fill-opacity='0.2' fill-rule='nonzero'%3E%3Cpath d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
background-color: #fff;
}
+15
View File
@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
]
}
+35
View File
@@ -0,0 +1,35 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve",
"baseUrl": "src",
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+14
View File
@@ -0,0 +1,14 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}
+279
View File
@@ -0,0 +1,279 @@
using Microsoft.AspNetCore.Mvc;
using Auth.Services;
using Auth.Services.DatabaseService;
using Auth.Entities;
using System.Web.Http;
namespace Auth.Controllers {
[ApiController]
[Route("api/account/")]
public class AuthenticationController : MistoxControllerBase {
EmailService _emailContext;
public AuthenticationController(DatabaseService db, EmailService emailContext) : base(db) {
_emailContext = emailContext;
}
[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 = AuthJWT.GenereateJWTToken(test.ID, StayLoggedIn);
AuthJWT.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()) {
Account user = await getLoggedInUser();
if (BCrypt.Net.BCrypt.Verify(OldPassword, user.PasswordHash)) {
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(NewPassword);
user.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(user);
return Ok();
}
}
return NotFound("Not logged in");
} catch (Exception ex) {
Console.WriteLine("ChangePassword Error: " + ex.Message);
return NotFound("An internal server error has occured");
}
}
[Route("toggleaccountlock")]
[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() {
if (isLoggedIn()) {
AuthJWT.SignOut(Response);
return Ok();
}
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");
}
}
}
}
@@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Mvc;
using Auth.Entities;
using Auth.Services.DatabaseService;
using System.Security.Claims;
namespace Auth.Controllers {
public class MistoxControllerBase : ControllerBase {
public DatabaseService _databaseService;
public MistoxControllerBase(DatabaseService databaseService) {
_databaseService = databaseService;
}
public bool isLoggedIn() {
if (User.Identity != null && User.Identity.IsAuthenticated) {
return true;
}
return false;
}
public int getLoggedInUserID() {
return Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
public async Task<Account> getLoggedInUser() {
try {
Account? test = await _databaseService.GetAccount(getLoggedInUserID());
if (test != null) {
return test;
}
return new Account();
} catch {
return new Account();
}
}
public string Substitue(string message, string subString, string Replacement) {
for (int i = 0; i < (message.Length - subString.Length); i++) {
if (message.Substring(i, subString.Length) == subString) {
string before = message.Substring(0, i);
string after = message.Substring(i + subString.Length);
return before + Replacement + after;
}
}
return message;
}
public bool contains(string outer, string inner) {
if (outer.Length >= inner.Length) {
for (int i = 0; i < outer.Length - inner.Length; i++) {
if (outer.Substring(i, inner.Length) == inner) {
return true;
}
}
}
return false;
}
}
}
+15
View File
@@ -0,0 +1,15 @@
namespace Auth.Entities {
public class Account {
public int ID { get; set; } // PK
public string UserName { get; set; } = "";
public string Email { get; set; } = "";
public bool EmailVerified { get; set; } = false;
public string PasswordHash { get; set; } = "";
public bool FailedPasswordLock { get; set; } = false;
public int PasswordAttempts { get; set; } = 5;
public int CurrentPasswordAttempts { get; set; } = 0;
public string Role { get; set; } = "Generic";
public string EmailToken { get; set; } = "";
public string DataServer { get; set; } = "";
}
}
+153
View File
@@ -0,0 +1,153 @@
using Auth.Services;
using Auth.Services.DatabaseService;
using System.Threading.RateLimiting;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Disable null warnings becuse string.IsNullOrEmpty checks for NULL or Empty
#pragma warning disable CS8604
////////////////////////////////
/////// Database Service ///////
////////////////////////////////
// Address
string? _dbserver = Environment.GetEnvironmentVariable("MySQLServer");
string dbserver = !string.IsNullOrEmpty(_dbserver) ? _dbserver : "localhost";
// Database
string? _dbdatabase = Environment.GetEnvironmentVariable("MySQLDatabase");
string dbdatabase = !string.IsNullOrEmpty(_dbdatabase) ? _dbdatabase : "Auth";
// UserName
string? _dbuser = Environment.GetEnvironmentVariable("MySQLUser");
string dbUser = !string.IsNullOrEmpty(_dbuser) ? _dbuser : "root";
// Password
string? _dbpass = Environment.GetEnvironmentVariable("MySQLPass");
string dbPass = !string.IsNullOrEmpty(_dbpass) ? _dbpass : "oasv34$8gpv023dd";
// Create the database serivice
DatabaseService databaseService = new DatabaseService(connectionString: "server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;");
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";
AuthJWT.TokenSecretKey = JWTsecret;
////////////////////////////////
///////// Email Service ////////
////////////////////////////////
// Address
string? _eServer = Environment.GetEnvironmentVariable("EmailServer");
string EmailServer = !string.IsNullOrEmpty(_eServer) ? _eServer : "mail.mistox.com";
// Port
string? _ePort = Environment.GetEnvironmentVariable("EmailPort");
int EmailPort = !string.IsNullOrEmpty(_ePort) ? Convert.ToInt32(_ePort) : 587;
// User
string? _eAddress = Environment.GetEnvironmentVariable("EmailAddress");
string EmailAddress = !string.IsNullOrEmpty(_eAddress) ? _eAddress : "no-reply@mistox.com";
// Password
string? _ePassword = Environment.GetEnvironmentVariable("EmailPassword");
string EmailPassword = !string.IsNullOrEmpty(_ePassword) ? _ePassword : "";
// Create the email service
EmailService Emailservice = new EmailService( EmailServer, EmailPort, EmailAddress, EmailPassword );
builder.Services.Add( new ServiceDescriptor( typeof( EmailService ), Emailservice ));
// Authentication Service
builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = AuthJWT.TokenIssuer,
ValidAudience = AuthJWT.TokenAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AuthJWT.TokenSecretKey)),
ClockSkew = TimeSpan.FromMinutes(1)
};
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
context.Token = context.Request.Cookies[AuthJWT.TokenName];
return Task.CompletedTask;
},
OnTokenValidated = context => {
var jwtToken = context.SecurityToken as JwtSecurityToken;
if (jwtToken != null) {
var exp = jwtToken.ValidTo;
var now = DateTime.UtcNow;
if ((exp - now) < TimeSpan.FromDays(3)) {
int accountID = Convert.ToInt32(context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value);
bool isPersistent = bool.Parse(context.Principal?.FindFirst(ClaimTypes.IsPersistent)?.Value);
var newJWT = AuthJWT.GenereateJWTToken(accountID, isPersistent);
AuthJWT.SignIn(context.HttpContext.Response, isPersistent, newJWT);
}
}
return Task.CompletedTask;
}
};
});
builder.Services.AddCors(o => o.AddDefaultPolicy(builder => {
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); // No CORS
}));
builder.Services.AddRateLimiter(options => {
options.AddPolicy("PerUserPolicy", httpContext => {
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? httpContext.User.Identity?.Name
?? httpContext.Connection.RemoteIpAddress?.ToString();
return RateLimitPartition.GetTokenBucketLimiter(userId, key => new TokenBucketRateLimiterOptions {
TokenLimit = 10, // max 10 requests
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0,
ReplenishmentPeriod = TimeSpan.FromSeconds(15),
TokensPerPeriod = 2,
AutoReplenishment = true
});
});
});
// Pages Service
builder.Services.AddControllers();
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if( !app.Environment.IsDevelopment() ) {
app.UseHsts();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseCors();
app.UseRouting();
app.UseAuthentication();
app.MapControllers().RequireRateLimiting("perUserPolicy");
app.MapFallbackToFile("index.html");
app.Run();
+27
View File
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.3.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="MySql.Data" Version="9.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
+160
View File
@@ -0,0 +1,160 @@
using Auth.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
namespace Auth.Services.DatabaseService {
public partial class DatabaseService {
public async Task<Account?> GetAccount( string UserNameOrEmail ) {
Account? account = null;
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT *
FROM Account
WHERE UserName = @UorE OR Email = @UorE;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@UorE", UserNameOrEmail);
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
while( await reader.ReadAsync() ) {
if( reader == null ) { break; }
int _id = reader.GetInt32("ID");
string _username = reader.GetString("UserName");
string _email = reader.GetString("Email");
bool _emailVerified = reader.GetBoolean("EmailVerified");
string _passwordhash = reader.GetString("PasswordHash");
bool _failedpasswordlock = reader.GetBoolean( "FailedPasswordLock" );
int _passwordattempts = reader.GetInt32( "PasswordAttempts" );
int _curpasswordattempts = reader.GetInt32( "CurrentPasswordAttempts" );
string _role = reader.GetString( "Role" );
string _emailtoken = reader.GetString( "EmailToken" );
string _dataserver = reader.GetString( "DataServer" );
account = new Account() {
ID = _id,
UserName = _username,
Email = _email,
EmailVerified = _emailVerified,
PasswordHash = _passwordhash,
CurrentPasswordAttempts = _curpasswordattempts,
PasswordAttempts = _passwordattempts,
EmailToken = _emailtoken,
FailedPasswordLock = _failedpasswordlock,
Role = _role,
DataServer = _dataserver
};
}
}
}
return account;
}
public async Task<Account?> GetAccount( int AccountID ) {
Account? account = null;
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT *
FROM Account
WHERE ID = @ID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ID", AccountID);
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
while( await reader.ReadAsync() ) {
if( reader == null ) {
break;
}
int _id = reader.GetInt32("ID");
string _username = reader.GetString("UserName");
string _email = reader.GetString("Email");
bool _emailVerified = reader.GetBoolean("EmailVerified");
string _passwordhash = reader.GetString("PasswordHash");
bool _failedpasswordlock = reader.GetBoolean( "FailedPasswordLock" );
int _passwordattempts = reader.GetInt32( "PasswordAttempts" );
int _curpasswordattempts = reader.GetInt32( "CurrentPasswordAttempts" );
string _role = reader.GetString( "Role" );
string _emailtoken = reader.GetString( "EmailToken" );
string _dataserver = reader.GetString("DataServer");
account = new Account() {
ID = _id,
UserName = _username,
Email = _email,
EmailVerified = _emailVerified,
PasswordHash = _passwordhash,
CurrentPasswordAttempts = _passwordattempts,
PasswordAttempts = _passwordattempts,
EmailToken = _emailtoken,
FailedPasswordLock = _failedpasswordlock,
Role = _role,
DataServer = _dataserver
};
}
}
}
return account;
}
public async Task SetAccount( Account Profile ) {
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
INSERT INTO Account
(ID,UserName,Email,EmailVerified,PasswordHash,FailedPasswordLock,PasswordAttempts,CurrentPasswordAttempts,Role,EmailToken,DataServer)
VALUES
(@ID,@UserName,@Email,@EmailVerified,@PasswordHash,@FailedPasswordLock,@PasswordAttempts,@CurrentPasswordAttempts,@Role,@EmailToken,@DataServer)
ON DUPLICATE KEY UPDATE
UserName = @UserName,
Email = @Email,
EmailVerified = @EmailVerified,
PasswordHash = @PasswordHash,
FailedPasswordLock = @FailedPasswordLock,
PasswordAttempts = @PasswordAttempts,
CurrentPasswordAttempts = @CurrentPasswordAttempts,
Role = @Role,
EmailToken = @EmailToken,
DataServer = @DataServer;
";
MySqlCommand cmd = new MySqlCommand( command , connection);
cmd.Parameters.AddWithValue("@ID", Profile.ID);
cmd.Parameters.AddWithValue("@UserName", Profile.UserName);
cmd.Parameters.AddWithValue("@Email", Profile.Email);
cmd.Parameters.AddWithValue("@EmailVerified", Profile.EmailVerified);
cmd.Parameters.AddWithValue("@PasswordHash", Profile.PasswordHash);
cmd.Parameters.AddWithValue("@FailedPasswordLock", Profile.FailedPasswordLock);
cmd.Parameters.AddWithValue("@PasswordAttempts", Profile.PasswordAttempts);
cmd.Parameters.AddWithValue("@CurrentPasswordAttempts", Profile.CurrentPasswordAttempts);
cmd.Parameters.AddWithValue("@Role", Profile.Role);
cmd.Parameters.AddWithValue("@EmailToken", Profile.EmailToken);
cmd.Parameters.AddWithValue("@DataServer", Profile.DataServer);
await cmd.ExecuteNonQueryAsync();
}
}
public async Task DeleteAccount( int AccountID ) {
using( MySqlConnection connection = GetConnection() ) {
MySqlCommand cmd;
connection.Open();
string command = @"
DELETE FROM Account WHERE ID = @ID;
";
cmd = new MySqlCommand( command, connection );
cmd.Parameters.AddWithValue("@ID", AccountID);
await cmd.ExecuteNonQueryAsync();
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
using MySql.Data.MySqlClient;
namespace Auth.Services.DatabaseService {
public partial class DatabaseService {
public string ConnectionString {
get; set;
}
public DatabaseService( string connectionString ) {
ConnectionString = connectionString;
}
MySqlConnection GetConnection() {
return new MySqlConnection( ConnectionString );
}
}
}
+41
View File
@@ -0,0 +1,41 @@
using System.Net.Mail;
namespace Auth.Services {
public partial class EmailService {
public Dictionary<string, DateTime> _SentEmails = new Dictionary<string, DateTime>();
public string EmailServer = "";
public string EmailAddress = "";
public string EmailPassword = "";
public int EmailPort;
public EmailService( string _EmailServer, int _EmailPort, string _EmailAddress, string _EmailPassword ) {
EmailServer = _EmailServer;
EmailPort = _EmailPort;
EmailAddress = _EmailAddress;
EmailPassword = _EmailPassword;
}
public string Send( string Destination, string Subject, string Body ) {
using (SmtpClient client = new SmtpClient( EmailServer, EmailPort )){
client.EnableSsl = true;
client.Credentials = new System.Net.NetworkCredential( EmailAddress, EmailPassword );
try {
MailMessage msg = new MailMessage(){
IsBodyHtml = true,
Subject = Subject,
Body = Body
};
msg.From = new MailAddress( EmailAddress, "no-reply" );
msg.To.Add( new MailAddress( Destination ) );
client.Send( msg );
return "Success";
} catch( Exception e ) {
return "An Error Has Occurred Sending Email : " + e.ToString();
}
}
}
}
}
+51
View File
@@ -0,0 +1,51 @@
namespace Auth.Services {
public partial class EmailService {
// @UserName
// @ResetPassWord
// https://mistox.com/account/resetpassword?UserName=@UserName&ResetPwd=@ResetPassWord
public static string ResetPasswordSubject = "Password Reset Request";
public static string ResetPasswordEmail = @"
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
<title>Password Reset</title>
</head>
<body style=""font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;"">
<table role=""presentation"" style=""width: 100%; background-color: #f4f4f4; padding: 20px 0;"">
<tr>
<td>
<table role=""presentation"" style=""max-width: 600px; width: 100%; background-color: #ffffff; margin: 0 auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);"">
<tr>
<td style=""padding: 20px; text-align: center; background-color: #4CAF50; color: #ffffff; border-top-left-radius: 8px; border-top-right-radius: 8px;"">
<h2>Password Reset Request</h2>
</td>
</tr>
<tr>
<td style=""padding: 20px; text-align: left; font-size: 16px; color: #333333;"">
<p>Hi @UserName,</p>
<p>We received a request to reset your password. You can reset your password by clicking the button below:</p>
<p style=""text-align: center;"">
<a href=""https://mistox.com/account/resetpassword?UserName=@UserName&ResetPwd=@ResetPassWord"" style=""background-color: #4CAF50; color: #ffffff; text-decoration: none; padding: 15px 25px; font-size: 16px; border-radius: 5px; display: inline-block;"">Reset Password</a>
</p>
<p>If you didn't request a password reset, you can safely ignore this email.</p>
<p>Best regards</p>
</td>
</tr>
<tr>
<td style=""padding: 10px; text-align: center; background-color: #f4f4f4; color: #888888; font-size: 12px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;"">
<p>If you have any questions, feel free to <a href=""mailto:webmaster@mistox.com"" style=""color: #4CAF50; text-decoration: none;"">contact support</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
";
}
}
+52
View File
@@ -0,0 +1,52 @@
namespace Auth.Services {
public partial class EmailService {
// @UserName
// @VerifyPassword
// https://mistox.com/api/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword
public static string VerifyEmailSubject = "Verify Your Email Address";
public static string VerifyEmailEmail = @"
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
<title>Verify Your Email</title>
</head>
<body style=""font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;"">
<table role=""presentation"" style=""width: 100%; background-color: #f4f4f4; padding: 20px 0;"">
<tr>
<td>
<table role=""presentation"" style=""max-width: 600px; width: 100%; background-color: #ffffff; margin: 0 auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);"">
<tr>
<td style=""padding: 20px; text-align: center; background-color: #4CAF50; color: #ffffff; border-top-left-radius: 8px; border-top-right-radius: 8px;"">
<h2>Verify Email Request</h2>
</td>
</tr>
<tr>
<td style=""padding: 20px; text-align: left; font-size: 16px; color: #333333;"">
<p>Hi @UserName,</p>
<p>Thank you for making an account with us:</p>
<p>In order to start using your account we need to verify your email address by clicking the link below:</p>
<p style=""text-align: center;"">
<a href=""https://mistox.com/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword"" style=""background-color: #4CAF50; color: #ffffff; text-decoration: none; padding: 15px 25px; font-size: 16px; border-radius: 5px; display: inline-block;"">Verify Email</a>
</p>
<p>If you didn't create an account please ignore this email.</p>
<p>Best regards</p>
</td>
</tr>
<tr>
<td style=""padding: 10px; text-align: center; background-color: #f4f4f4; color: #888888; font-size: 12px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;"">
<p>If you have any questions, feel free to <a href=""mailto:webmaster@mistox.com"" style=""color: #4CAF50; text-decoration: none;"">contact support</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
";
}
}
+57
View File
@@ -0,0 +1,57 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace Auth.Services {
public class AuthJWT {
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);
}
}
}
+33
View File
@@ -0,0 +1,33 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", ".\Server.csproj", "{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Release|Any CPU.Build.0 = Release|Any CPU
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Release|Any CPU.Build.0 = Release|Any CPU
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B413876B-4048-47F1-B8B8-B974DF5E9E2A}
EndGlobalSection
EndGlobal
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB