15 Commits

Author SHA1 Message Date
derek 10868018fb Update to use Auth.Mistox.Com
Docker Build and Release Upload / build (push) Successful in 1m25s
2025-08-07 23:22:53 -07:00
derek 8ce7df46c2 Fix that should have been done long ago
Docker Build and Release Upload / build (push) Successful in 1m30s
2025-07-26 12:27:48 -07:00
derek f4aa59f2df Fix setaccount
Docker Build and Release Upload / build (push) Successful in 1m23s
2025-07-17 20:50:41 -07:00
derek ea542ca0a7 allow running on container
Docker Build and Release Upload / build (push) Successful in 1m23s
2025-07-14 22:00:12 +00:00
derek 3b169f18d9 Update route for proper routing
Docker Build and Release Upload / build (push) Has been cancelled
2025-07-14 21:59:18 +00:00
derek b8634dbc87 update build scripts
Docker Build and Release Upload / build (push) Successful in 1m24s
2025-07-10 18:45:25 -07:00
derek d33edd41cf "Crosscompile" Node as well
Docker Build and Release Upload / build (push) Successful in 1m16s
2025-07-10 18:34:57 -07:00
derek f9cfd204bf improve for bin/sh
Docker Build and Release Upload / build (push) Has been cancelled
2025-07-10 18:25:51 -07:00
derek d5e7b8b95d Fix bad if command
Docker Build and Release Upload / build (push) Failing after 53s
2025-07-10 18:21:55 -07:00
derek 9f24f8453e run as consecutive commands
Docker Build and Release Upload / build (push) Failing after 51s
2025-07-10 18:18:14 -07:00
derek d385f21f43 add buildkit step
Docker Build and Release Upload / build (push) Failing after 52s
2025-07-10 18:16:32 -07:00
derek 75fb6592f6 Try using cross compile instead of emulation
Docker Build and Release Upload / build (push) Failing after 3s
2025-07-10 18:12:27 -07:00
derek 34e4328050 Use new local docker server
Docker Build and Release Upload / build (push) Successful in 4s
2025-07-10 20:25:52 +00:00
derek bd4b4bc837 Publish to local docker server
Docker Build and Release Upload / build (push) Successful in 47s
2025-07-10 20:21:05 +00:00
derek 18b58b9b5d add cd/ci workflow
Docker Build and Release Upload / build (push) Successful in 24s
2025-07-09 19:34:17 -07:00
40 changed files with 284 additions and 1228 deletions
+1
View File
@@ -14,3 +14,4 @@ csharp_new_line_before_open_brace = none
csharp_new_line_before_catch = false csharp_new_line_before_catch = false
csharp_new_line_before_finally = false csharp_new_line_before_finally = false
csharp_new_line_after_else = false csharp_new_line_after_else = false
csharp_new_line_before_else = false
+33
View File
@@ -0,0 +1,33 @@
name: Docker Build and Release Upload
on:
push:
branches:
- main
jobs:
build:
runs-on: alpine-linux
steps:
- name: checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: build and push database
run: |
docker buildx build \
--platform=linux/amd64,linux/arm64 \
-t docker.mistox.net/mistox-sql \
--push \
./database
- name: build and push server
run: |
docker buildx build \
--platform=linux/amd64,linux/arm64 \
--build-arg BASE_URL=https://mistox.com \
-t docker.mistox.net/mistox-website \
--push \
.
-51
View File
@@ -1,51 +0,0 @@
name: Docker Build and Release Upload
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: build database
run: |
docker build -t mistox-sql ./database
- name: build server
run: |
docker build --build-arg BASE_URL=https://mistox.com -t mistox-website .
- name: create release folder
run: |
mkdir release
- name: export database
run: docker save mistox-sql -o release/mistox-sql.tar
- name: export server
run: docker save mistox-website -o release/mistox-website.tar
- name: create release
run: |
curl -X POST -H "Authorization: token ${{ secrets.PUBLISH_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"tag_name": "v0.0.1",
"name": "Release v0.0.1",
"body": "This is an automated release",
"draft": false,
"prerelease": false
}' \
https://git.mistox.net/api/v1/repos/derek/MistoxCom-Angular/releases
- name: publish database
run: |
curl -X POST -H "Authorization: token ${{ secrets.PUBLISH_TOKEN }}" \
-F name="mistox-sql.tar" \
-F attachment=@./release/mistox-sql.tar \
https://git.mistox.net/api/v1/repos/derek/MistoxCom-Angular/releases/v0.0.1/assets
+1
View File
@@ -22,6 +22,7 @@
}, },
"args": [ "args": [
"build", "build",
"--configuration=development",
"--base-href=http://localhost:5000" "--base-href=http://localhost:5000"
], ],
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
+11 -3
View File
@@ -2,7 +2,7 @@
## Build Frontend ## ## Build Frontend ##
###################### ######################
FROM node:alpine AS build-frontend FROM --platform=$BUILDPLATFORM node:alpine AS build-frontend
WORKDIR /src WORKDIR /src
# Define base address # Define base address
@@ -27,7 +27,7 @@ RUN ng build --base-href=${BASE_URL}
## Build Backend ## ## Build Backend ##
##################### #####################
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-backend FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build-backend
WORKDIR /src WORKDIR /src
# Copy the csproj # Copy the csproj
@@ -39,8 +39,16 @@ RUN dotnet restore './MistoxWebsite.Server.csproj'
# Copy the rest of the backend over # Copy the rest of the backend over
COPY ./src/MistoxWebsite.Server/ ./ COPY ./src/MistoxWebsite.Server/ ./
# Get the target arch
ARG TARGETARCH
# Build the source # Build the source
RUN dotnet publish './MistoxWebsite.Server.csproj' -c Release -o /app/publish RUN set -e && \
if [ "$TARGETARCH" = "arm64" ]; then RID="linux-arm64"; \
elif [ "$TARGETARCH" = "amd64" ]; then RID="linux-x64"; \
else echo "Unsupported ARCH: $TARGETARCH"; exit 1; \
fi && \
dotnet publish './MistoxWebsite.Server.csproj' -c Release -r ${RID} -o /app/publish
################ ################
## Publish ## ## Publish ##
-39
View File
@@ -1,20 +1,6 @@
CREATE DATABASE IF NOT EXISTS `mistox`; CREATE DATABASE IF NOT EXISTS `mistox`;
USE `mistox`; USE `mistox`;
CREATE TABLE IF NOT EXISTS `Account` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`UserName` varchar(60) DEFAULT NULL,
`Email` varchar(60) DEFAULT NULL,
`EmailVerified` tinyint(4) DEFAULT NULL,
`PasswordHash` varchar(100) DEFAULT NULL,
`FailedPasswordLock` tinyint(4) DEFAULT NULL,
`PasswordAttempts` int(11) DEFAULT NULL,
`CurrentPasswordAttempts` int(11) DEFAULT NULL,
`Role` varchar(45) DEFAULT NULL,
`EmailToken` varchar(45) DEFAULT NULL,
PRIMARY KEY (`ID`)
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `Product` ( CREATE TABLE IF NOT EXISTS `Product` (
`ID` int(11) NOT NULL AUTO_INCREMENT, `ID` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(45) DEFAULT NULL, `Name` varchar(45) DEFAULT NULL,
@@ -61,28 +47,3 @@ CREATE TABLE IF NOT EXISTS `Receipt` (
`TotalCost` int(11) DEFAULT NULL, `TotalCost` int(11) DEFAULT NULL,
PRIMARY KEY (`AccountID`,`ProductID`,`ReceiptID`) PRIMARY KEY (`AccountID`,`ProductID`,`ReceiptID`)
); );
INSERT INTO Account (
ID,
UserName,
Email,
EmailVerified,
PasswordHash,
FailedPasswordLock,
PasswordAttempts,
CurrentPasswordAttempts,
Role,
EmailToken
) VALUES (
1,
'admin',
'admin@mistox.com',
1,
'$2a$11$0UeWLLqTXe3FG161QVuI0OQJ9rulspUpMG581DI6KSzDXBbFKd00S',
1,
1,
5,
0,
'Admin',
''
);
+2 -2
View File
@@ -2,7 +2,7 @@ services:
mistox-server: mistox-server:
container_name: mistox_server container_name: mistox_server
image: mistox-website:latest image: docker.mistox.net/mistox-website:latest
restart: always restart: always
environment: environment:
- PaymentService=${Payment_Service} - PaymentService=${Payment_Service}
@@ -24,7 +24,7 @@ services:
mistox-database: mistox-database:
container_name: mistox_database container_name: mistox_database
image: mistox-sql:latest image: docker.mistox.net/mistox-sql:latest
restart: always restart: always
volumes: volumes:
- ./data:/var/lib/mysql - ./data:/var/lib/mysql
+11 -6
View File
@@ -41,13 +41,18 @@
</div> </div>
<!-- Login Stuff --> <!-- Login Stuff -->
<div class="nav-login"> <div class="nav-login">
<div *ngIf="auth.isLoggedIn"> <div *ngIf="auth.isLoggedIn" class="top-bar-buttons flex-right">
<a class="nav-login-button" href="/account/settings"><span>{{ auth.loggedInUser.userName }}</span></a> <a class="nav-login-button" href="https://auth.mistox.com/"><span>{{ auth.loggedInUser.userName.toUpperCase() }}</span></a>
<a class="nav-login-button" href="/account/logout"><span>Logout</span></a> <a class="nav-login-button" href="/api/account/logout"><span>LOGOUT</span></a>
</div> </div>
<div *ngIf="!auth.isLoggedIn"> <div *ngIf="!auth.isLoggedIn" class="top-bar-buttons flex-right">
<a class="nav-login-button" href="/account/login"><span>Login</span></a>
<a class="nav-login-button" href="/account/register"><span>Register</span></a> <!-- Testing Login -->
<a *ngIf="devMode" class="nav-login-button" href="https://auth.mistox.com/account/login?returnURL=http://localhost:5000/"><span>Testing Login</span></a>
<!-- Testing Login -->
<a class="nav-login-button" href="https://auth.mistox.com/account/login?returnURL=https://mistox.com/"><span>LOGIN</span></a>
<a class="nav-login-button" href="https://auth.mistox.com/account/register?returnURL=https://mistox.com/"><span>REGISTER</span></a>
</div> </div>
</div> </div>
</nav> </nav>
@@ -1,28 +1,11 @@
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 { MistComponent } from './pages/project/mist/mist.component'; import { MistComponent } from './pages/project/mist/mist.component';
import { CatalogComponent } from './pages/store/catalog/catalog.component'; import { CatalogComponent } from './pages/store/catalog/catalog.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 { NewItemComponent } from './pages/store/admin/newitem/new.component'; import { NewItemComponent } from './pages/store/admin/newitem/new.component';
import { EditItemComponent } from './pages/store/admin/edititem/edit.component'; import { EditItemComponent } from './pages/store/admin/edititem/edit.component';
export const routes: Routes = [ export const routes: Routes = [
// 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 },
// Projects // Projects
{ path: "project/mist", component: MistComponent }, { path: "project/mist", component: MistComponent },
@@ -33,6 +16,4 @@ export const routes: Routes = [
{ path: "store/admin/new", component: NewItemComponent }, { path: "store/admin/new", component: NewItemComponent },
{ path: "store/admin/edit", component: EditItemComponent }, { path: "store/admin/edit", component: EditItemComponent },
// Legal
{ path: "about", component: AboutComponent },
] ]
+31 -4
View File
@@ -1,7 +1,8 @@
import { Component, ElementRef, ViewChild } from '@angular/core'; import { Component, ElementRef, isDevMode, ViewChild } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router'; import { ActivatedRoute, Router, RouterOutlet } 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',
@@ -11,12 +12,38 @@ import { CommonModule } from '@angular/common';
}) })
export class App { export class App {
devMode: boolean = false;
@ViewChild('homeLink') homeLink!: ElementRef<HTMLAnchorElement>; @ViewChild('homeLink') homeLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('mistLink') mistLink!: ElementRef<HTMLAnchorElement>; @ViewChild('mistLink') mistLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('storeLink') storeLink!: ElementRef<HTMLAnchorElement>; @ViewChild('storeLink') storeLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('aboutLink') aboutLink!: ElementRef<HTMLAnchorElement>; @ViewChild('aboutLink') aboutLink!: 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.devMode = isDevMode();
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.mistLink, this.storeLink, this.aboutLink ]; let ViewLinks = [ this.homeLink, this.mistLink, this.storeLink, this.aboutLink ];
@@ -1,15 +1,7 @@
import { WebSiteData } from "./WebsiteData";
export class Account { export class Account {
public id: number = -1; public id: number | null = null;
public userName: string = ""; public userName: string = "";
public email: string = ""; public email: string = "";
public emailVerified: boolean = false; public role: string = "";
public passwordHash: string = ""; public dataServer: string = "";
public siteData: WebSiteData = new WebSiteData();
public error: string = "";
constructor(init?: Partial<Account>) {
Object.assign(this, init);
}
} }
@@ -1,5 +1,5 @@
export class Product { export class Product {
public id: number = -1; public id: number | null = null;
public name: string = ""; public name: string = "";
public description: string = ""; public description: string = "";
public curShowingIMG: number = 0; public curShowingIMG: number = 0;
@@ -1,5 +1,5 @@
export class WebSiteData { export class WebSiteData {
public accountID: number = -1; public accountID: number | null = null;
public failedPasswordLock: boolean = false; public failedPasswordLock: boolean = false;
public passwordAttempts: number = 5; public passwordAttempts: number = 5;
public currentPasswordAttempts: number = 0; public currentPasswordAttempts: number = 0;
@@ -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,57 +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(
data => {
if (data.error.length === 0){
this.router.navigate([this.returnURL]);
}else{
this.errorMsgs.pop();
this.errorMsgs.push(data.error);
}
}
)
}
}
@@ -1,27 +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();
this.router.navigate(["/"]);
}
}
@@ -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,85 +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) => {
if (data.error.length === 0){
this.errorMsgs = ["Account Created"];
await this.sleep(3000);
this.router.navigate([this.returnURL]);
}else{
this.errorMsgs = [];
this.errorMsgs.push(data.error);
}
},
error: err => {
console.log("HTTP Error Signing In: ", err);
}
});
}
}
@@ -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);
}
});
}
}
@@ -1,28 +0,0 @@
<div class="center">
<div class="big-frame background-border text-frame">
<p>Welcome to Mistox LLC. A project and hobby of Derek Holloway.</p>
<br />
<p>I am an indi-developer who has been making small projects since I was 13. I originally learned lua and spent 4 years mastering it. Then I moved onto C# which is my preferred language</p>
<p>My programming catalog consist of C#, Lua, SQL, C++, C, and JavaScript in the order of knowledge from best to passiable.</p>
<p>Im currently in college for computer sciences and should honestly be doing that instead of this but I find working on this website and hobby games to be way more enjoyable.</p>
<br />
<p>I would love to learn how to use Blender in order to make all the models for my games but with the amount of work ive already made for myself im going to hold off for now.</p>
<p>This website and everything on it are the long countless hours of my time and motivation to create something that I can be proud of and share that with the world.</p>
<p>So if you would like to support me as a small creator please feel free to leave a donation from on the store page. It would means a lot to me.</p>
<br />
<p>For the nerds out there, this website is a blazor webassembly app, hosted on an ubuntu webserver, with a mysql backend.</p>
<p>All the passwords are encrypted using bcrypt for your safety and all the data is only allowed through SSL.</p>
<p>After you make your account. All the data in the database is easily accessable through the account settings and</p>
<p>you can delete your account at any time. Including all your data with it so there is no risk.</p>
<p>I wont show ads and never will and I refuse to use trackers on this site.</p>
<br />
<br />
<p>If you have any questions, concerns, or would like to suggest a feature, bug-fix, or request to help. Please feel</p>
<p>free to reach out to me at <a href="mailto://derek@mistox.net">derek&commat;mistox.net</a></p>
<a href='https://ko-fi.com/A0A3TSI2D' target='_blank'>
<img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi6.png?v=6' alt='Buy Me a Coffee at ko-fi.com' />
</a>
</div>
</div>
@@ -1,19 +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';
@Component({
selector: 'legal-about',
templateUrl: './about.component.html',
imports: [ FormsModule, CommonModule ]
})
export class AboutComponent {
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
this.title.setTitle("About | Mistox");
};
}
@@ -32,13 +32,13 @@ export class EditItemComponent {
} }
// If user is not Admin -> route home // If user is not Admin -> route home
if (auth.loggedInUser.siteData.role != "Admin"){ if (auth.loggedInUser.role != "Admin"){
router.navigate(["/"]); router.navigate(["/"]);
} }
// Load product // Load product
const formData = new FormData(); const formData = new FormData();
formData.append("productID", this.newItem.id.toString()); formData.append("productID", this.newItem.id!.toString());
this.http.post<Product>( "api/product/get", formData ).subscribe({ this.http.post<Product>( "api/product/get", formData ).subscribe({
next: async (data) => { next: async (data) => {
this.newItem = data; this.newItem = data;
@@ -28,7 +28,7 @@ export class NewItemComponent {
} }
// If user is not Admin -> route home // If user is not Admin -> route home
if (auth.loggedInUser.siteData.role != "Admin"){ if (auth.loggedInUser.role != "Admin"){
router.navigate(["/"]); router.navigate(["/"]);
} }
}; };
@@ -15,16 +15,16 @@
</div> </div>
<h2 class="gameCard-Price">${{ (product.cost/100).toFixed(2) }}</h2> <h2 class="gameCard-Price">${{ (product.cost/100).toFixed(2) }}</h2>
<button class="gameCard-Button" >Add To Cart</button> <button class="gameCard-Button" >Add To Cart</button>
<div *ngIf="auth.loggedInUser.siteData.role == 'Admin'"> <div *ngIf="auth.loggedInUser.role == 'Admin'">
<button style="width: calc(50% - 10px); margin: 5px;" [routerLink]="['/store/admin/edit']" [queryParams]="{ ProductID: product.id }" > <button style="width: calc(50% - 10px); margin: 5px;" [routerLink]="['/store/admin/edit']" [queryParams]="{ ProductID: product.id }" >
Edit Edit
</button> </button>
<button style="width: calc(50% - 10px); margin: 5px;" (click)="DeleteItem(product.id)" > <button style="width: calc(50% - 10px); margin: 5px;" (click)="DeleteItem(product.id!)" >
Delete Delete
</button> </button>
</div> </div>
</div> </div>
<div *ngIf="auth.loggedInUser.siteData.role == 'Admin'"> <div *ngIf="auth.loggedInUser.role == 'Admin'">
<button style="width: calc(100% - 10px); margin: 5px;" [routerLink]="['/store/admin/new']" > <button style="width: calc(100% - 10px); margin: 5px;" [routerLink]="['/store/admin/new']" >
New New
</button> </button>
@@ -1,84 +1,39 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Account } from "../models/Account"; import { Account } from "../models/Account";
import { BehaviorSubject, Observable } from "rxjs"; import { BehaviorSubject, Observable } from "rxjs";
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http"; import { HttpClient } 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 => {
if (data.error.length === 0){
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); console.log("No login state found: ", err.error);
} }
}); });
return sub; return sub;
} }
Logout(){ Logout(){
this.http.post<Account>( "api/account/logout", {}, { responseType: 'json' } ).subscribe( );
this._user.next( new Account ); this._user.next( new Account );
this.delUserFromStorage(); return this.http.post<Account>( "api/account/logout", {}, { responseType: 'json' } );
} }
get isLoggedIn(): boolean { get isLoggedIn(): boolean {
return this._user.value.id != -1 ? true : false; return this._user.value.id != null ? true : false;
} }
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,284 +1,48 @@
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using MistoxWebsite.Server.Services;
using MistoxWebsite.Server.Services.DatabaseService; using MistoxWebsite.Server.Services.DatabaseService;
using MistoxWebsite.Server.Entities; using MistoxWebsite.Server.Entities;
using System.Text.Json;
using System.Text;
namespace MistoxWebsite.Server.Controllers { namespace MistoxWebsite.Server.Controllers {
[ApiController] [ApiController]
[Route("api/account/[controller]")] [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 new Account() { Error = "Too many failed password attempts. Please reset your password" };
}
}
if (BCrypt.Net.BCrypt.Verify(PasswordHash, test.PasswordHash)) {
test.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(test);
List<Claim> claims = new List<Claim>() {
new Claim("ID", test.ID.ToString())
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(new ClaimsIdentity(claims, "Auth")),
new AuthenticationProperties {
ExpiresUtc = DateTime.UtcNow.AddYears(30), // Add 30 years with sliding on
IsPersistent = StayLoggedIn, // Is set from the StayLoggedIn
}
);
return test;
}
else {
test.CurrentPasswordAttempts += 1;
await _databaseService.SetAccount(test);
return new Account() { Error = "Wrong password" };
}
}
else {
await SendVerify(test.UserName);
return new Account() { Error = "A new verify email has been sent. \n Note only 1 email send every 5 mintes" };
}
}
return new Account() { Error = "User doesn't exist" };
} catch (Exception ex) {
return new Account() { Error = ex.Message };
}
}
[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);
created = await _databaseService.GetAccount(Email.ToLower());
if (created != null) {
await SendVerify(created.UserName);
return created;
}
return new Account() { Error = "Unknown Error" };
}
else {
return new Account() { Error = "Email is already in use" };
}
}
else {
return new Account() { Error = "UserName is taken" };
}
} catch (Exception ex) {
Console.WriteLine("Error: " + ex.Message);
return new Account() { Error = ex.Message };
}
}
[Route("changepassword")]
[HttpPost]
public async Task<ActionResult<bool>> 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);
user.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(user);
return true;
}
}
return false;
} catch {
return false;
} }
return NotFound("Not logged in");
} }
[Route("toggleaccountlock")] [HttpPost("loginticket")]
[HttpPost] public async Task<ActionResult> LoginTicket([FromBody] string LoginToken) {
public async Task<ActionResult<string>> ToggleAccountLock([FromForm] bool AccountLock) { using (HttpClient client = new HttpClient()) {
try { var payload = new { ticket = LoginToken };
if (isLoggedIn()) { StringContent jsonPayload = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
Account user = await getLoggedInUser(); HttpResponseMessage JWTResponse = await client.PostAsync("https://auth.mistox.com/api/auth/token", jsonPayload);
user.FailedPasswordLock = AccountLock; if (JWTResponse.IsSuccessStatusCode) {
user.CurrentPasswordAttempts = 0; string JWT = await JWTResponse.Content.ReadAsStringAsync();
await _databaseService.SetAccount(user); signIn(JWT);
return "Account Lock Status Updated";
}
return "Unknown Error Occurred";
} catch (Exception ex) {
return ex.Message;
}
}
[Route("get")]
[HttpPost]
public async Task<ActionResult<Account?>> Get() {
try {
if (isLoggedIn()) {
return await getLoggedInUser();
}
return Ok();
} catch {
return Ok(); return Ok();
} else {
string error = await JWTResponse.Content.ReadAsStringAsync();
return NotFound(error);
}
} }
} }
[Route("logout")] [HttpGet("logout")]
[HttpPost] public ActionResult Logout() {
public async Task Logout() {
await HttpContext.SignOutAsync();
}
[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 "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 result;
}
return "Account not found";
} catch (Exception) {
return "The connection couldn't be established to the email server";
}
}
[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 true;
}
}
return false;
} catch {
return false;
}
}
[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 "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 result;
}
return "Account Not Found";
} catch (Exception e) {
Console.WriteLine("EmailService Error: " + e.ToString());
return "The connection couldn't be established to the email server";
}
}
[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 true;
}
}
return false;
} catch {
return false;
}
}
[Route("delete")]
[HttpPost]
public async Task<ActionResult<bool>> delete([FromForm] string Password) {
try {
if (isLoggedIn()) { if (isLoggedIn()) {
Account user = await getLoggedInUser(); signOut();
if (BCrypt.Net.BCrypt.Verify(Password, user.PasswordHash)) { return Redirect("/");
await _databaseService.DeleteAccount(user.ID);
return true;
}
}
return false;
} catch {
return false;
} }
return NotFound("Not logged in");
} }
} }
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using MistoxWebsite.Server.Entities; using MistoxWebsite.Server.Entities;
using MistoxWebsite.Server.Services.DatabaseService; using MistoxWebsite.Server.Services.DatabaseService;
@@ -12,6 +13,19 @@ namespace MistoxWebsite.Server.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;
@@ -20,16 +34,19 @@ namespace MistoxWebsite.Server.Controllers {
} }
public int getLoggedInUserID() { public int getLoggedInUserID() {
return Convert.ToInt32(User.FindFirst("ID")?.Value); 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();
} }
@@ -14,7 +14,7 @@ namespace MistoxWebsite.Server.Controllers {
public async Task<ActionResult<bool>> CreateProduct([FromForm] Product obj, [FromForm] IFormFile[] images) { public async Task<ActionResult<bool>> CreateProduct([FromForm] Product obj, [FromForm] IFormFile[] images) {
try { try {
if (isLoggedIn()) { if (isLoggedIn()) {
Account user = await getLoggedInUser(); Account user = getLoggedInUser();
if (user.Role == "Admin") { if (user.Role == "Admin") {
List<ProductImage> building = new List<ProductImage>(); List<ProductImage> building = new List<ProductImage>();
foreach (var file in images) { foreach (var file in images) {
@@ -70,7 +70,7 @@ namespace MistoxWebsite.Server.Controllers {
public async Task<ActionResult<bool>> DeleteProduct([FromForm] int productID) { public async Task<ActionResult<bool>> DeleteProduct([FromForm] int productID) {
try { try {
if (isLoggedIn()) { if (isLoggedIn()) {
Account user = await getLoggedInUser(); Account user = getLoggedInUser();
if (user.Role == "Admin") { if (user.Role == "Admin") {
await _databaseService.DeleteProduct(productID); await _databaseService.DeleteProduct(productID);
return true; return true;
@@ -3,17 +3,11 @@
namespace MistoxWebsite.Server.Entities { namespace MistoxWebsite.Server.Entities {
public class Account { public class Account {
public int ID { get; set; } // PK public int? ID { get; set; } // PK
public string UserName { get; set; } = ""; public string UserName { get; set; } = "";
public string Email { 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 Role { get; set; } = "Generic";
public string EmailToken { get; set; } = ""; public string DataServer { get; set; } = "";
public string Error { get; set; } = "";
} }
public class Product { public class Product {
@@ -11,6 +11,7 @@
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.3" /> <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.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="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.3.0" />
<PackageReference Include="MySql.Data" Version="9.2.0" /> <PackageReference Include="MySql.Data" Version="9.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+94 -14
View File
@@ -1,4 +1,10 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using MistoxWebsite.Server.Controllers.Payment; using MistoxWebsite.Server.Controllers.Payment;
using MistoxWebsite.Server.Services; using MistoxWebsite.Server.Services;
using MistoxWebsite.Server.Services.DatabaseService; using MistoxWebsite.Server.Services.DatabaseService;
@@ -78,25 +84,97 @@ 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()) {
while (PublicKey == null) {
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);
} else {
await Task.Delay(2000); // sleep the main thread for 2 seconds before sending another request. Prevent DDOS of my own equiptment
}
}
}
builder.Services.AddAuthentication(options => { builder.Services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
} ).AddCookie(options => { options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.Cookie.HttpOnly = true; }).AddJwtBearer(options => {
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.TokenValidationParameters = new TokenValidationParameters {
options.Cookie.SameSite = SameSiteMode.Strict; ValidateIssuer = true,
options.LoginPath = "/account/login"; ValidateAudience = true,
options.LogoutPath = "/account/logout"; ValidateLifetime = true,
options.SlidingExpiration = true; ValidateIssuerSigningKey = true,
ValidIssuer = "https://auth.mistox.com",
ValidAudience = "mistox-llc-auth-token",
IssuerSigningKey = PublicKey,
ClockSkew = TimeSpan.FromMinutes(1)
};
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
context.Token = context.Request.Cookies["mistox_session"];
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)) {
// Impliment token refresh
}
}
return Task.CompletedTask;
}
};
}); });
builder.Services.AddCors( o => o.AddDefaultPolicy( builder => { ////////////////////////////////
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); // No CORS /// Rate Limiting Service ////
} ) ); ////////////////////////////////
List<string> allowedOrigins = new List<string>{ "https://mistox.com", "https://www.mistox.com" };
if (builder.Environment.IsDevelopment()) {
allowedOrigins.Add("http://localhost:5000");
}
builder.Services.AddCors(options => {
options.AddDefaultPolicy(policy => {
policy.WithOrigins(allowedOrigins.ToArray())
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
builder.Services.AddRateLimiter(options => {
options.AddPolicy("PerUserPolicy", httpContext => {
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? $"ip:{httpContext.Connection.RemoteIpAddress}";
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
});
});
});
////////////////////////////////
///// ASPNET Core Function /////
////////////////////////////////
// Pages Service
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddRazorPages();
var app = builder.Build(); var app = builder.Build();
@@ -108,6 +186,8 @@ if( !app.Environment.IsDevelopment() ) {
app.UseDefaultFiles(); app.UseDefaultFiles();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseRateLimiter();
app.UseCors(); app.UseCors();
app.UseRouting(); app.UseRouting();
@@ -1,160 +0,0 @@
using MistoxWebsite.Server.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
namespace MistoxWebsite.Server.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" );
account = new Account() {
ID = _id,
UserName = _username,
Email = _email,
EmailVerified = _emailVerified,
PasswordHash = _passwordhash,
CurrentPasswordAttempts = _curpasswordattempts,
PasswordAttempts = _passwordattempts,
EmailToken = _emailtoken,
FailedPasswordLock = _failedpasswordlock,
Role = _role,
};
}
}
}
return account;
}
public async Task<Account?> GetAccount( int ID ) {
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", ID);
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" );
account = new Account() {
ID = _id,
UserName = _username,
Email = _email,
EmailVerified = _emailVerified,
PasswordHash = _passwordhash,
CurrentPasswordAttempts = _passwordattempts,
PasswordAttempts = _passwordattempts,
EmailToken = _emailtoken,
FailedPasswordLock = _failedpasswordlock,
Role = _role,
};
}
}
}
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)
VALUES
(@ID,@UserName,@Email,@EmailVerified,@PasswordHash,@FailedPasswordLock,@PasswordAttempts,@CurrentPasswordAttempts,@Role,@EmailToken);
ON DUPLICATE KEY UPDATE
UserName = @UserName,
Email = @Email,
EmailVerified = @EmailVerified,
PasswordHash = @PasswordHash,
FailedPasswordLock = @FailedPasswordLock,
PasswordAttempts = @PasswordAttempts,
CurrentPasswordAttempts = @CurrentPasswordAttempts,
Role = @Role,
EmailToken = @EmailToken;
";
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);
await cmd.ExecuteNonQueryAsync();
}
}
public async Task DeleteAccount( int ID ) {
using( MySqlConnection connection = GetConnection() ) {
MySqlCommand cmd;
connection.Open();
string command = @"
DELETE FROM Account WHERE ID = @ID;
DELETE FROM AccountInventory WHERE AccountID = @ID;
DELETE FROM ProjectMistData WHERE AccountID = @ID;
DELETE FROM Cart WHERE AccountID = @ID;
";
cmd = new MySqlCommand( command, connection );
cmd.Parameters.AddWithValue("@ID", ID);
await cmd.ExecuteNonQueryAsync();
}
}
}
}