Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10868018fb | |||
| 8ce7df46c2 | |||
| f4aa59f2df | |||
| ea542ca0a7 | |||
| 3b169f18d9 | |||
| b8634dbc87 | |||
| d33edd41cf | |||
| f9cfd204bf | |||
| d5e7b8b95d | |||
| 9f24f8453e | |||
| d385f21f43 | |||
| 75fb6592f6 | |||
| 34e4328050 | |||
| bd4b4bc837 | |||
| 18b58b9b5d |
@@ -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
|
||||||
@@ -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 \
|
||||||
|
.
|
||||||
@@ -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
|
|
||||||
Vendored
+1
@@ -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
@@ -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 ##
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 },
|
|
||||||
]
|
]
|
||||||
@@ -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;
|
||||||
|
|||||||
-23
@@ -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>
|
|
||||||
-55
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -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>
|
|
||||||
-69
@@ -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@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" />
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user