This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.cs]
|
||||
csharp_new_line_before_open_brace = none
|
||||
csharp_new_line_before_catch = false
|
||||
csharp_new_line_before_finally = false
|
||||
csharp_new_line_after_else = false
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
####################
|
||||
## Authentication ##
|
||||
####################
|
||||
|
||||
# Random secret token for encrypting JWT contents [ Has to match web clients ]
|
||||
JWT_Secret=
|
||||
|
||||
|
||||
|
||||
##############
|
||||
## Database ##
|
||||
##############
|
||||
|
||||
MySQL_User=root
|
||||
MySQL_Pass=oasv34$8gpv023dd
|
||||
|
||||
|
||||
|
||||
##############
|
||||
## Email ##
|
||||
##############
|
||||
|
||||
# Hostname of email server
|
||||
Email_Server=
|
||||
|
||||
# SMTP port used
|
||||
Email_Port=
|
||||
|
||||
# Email Address to send from
|
||||
Email_Address=
|
||||
|
||||
# Password for the email address
|
||||
Email_Password=
|
||||
@@ -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/boredcareers-sql \
|
||||
--push \
|
||||
./database
|
||||
|
||||
- name: build and push server
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--build-arg BASE_URL=https://boredcareers.com \
|
||||
-t docker.mistox.net/boredcareers-website \
|
||||
--push \
|
||||
.
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
/resources
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.angular
|
||||
|
||||
# DotNet
|
||||
**/bin
|
||||
**/obj
|
||||
/debug
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.env
|
||||
data
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch ASP.NET Core backend",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build-all",
|
||||
"program": "Server.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/debug/",
|
||||
"stopAtEntry": false,
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||
},
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"PaymentService": "StripeIntent"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Views"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "server-build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/src/Server/Server.csproj",
|
||||
"-o",
|
||||
"${workspaceFolder}/debug/",
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "client-build",
|
||||
"command": "ng",
|
||||
"type": "process",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/Client"
|
||||
},
|
||||
"args": [
|
||||
"build",
|
||||
"--base-href=http://localhost:5000"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "client-packages",
|
||||
"command": "npm",
|
||||
"type": "process",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/Client"
|
||||
},
|
||||
"args": [
|
||||
"install"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "build-all",
|
||||
"dependsOn": ["client-packages", "client-build", "server-build" ],
|
||||
"dependsOrder": "sequence"
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
######################
|
||||
## Build Frontend ##
|
||||
######################
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:alpine AS build-frontend
|
||||
WORKDIR /src
|
||||
|
||||
# Define base address
|
||||
ARG BASE_URL=/
|
||||
|
||||
# Install the angular CLI
|
||||
RUN npm install -g @angular/cli
|
||||
|
||||
# Copy the package.json into this build step
|
||||
COPY ./src/Client/package.json ./
|
||||
|
||||
# Pull dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the frontend over
|
||||
COPY ./src/Client/ ./
|
||||
|
||||
# Compile the source
|
||||
RUN ng build --base-href=${BASE_URL}
|
||||
|
||||
#####################
|
||||
## Build Backend ##
|
||||
#####################
|
||||
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build-backend
|
||||
WORKDIR /src
|
||||
|
||||
# Copy the csproj
|
||||
COPY ./src/Server/Server.csproj ./
|
||||
|
||||
# Restore the Server
|
||||
RUN dotnet restore './Server.csproj'
|
||||
|
||||
# Copy the rest of the backend over
|
||||
COPY ./src/Server/ ./
|
||||
|
||||
# Get the target arch
|
||||
ARG TARGETARCH
|
||||
|
||||
# Build the source
|
||||
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 './Server.csproj' -c Release -r ${RID} -o /app/publish
|
||||
|
||||
################
|
||||
## Publish ##
|
||||
################
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
||||
WORKDIR /app
|
||||
|
||||
ENV ASPNETCORE_HTTP_PORTS=5000
|
||||
ENV StripeKey=null
|
||||
ENV MySQLServer=null
|
||||
ENV MySQLUser=null
|
||||
ENV MySQLPass=null
|
||||
ENV MySQLDatabase=Mistox
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
# Copy in the server
|
||||
COPY --from=build-backend /app/publish ./
|
||||
|
||||
# Copy in the client
|
||||
COPY --from=build-frontend /debug/wwwroot ./wwwroot/
|
||||
|
||||
ENTRYPOINT ["dotnet", "MistoxWebsite.Server.dll", "--url", "http://localhost:5000"]
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
FROM mysql
|
||||
|
||||
ENV MYSQL_DATABASE=Auth
|
||||
ENV MYSQL_ROOT_PASSWORD=90pa8pav89h4g08hads
|
||||
|
||||
ADD mistox.sql /docker-entrypoint-initdb.d
|
||||
|
||||
EXPOSE 3306
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
CREATE DATABASE IF NOT EXISTS `Auth`;
|
||||
USE `Auth`;
|
||||
|
||||
-- Account Section
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `Account` (
|
||||
`ID` int NOT NULL AUTO_INCREMENT,
|
||||
`UserName` varchar(60) NOT NULL,
|
||||
`Email` varchar(255) NOT NULL,
|
||||
`EmailVerified` boolean DEFAULT 0,
|
||||
`PasswordHash` char(60) DEFAULT NULL,
|
||||
`FailedPasswordLock` boolean DEFAULT 0,
|
||||
`PasswordAttempts` int(11) DEFAULT NULL,
|
||||
`CurrentPasswordAttempts` int(11) DEFAULT NULL,
|
||||
`Role` varchar(45) DEFAULT NULL,
|
||||
`EmailToken` varchar(45) DEFAULT NULL,
|
||||
`DataServer` varchar(200) DEFAULT NULL,
|
||||
UNIQUE(`Email`),
|
||||
UNIQUE(`UserName`),
|
||||
PRIMARY KEY (`ID`)
|
||||
) AUTO_INCREMENT=1;
|
||||
|
||||
-- Default Account
|
||||
|
||||
INSERT INTO Account (
|
||||
ID,
|
||||
UserName,
|
||||
Email,
|
||||
EmailVerified,
|
||||
PasswordHash,
|
||||
FailedPasswordLock,
|
||||
PasswordAttempts,
|
||||
CurrentPasswordAttempts,
|
||||
Role,
|
||||
EmailToken,
|
||||
DataServer
|
||||
) VALUES (
|
||||
1,
|
||||
'admin',
|
||||
'admin@mistox.com',
|
||||
1,
|
||||
'$2a$11$0UeWLLqTXe3FG161QVuI0OQJ9rulspUpMG581DI6KSzDXBbFKd00S',
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
'Admin',
|
||||
'',
|
||||
''
|
||||
);
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
|
||||
auth-server:
|
||||
container_name: auth_server
|
||||
image: docker.mistox.net/boredcareers-website:latest
|
||||
restart: always
|
||||
environment:
|
||||
- MySQLServer=auth-database
|
||||
- MySQLDatabase=Auth
|
||||
- MySQLUser=${MySQL_User}
|
||||
- MySQLPass=${MySQL_Pass}
|
||||
- EmailServer=${Email_Server}
|
||||
- EmailPort=${Email_Port}
|
||||
- EmailAddress=${Email_Address}
|
||||
- EmailPassword=${Email_Password}
|
||||
- JWTsecret=${JWT_Secret}
|
||||
ports:
|
||||
- 5000:5000
|
||||
depends_on:
|
||||
- boredcareers-database
|
||||
|
||||
auth-database:
|
||||
container_name: auth_database
|
||||
image: docker.mistox.net/boredcareers-sql:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- ./data:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MySQL_Pass}
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"Mistox-Frontend": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": ["zone.js" ],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"aot": true,
|
||||
"outputMode": "static",
|
||||
"outputPath": {
|
||||
"base": "../../debug/wwwroot",
|
||||
"browser": ""
|
||||
},
|
||||
"deleteOutputPath": false
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "Mistox-Frontend:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "Mistox-Frontend:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
Generated
+8931
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "mistox-frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^20.0.0",
|
||||
"@angular/compiler": "^20.0.0",
|
||||
"@angular/core": "^20.0.0",
|
||||
"@angular/forms": "^20.0.0",
|
||||
"@angular/platform-browser": "^20.0.0",
|
||||
"@angular/router": "^20.0.0",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^20.0.2",
|
||||
"@angular/cli": "^20.0.2",
|
||||
"@angular/compiler-cli": "^20.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.7.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { routes } from './app.routes';
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(withInterceptorsFromDi())
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
.top-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: linear-gradient(0deg,#99999988, #000000FF);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.top-bar-logo {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
margin: 10px
|
||||
}
|
||||
|
||||
.top-bar-buttons {
|
||||
display: flex;
|
||||
width: calc(50% - 110px);
|
||||
height: 180px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.flex-right {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
height: 15px;
|
||||
width: 150px;
|
||||
border-radius: 5px;
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
transition: 0.5s;
|
||||
background-color: #00000000;
|
||||
border: 1px solid var(--Mistox-White);
|
||||
color: var(--Mistox-White);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background-color: #00000044;
|
||||
color: var(--Mistox-Light);
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #00000010;
|
||||
color: var(--Mistox-Frame);
|
||||
}
|
||||
|
||||
.nav-button-login {
|
||||
width: 100px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
min-height: calc(100vh - 300px);
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background: linear-gradient(180deg,#99999988, #000000FF);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.bottom-bar-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 10px 50px;
|
||||
}
|
||||
|
||||
.bottom-bar-logo img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.bottom-bar-buttons {
|
||||
display: flex;
|
||||
width: calc(50% - 110px);
|
||||
height: 80px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.nav-button-bottom {
|
||||
height: 15px;
|
||||
width: 150px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
transition: 0.5s;
|
||||
background-color: #00000000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.bottom-bar-float {
|
||||
align-items: end;
|
||||
color: var(--Mistox-White);
|
||||
}
|
||||
|
||||
.bottom-bar-padding {
|
||||
color: var(--Mistox-White);
|
||||
margin: 20px;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="top-bar">
|
||||
<div class="top-bar-buttons">
|
||||
<a #homeLink class="nav-button" routerLink="">HOME</a>
|
||||
<a #jobsLink class="nav-button" routerLink="/jobs">JOB BOARD</a>
|
||||
<a #resumesLink class="nav-button" routerLink="/resumes">RESUMES</a>
|
||||
</div>
|
||||
<a class="top-bar-logo" routerLink="">
|
||||
<img class="top-bar-logo" style="margin: 0;" src="img/logo-full.png" />
|
||||
</a>
|
||||
<div *ngIf="auth.isLoggedIn" class="top-bar-buttons flex-right">
|
||||
<a class="nav-button nav-button-login" routerLink="/account/settings"><span>{{ auth.loggedInUser.userName.toUpperCase() }}</span></a>
|
||||
<a class="nav-button nav-button-login" routerLink="/account/logout"><span>LOGOUT</span></a>
|
||||
</div>
|
||||
<div *ngIf="!auth.isLoggedIn" class="top-bar-buttons flex-right">
|
||||
<a class="nav-button nav-button-login" routerLink="/account/login"><span>LOGIN</span></a>
|
||||
<a class="nav-button nav-button-login" routerLink="/account/register"><span>REGISTER</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<div class="bottom-bar">
|
||||
<div class="bottom-bar-buttons bottom-bar-float">
|
||||
<a class="nav-button-bottom bottom-bar-padding" routerLink="/contact">CONTACT</a>
|
||||
<a class="nav-button-bottom bottom-bar-padding" routerLink="/privacy">PRIVACY</a>
|
||||
<a class="nav-button-bottom bottom-bar-padding" routerLink="/about">ABOUT</a>
|
||||
</div>
|
||||
<a class="bottom-bar-logo" href="https://mistox.com">
|
||||
<img src="img/mistox-logo.png" />
|
||||
</a>
|
||||
<div class="bottom-bar-buttons flex-right bottom-bar-float">
|
||||
<div class="bottom-bar-padding">
|
||||
<span>Mistox LLC</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { ForgotPasswordComponent } from './pages/account/forgotpassword/forgotpassword.component';
|
||||
import { LoginComponent } from './pages/account/login/login.component';
|
||||
import { RegisterComponent } from './pages/account/register/register.component';
|
||||
import { SettingsComponent } from './pages/account/settings/settings.component';
|
||||
import { LogoutComponent } from './pages/account/logout/logout.component';
|
||||
import { ResetPasswordComponent } from './pages/account/resetpassword/resetpassword.component';
|
||||
import { VerifyEmailComponent } from './pages/account/verifyemail/verifyemail.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
|
||||
{ path: "account/forgotpassword", component: ForgotPasswordComponent },
|
||||
{ path: "account/resetpassword", component: ResetPasswordComponent },
|
||||
{ path: "account/verifyemail", component: VerifyEmailComponent },
|
||||
{ path: "account/login", component: LoginComponent },
|
||||
{ path: "account/logout", component: LogoutComponent },
|
||||
{ path: "account/register", component: RegisterComponent },
|
||||
{ path: "account/settings", component: SettingsComponent },
|
||||
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Router, RouterModule, RouterOutlet } from '@angular/router';
|
||||
import { Authentication } from './services/Authentication';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet, CommonModule, RouterModule],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.css'
|
||||
})
|
||||
export class App {
|
||||
|
||||
@ViewChild('homeLink') homeLink!: ElementRef<HTMLAnchorElement>;
|
||||
@ViewChild('jobsLink') jobLink!: ElementRef<HTMLAnchorElement>;
|
||||
@ViewChild('resumesLink') resumeLink!: ElementRef<HTMLAnchorElement>;
|
||||
|
||||
constructor(public auth: Authentication, private router: Router){}
|
||||
|
||||
ngAfterViewInit(){
|
||||
let ViewLinks = [ this.homeLink, this.resumeLink, this.jobLink ];
|
||||
ViewLinks.forEach(link => {
|
||||
if (new URL(link.nativeElement.href).pathname === new URL(window.location.href).pathname){
|
||||
link.nativeElement.classList.add("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export class Account {
|
||||
public id: number = 0;
|
||||
public userName: string = "";
|
||||
public email: string = "";
|
||||
public emailVerified: boolean = false;
|
||||
public passwordHash: string = "";
|
||||
public failedPasswordLock: boolean = false;
|
||||
public passwordAttempts: number = 5;
|
||||
public currentPasswordAttempts: number = 0;
|
||||
public role: string = "Generic";
|
||||
public emailToken: string = "";
|
||||
public dataServer: string = "";
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="center">
|
||||
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
|
||||
|
||||
<h3>Forgot Password</h3>
|
||||
|
||||
<div class="frame-item">
|
||||
<input type="text" [(ngModel)]="email" name="email" placeholder=" " />
|
||||
<label>Email</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex-row">
|
||||
<div class="frame-button">
|
||||
<input class="submit" type="submit" value="Send Code" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul *ngIf="errorMsgs.length > 0" >
|
||||
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'account-forgot',
|
||||
templateUrl: './forgotpassword.component.html',
|
||||
imports: [ FormsModule, CommonModule ]
|
||||
})
|
||||
export class ForgotPasswordComponent {
|
||||
email: string = "";
|
||||
errorMsgs: string[] = [];
|
||||
returnURL: string = '/';
|
||||
|
||||
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title) {
|
||||
this.title.setTitle("Forgot Password | Mistox");
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.returnURL = params['returnURL'] || '/';
|
||||
});
|
||||
}
|
||||
|
||||
sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
// Clear errors
|
||||
this.errorMsgs = [];
|
||||
|
||||
// Send to server and wait for response
|
||||
this.errorMsgs.push("Waiting for response from server");
|
||||
const body = new HttpParams()
|
||||
.set("Email", this.email)
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
});
|
||||
this.http.post( "api/account/sendresetpassword", body, { headers, responseType: "text" } ).subscribe({
|
||||
next: async (data) => {
|
||||
if (data.trim() == "Success"){
|
||||
this.errorMsgs = ["Reset-password sent"];
|
||||
await this.sleep(3000);
|
||||
this.router.navigate([this.returnURL]);
|
||||
}else{
|
||||
this.errorMsgs = [data];
|
||||
}
|
||||
},
|
||||
error: err => {
|
||||
console.log("HTTP Error Signing In: ", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="center">
|
||||
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
|
||||
<h3>Login</h3>
|
||||
|
||||
<div class="frame-item">
|
||||
<input type="text" [(ngModel)]="UserName" name="userName" placeholder=" " autocomplete="username" />
|
||||
<label>UserName</label>
|
||||
</div>
|
||||
|
||||
<div class="frame-item">
|
||||
<input type="password" [(ngModel)]="Password" name="password" placeholder=" " autocomplete="current-password" />
|
||||
<label>Password</label>
|
||||
</div>
|
||||
|
||||
<div class="flex-row">
|
||||
<div class="frame-button">
|
||||
<input class="submit" type="submit" value="LOGIN" />
|
||||
</div>
|
||||
|
||||
<div class="frame-forgot">
|
||||
<div class="sub-frame">
|
||||
Stay Logged In
|
||||
<input type="checkbox" [(ngModel)]="StayLoggedIn" name="stayLoggedIn" />
|
||||
</div>
|
||||
<div class="sub-frame">
|
||||
<a href="/account/forgotpassword">Forgot Password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul *ngIf="errorMsgs.length > 0" >
|
||||
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Authentication, SessionType } from '../../../services/Authentication';
|
||||
|
||||
@Component({
|
||||
selector: 'account-login',
|
||||
templateUrl: './login.component.html',
|
||||
imports: [ FormsModule, CommonModule ],
|
||||
standalone: true
|
||||
})
|
||||
export class LoginComponent {
|
||||
UserName: string = "";
|
||||
Password: string = "";
|
||||
StayLoggedIn: boolean = false;
|
||||
errorMsgs: string[] = [];
|
||||
returnURL: string = '/';
|
||||
|
||||
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
|
||||
this.title.setTitle("Login | Mistox");
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.returnURL = params['returnURL'] || '/';
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.errorMsgs = [];
|
||||
|
||||
if (!this.UserName) {
|
||||
this.errorMsgs.push("The 'username' field is required");
|
||||
}
|
||||
if (!this.Password) {
|
||||
this.errorMsgs.push("The 'password' field is required");
|
||||
}
|
||||
if (this.Password.length < 6) {
|
||||
this.errorMsgs.push("Password must be at least 6 Characters long");
|
||||
}
|
||||
if (this.errorMsgs.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.errorMsgs.push("Waiting for response from server");
|
||||
this.auth.Login(this.UserName, this.Password, this.StayLoggedIn).subscribe({
|
||||
next: data => {
|
||||
this.router.navigate([this.returnURL]);
|
||||
},
|
||||
error: err => {
|
||||
this.errorMsgs = [ err.error ];
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Authentication } from '../../../services/Authentication';
|
||||
|
||||
@Component({
|
||||
selector: 'account-logout',
|
||||
templateUrl: './logout.component.html',
|
||||
imports: [ FormsModule, CommonModule ],
|
||||
standalone: true
|
||||
})
|
||||
export class LogoutComponent {
|
||||
errorMsgs: string[] = [];
|
||||
returnURL: string = '/';
|
||||
|
||||
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
|
||||
this.title.setTitle("Logout | Mistox");
|
||||
}
|
||||
|
||||
ngAfterViewInit(){
|
||||
this.auth.Logout().subscribe({
|
||||
next: data => {
|
||||
window.location.href = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="center">
|
||||
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
|
||||
<h3>Register</h3>
|
||||
|
||||
<div class="frame-item">
|
||||
<input type="text" [(ngModel)]="userName" name="userName" placeholder=" " autocomplete="username" />
|
||||
<label>UserName</label>
|
||||
</div>
|
||||
|
||||
<div class="frame-item">
|
||||
<input type="email" [(ngModel)]="email" name="email" placeholder=" " autocomplete="current-password" />
|
||||
<label>Email</label>
|
||||
</div>
|
||||
|
||||
<div class="frame-item">
|
||||
<input type="password" [(ngModel)]="passwordHash" name="password" placeholder=" " autocomplete="current-password" />
|
||||
<label>Password</label>
|
||||
</div>
|
||||
|
||||
<div class="frame-item">
|
||||
<input type="password" [(ngModel)]="passwordHash2" name="repeat password" placeholder=" " autocomplete="current-password" />
|
||||
<label>Repeat Password</label>
|
||||
</div>
|
||||
|
||||
<div class="flex-row">
|
||||
<div class="frame-button">
|
||||
<input class="submit" type="submit" value="REGISTER" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul *ngIf="errorMsgs.length > 0" >
|
||||
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Account } from '../../../models/Account';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'account-register',
|
||||
templateUrl: './register.component.html',
|
||||
imports: [ FormsModule, CommonModule ]
|
||||
})
|
||||
export class RegisterComponent {
|
||||
userName: string = ""
|
||||
email: string = "";
|
||||
passwordHash: string = "";
|
||||
passwordHash2: string = "";
|
||||
error: string = "";
|
||||
|
||||
errorMsgs: string[] = [];
|
||||
returnURL: string = '/';
|
||||
|
||||
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
|
||||
this.title.setTitle("Register | Mistox");
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.returnURL = params['returnURL'] || '/';
|
||||
});
|
||||
}
|
||||
|
||||
sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
// Clear errors
|
||||
this.errorMsgs = [];
|
||||
|
||||
// Validate data
|
||||
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
if (!regex.test(this.email)){
|
||||
this.errorMsgs.push("A valid email is required");
|
||||
}
|
||||
if (!this.userName) {
|
||||
this.errorMsgs.push("The 'username' field is required");
|
||||
}
|
||||
if (!this.passwordHash) {
|
||||
this.errorMsgs.push("The 'password' field is required");
|
||||
}
|
||||
if (this.passwordHash.length < 6) {
|
||||
this.errorMsgs.push("Password must be at least 6 Characters long");
|
||||
}
|
||||
if (this.passwordHash !== this.passwordHash2){
|
||||
this.errorMsgs.push("Passwords don't match");
|
||||
}
|
||||
if (this.errorMsgs.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send to server and wait for response
|
||||
this.errorMsgs.push("Waiting for response from server");
|
||||
const body = new HttpParams()
|
||||
.set("Email", this.email)
|
||||
.set("UserName", this.userName)
|
||||
.set("PasswordHash", this.passwordHash);
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
});
|
||||
this.http.post<Account>( "api/account/register", body, { headers } ).subscribe({
|
||||
next: async (data) => {
|
||||
this.errorMsgs = ["Account Created"];
|
||||
await this.sleep(3000);
|
||||
this.router.navigate([this.returnURL]);
|
||||
},
|
||||
error: err => {
|
||||
this.errorMsgs = [ err.error ]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="center">
|
||||
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
|
||||
|
||||
<h3>Reset Password</h3>
|
||||
<h2>User: {{ UserName }}</h2>
|
||||
|
||||
<div class="frame-item">
|
||||
<input type="password" [(ngModel)]="Password" name="Password" placeholder=" " />
|
||||
<label>New Password</label>
|
||||
</div>
|
||||
|
||||
<div class="frame-item">
|
||||
<input type="password" [(ngModel)]="PassworR" name="PassworR" placeholder=" " />
|
||||
<label>Repeat New Password</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex-row">
|
||||
<div class="frame-button">
|
||||
<input class="submit" type="submit" value="Send Code" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul *ngIf="errorMsgs.length > 0" >
|
||||
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'account-reset',
|
||||
templateUrl: './resetpassword.component.html',
|
||||
imports: [ FormsModule, CommonModule ]
|
||||
})
|
||||
export class ResetPasswordComponent {
|
||||
|
||||
UserName: string = "";
|
||||
ResetPwd: string = "";
|
||||
Password: string = "";
|
||||
PassworR: string = "";
|
||||
|
||||
errorMsgs: string[] = [];
|
||||
|
||||
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
|
||||
this.title.setTitle("Reset Password | Mistox");
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.UserName = params['UserName'] || '';
|
||||
this.ResetPwd = params['ResetPwd'] || '';
|
||||
});
|
||||
}
|
||||
|
||||
sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.Password != this.PassworR){
|
||||
this.errorMsgs.push("Passwords must match");
|
||||
}
|
||||
if (this.Password.length < 6){
|
||||
this.errorMsgs.push("Password must be at least 6 Characters long");
|
||||
}
|
||||
if (this.errorMsgs.length == 0){
|
||||
// Send to server and wait for response
|
||||
this.errorMsgs.push("Waiting for response from server");
|
||||
const body = new HttpParams()
|
||||
.set("UserName", this.UserName)
|
||||
.set("NewPassword", this.Password)
|
||||
.set("ResetToken", this.ResetPwd);
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
});
|
||||
this.http.post<boolean>( "api/account/resetpassword", body, { headers } ).subscribe({
|
||||
next: async (data) => {
|
||||
if (data == true){
|
||||
this.errorMsgs = ["Password reset successfully"];
|
||||
await this.sleep(3000);
|
||||
this.router.navigate(["/account/login"]);
|
||||
}else{
|
||||
this.errorMsgs = ["An error has ocurred"];
|
||||
await this.sleep(3000);
|
||||
this.router.navigate(["/account/sendresetpassword"]);
|
||||
}
|
||||
},
|
||||
error: err => {
|
||||
console.log("HTTP Error Signing In: ", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Account } from '../../../models/Account';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'account-settings',
|
||||
templateUrl: './settings.component.html',
|
||||
imports: [ FormsModule, CommonModule ]
|
||||
})
|
||||
export class SettingsComponent {
|
||||
user!: Account;
|
||||
errorMsgs: string[] = [];
|
||||
returnURL: string = '/';
|
||||
|
||||
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
|
||||
this.title.setTitle("Settings | Mistox");
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.returnURL = params['returnURL'] || '/';
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="center">
|
||||
<form class="big-frame background-border" #accountForm="ngForm" (ngSubmit)="onSubmit()">
|
||||
|
||||
<h3>Verifying Email</h3>
|
||||
<h3 style="color: red;">{{ Result }}</h3>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'account-verifyemail',
|
||||
templateUrl: './verifyemail.component.html',
|
||||
imports: [ FormsModule, CommonModule ]
|
||||
})
|
||||
export class VerifyEmailComponent {
|
||||
|
||||
UserName: string = "";
|
||||
Guid: string = "";
|
||||
Result: string = "";
|
||||
|
||||
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
|
||||
this.title.setTitle("Verify Email | Mistox");
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.UserName = params['UserName'] || '';
|
||||
this.Guid = params['Guid'] || '';
|
||||
});
|
||||
}
|
||||
|
||||
sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
// Send to server and wait for response
|
||||
const body = new HttpParams()
|
||||
.set("UserName", this.UserName)
|
||||
.set("EmailToken", this.Guid);
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
});
|
||||
this.http.post<boolean>( "api/account/verifyemail", body, { headers } ).subscribe({
|
||||
next: async (data) => {
|
||||
if (data == true){
|
||||
this.Result = "Verified Email Successfully";
|
||||
}else{
|
||||
this.Result = "Email was not able to be verified please resend email";
|
||||
}
|
||||
await this.sleep(3000);
|
||||
this.router.navigate(["/"]);
|
||||
},
|
||||
error: err => {
|
||||
console.log("HTTP Error Signing In: ", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Account } from "../models/Account";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class Authentication{
|
||||
|
||||
private _user = new BehaviorSubject<Account>(this.getUserFromStorage());
|
||||
user$ = this._user.asObservable();
|
||||
|
||||
constructor( private http: HttpClient){ }
|
||||
|
||||
Login(UserName: string, Password: string, StayLoggedIn: boolean): Observable<Account> {
|
||||
|
||||
const body = new HttpParams()
|
||||
.set("UserName", UserName)
|
||||
.set("PasswordHash", Password)
|
||||
.set("StayLoggedIn", StayLoggedIn );
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
});
|
||||
|
||||
let sub = this.http.post<Account>( "api/account/login", body, { headers } );
|
||||
sub.subscribe({
|
||||
next: data => {
|
||||
data.passwordHash = "";
|
||||
this._user.next(data);
|
||||
this.setUserToStorage(data, StayLoggedIn == true ? SessionType.Forever : SessionType.Session);
|
||||
},
|
||||
error: err => {
|
||||
console.log("HTTP Error Signing In: ", err.error);
|
||||
}
|
||||
});
|
||||
return sub;
|
||||
}
|
||||
|
||||
Logout(){
|
||||
this._user.next( new Account );
|
||||
this.delUserFromStorage();
|
||||
return this.http.post<Account>( "api/account/logout", {}, { responseType: 'json' } );
|
||||
}
|
||||
|
||||
get isLoggedIn(): boolean {
|
||||
return this._user.value.id != -1 ? true : false;
|
||||
}
|
||||
|
||||
get loggedInUser(): Account {
|
||||
return this._user.value;
|
||||
}
|
||||
|
||||
private getUserFromStorage(): Account {
|
||||
const foreverUser = localStorage.getItem('user');
|
||||
const sessionUser = sessionStorage.getItem('user');
|
||||
let user = null;
|
||||
if (foreverUser != null){
|
||||
user = JSON.parse(foreverUser)
|
||||
} else if (sessionUser != null){
|
||||
user = JSON.parse(sessionUser)
|
||||
} else {
|
||||
user = new Account();
|
||||
user.id = -1;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
private setUserToStorage(user: Account, session: SessionType): void {
|
||||
if (session == SessionType.Forever){
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}else if(session == SessionType.Session){
|
||||
sessionStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
}
|
||||
private delUserFromStorage(): void {
|
||||
localStorage.removeItem('user');
|
||||
sessionStorage.removeItem('user');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export enum SessionType {
|
||||
Forever,
|
||||
Session
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Auth | Mistox</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="img/logo-full.png">
|
||||
</head>
|
||||
<body style="border: 0; margin: 0; padding: 0;">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||
@@ -0,0 +1,23 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
:root {
|
||||
--Mistox-Dark: #2C0703;
|
||||
--Mistox-Medium: #890620;
|
||||
--Mistox-Light: #B6465F;
|
||||
--Mistox-Bright: #FC440F;
|
||||
--Mistox-Frame: #FF5A00CC;
|
||||
--Mistox-Button: #ff9999;
|
||||
--Mistox-Button-Hover: #ff999977;
|
||||
--Mistox-White: #FFF;
|
||||
--Mistox-Black: #000;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
body {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cg fill-rule='evenodd'%3E%3Cg id='hexagons' fill='%23999999' fill-opacity='0.2' fill-rule='nonzero'%3E%3Cpath d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
background-color: #fff;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve",
|
||||
"baseUrl": "src",
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Auth.Services;
|
||||
using Auth.Services.DatabaseService;
|
||||
using Auth.Entities;
|
||||
using System.Web.Http;
|
||||
|
||||
namespace Auth.Controllers {
|
||||
[ApiController]
|
||||
[Route("api/account/")]
|
||||
public class AuthenticationController : MistoxControllerBase {
|
||||
|
||||
EmailService _emailContext;
|
||||
|
||||
public AuthenticationController(DatabaseService db, EmailService emailContext) : base(db) {
|
||||
_emailContext = emailContext;
|
||||
}
|
||||
|
||||
[Route("login")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Account>> Login([FromForm] string UserName, [FromForm] string PasswordHash, [FromForm] bool StayLoggedIn) {
|
||||
try {
|
||||
Account? test = await _databaseService.GetAccount(UserName.ToLower());
|
||||
if (test != null) {
|
||||
if (test.EmailVerified == true) {
|
||||
if (test.FailedPasswordLock) {
|
||||
if (test.CurrentPasswordAttempts >= test.PasswordAttempts) {
|
||||
return NotFound("Too many failed password attempts. Please reset your password");
|
||||
}
|
||||
}
|
||||
if (BCrypt.Net.BCrypt.Verify(PasswordHash, test.PasswordHash)) {
|
||||
test.CurrentPasswordAttempts = 0;
|
||||
await _databaseService.SetAccount(test);
|
||||
|
||||
string jwt = AuthJWT.GenereateJWTToken(test.ID, StayLoggedIn);
|
||||
AuthJWT.SignIn(Response, StayLoggedIn, jwt);
|
||||
|
||||
return Ok(test);
|
||||
} else {
|
||||
test.CurrentPasswordAttempts += 1;
|
||||
await _databaseService.SetAccount(test);
|
||||
return NotFound("Wrong Password");
|
||||
}
|
||||
} else {
|
||||
await SendVerify(test.UserName);
|
||||
return NotFound("A new verify email has been sent. \n Note only 1 email send every 5 mintes");
|
||||
}
|
||||
}
|
||||
return NotFound("Account Not Found");
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine("Login Error: " + ex.Message);
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
}
|
||||
|
||||
[Route("register")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Account>> Register([FromForm] string Email, [FromForm] string UserName, [FromForm] string PasswordHash) {
|
||||
try {
|
||||
if (await _databaseService.GetAccount(UserName.ToLower()) == null) {
|
||||
if (await _databaseService.GetAccount(Email.ToLower()) == null) {
|
||||
Account created = new Account() {
|
||||
UserName = UserName.ToLower(),
|
||||
Email = Email.ToLower(),
|
||||
EmailVerified = false,
|
||||
PasswordHash = BCrypt.Net.BCrypt.HashPassword(PasswordHash),
|
||||
};
|
||||
await _databaseService.SetAccount(created);
|
||||
Account? loadedAccount = await _databaseService.GetAccount(Email.ToLower());
|
||||
if (loadedAccount != null) {
|
||||
await SendVerify(loadedAccount.UserName);
|
||||
return Ok(loadedAccount);
|
||||
}
|
||||
return NotFound("Unable to create the account");
|
||||
} else {
|
||||
return NotFound("Email is already in use");
|
||||
}
|
||||
} else {
|
||||
return NotFound("UserName is taken");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine("Register Error: " + ex.Message);
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Route("changepassword")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> ChangePassword([FromForm] string OldPassword, [FromForm] string NewPassword) {
|
||||
try {
|
||||
if (isLoggedIn()) {
|
||||
Account user = await getLoggedInUser();
|
||||
if (BCrypt.Net.BCrypt.Verify(OldPassword, user.PasswordHash)) {
|
||||
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(NewPassword);
|
||||
user.CurrentPasswordAttempts = 0;
|
||||
await _databaseService.SetAccount(user);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
return NotFound("Not logged in");
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine("ChangePassword Error: " + ex.Message);
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
}
|
||||
|
||||
[Route("toggleaccountlock")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<string>> ToggleAccountLock([FromForm] bool AccountLock) {
|
||||
try {
|
||||
if (isLoggedIn()) {
|
||||
Account user = await getLoggedInUser();
|
||||
user.FailedPasswordLock = AccountLock;
|
||||
user.CurrentPasswordAttempts = 0;
|
||||
await _databaseService.SetAccount(user);
|
||||
return Ok();
|
||||
}
|
||||
return NotFound("Not logged in");
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine("ToggleAccountLock Error: " + ex.Message);
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
}
|
||||
|
||||
[Route("get")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Account>> Get() {
|
||||
try {
|
||||
if (isLoggedIn()) {
|
||||
return Ok(await getLoggedInUser());
|
||||
}
|
||||
return NotFound("Not logged in");
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine("Get Error: " + ex);
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
}
|
||||
|
||||
[Route("logout")]
|
||||
[HttpPost]
|
||||
public ActionResult Logout() {
|
||||
if (isLoggedIn()) {
|
||||
AuthJWT.SignOut(Response);
|
||||
return Ok();
|
||||
}
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
[Route("sendverifyemail")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<string>> SendVerify([FromForm] string UserName) {
|
||||
try {
|
||||
string key = "v" + UserName;
|
||||
// Stop from sending multiple emails quickly
|
||||
if (_emailContext._SentEmails.ContainsKey(key)) {
|
||||
DateTime PreviousSentTime = _emailContext._SentEmails.GetValueOrDefault(key);
|
||||
if (PreviousSentTime.AddMinutes(5) > DateTime.Now) {
|
||||
return NotFound("Cannot sent another verify email until 5 minutes has elapsed");
|
||||
}
|
||||
else {
|
||||
_emailContext._SentEmails.Remove(key);
|
||||
}
|
||||
}
|
||||
Account? test = await _databaseService.GetAccount(UserName.ToLower());
|
||||
if (test != null) {
|
||||
test.EmailToken = Guid.NewGuid().ToString();
|
||||
await _databaseService.SetAccount(test);
|
||||
|
||||
string EmailContents = EmailService.VerifyEmailEmail;
|
||||
EmailContents = Substitue(EmailContents, "@UserName", UserName);
|
||||
EmailContents = Substitue(EmailContents, "@UserName", UserName);
|
||||
EmailContents = Substitue(EmailContents, "@VerifyPassword", test.EmailToken);
|
||||
|
||||
string result = _emailContext.Send(test.Email, EmailService.VerifyEmailSubject, EmailContents);
|
||||
_emailContext._SentEmails.Add(key, DateTime.Now);
|
||||
return Ok(result);
|
||||
}
|
||||
return NotFound("Account not found");
|
||||
} catch (Exception) {
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
}
|
||||
|
||||
[Route("verifyemail")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<bool>> VerifyEmail([FromForm] string UserName, [FromForm] string EmailToken) {
|
||||
try {
|
||||
Account? test = await _databaseService.GetAccount(UserName.ToLower());
|
||||
if (test != null) {
|
||||
if (!string.IsNullOrEmpty(test.EmailToken) && test.EmailToken == EmailToken) {
|
||||
test.EmailToken = "";
|
||||
test.EmailVerified = true;
|
||||
await _databaseService.SetAccount(test);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
return NotFound("Account not found or token is invalid");;
|
||||
} catch {
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
}
|
||||
|
||||
[Route("sendresetpassword")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<string>> ResetPassword([FromForm] string Email) {
|
||||
try {
|
||||
string key = "p" + Email.ToLower();
|
||||
// Stop from sending multiple emails quickly
|
||||
if (_emailContext._SentEmails.ContainsKey(key)) {
|
||||
DateTime PreviousSentTime = _emailContext._SentEmails.GetValueOrDefault(key);
|
||||
if (PreviousSentTime.AddMinutes(5) > DateTime.Now) {
|
||||
return NotFound("Cannot sent another reset requests until 5 minutes has elapsed");
|
||||
}
|
||||
else {
|
||||
_emailContext._SentEmails.Remove(key);
|
||||
}
|
||||
}
|
||||
Account? test = await _databaseService.GetAccount(Email.ToLower());
|
||||
if (test != null) {
|
||||
test.EmailToken = Guid.NewGuid().ToString();
|
||||
await _databaseService.SetAccount(test);
|
||||
|
||||
string EmailContents = EmailService.ResetPasswordEmail;
|
||||
EmailContents = Substitue(EmailContents, "@UserName", test.UserName);
|
||||
EmailContents = Substitue(EmailContents, "@UserName", test.UserName);
|
||||
EmailContents = Substitue(EmailContents, "@ResetPassWord", test.EmailToken);
|
||||
|
||||
string result = _emailContext.Send(test.Email, EmailService.VerifyEmailSubject, EmailContents);
|
||||
_emailContext._SentEmails.Add(key, DateTime.Now);
|
||||
return Ok(result);
|
||||
}
|
||||
return NotFound("Account Not Found");
|
||||
} catch (Exception e) {
|
||||
Console.WriteLine("EmailService Error: " + e.ToString());
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Route("resetpassword")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<bool>> ResetPwdVerify([FromForm] string UserName, [FromForm] string NewPassword, [FromForm] string ResetToken) {
|
||||
try {
|
||||
Account? test = await _databaseService.GetAccount(UserName.ToLower());
|
||||
if (test != null && !string.IsNullOrEmpty(test.EmailToken)) {
|
||||
if (!string.IsNullOrEmpty(test.EmailToken) && test.EmailToken == ResetToken) {
|
||||
test.CurrentPasswordAttempts = 0;
|
||||
test.EmailToken = "";
|
||||
test.PasswordHash = BCrypt.Net.BCrypt.HashPassword(NewPassword);
|
||||
await _databaseService.SetAccount(test);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
return NotFound("Account not found or reset token is bad");
|
||||
} catch {
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
}
|
||||
|
||||
[Route("delete")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> delete([FromForm] string Password) {
|
||||
try {
|
||||
if (isLoggedIn()) {
|
||||
Account user = await getLoggedInUser();
|
||||
if (BCrypt.Net.BCrypt.Verify(Password, user.PasswordHash)) {
|
||||
await _databaseService.DeleteAccount(user.ID);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
return NotFound("User is not logged in");
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine("Delete Error: " + ex.Message);
|
||||
return NotFound("An internal server error has occured");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Auth.Entities;
|
||||
using Auth.Services.DatabaseService;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Auth.Controllers {
|
||||
|
||||
public class MistoxControllerBase : ControllerBase {
|
||||
|
||||
public DatabaseService _databaseService;
|
||||
|
||||
public MistoxControllerBase(DatabaseService databaseService) {
|
||||
_databaseService = databaseService;
|
||||
}
|
||||
|
||||
public bool isLoggedIn() {
|
||||
if (User.Identity != null && User.Identity.IsAuthenticated) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getLoggedInUserID() {
|
||||
return Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier));
|
||||
}
|
||||
|
||||
public async Task<Account> getLoggedInUser() {
|
||||
try {
|
||||
Account? test = await _databaseService.GetAccount(getLoggedInUserID());
|
||||
if (test != null) {
|
||||
return test;
|
||||
}
|
||||
return new Account();
|
||||
} catch {
|
||||
return new Account();
|
||||
}
|
||||
}
|
||||
|
||||
public string Substitue(string message, string subString, string Replacement) {
|
||||
for (int i = 0; i < (message.Length - subString.Length); i++) {
|
||||
if (message.Substring(i, subString.Length) == subString) {
|
||||
string before = message.Substring(0, i);
|
||||
string after = message.Substring(i + subString.Length);
|
||||
return before + Replacement + after;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
public bool contains(string outer, string inner) {
|
||||
if (outer.Length >= inner.Length) {
|
||||
for (int i = 0; i < outer.Length - inner.Length; i++) {
|
||||
if (outer.Substring(i, inner.Length) == inner) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Auth.Entities {
|
||||
public class Account {
|
||||
public int ID { get; set; } // PK
|
||||
public string UserName { get; set; } = "";
|
||||
public string Email { get; set; } = "";
|
||||
public bool EmailVerified { get; set; } = false;
|
||||
public string PasswordHash { get; set; } = "";
|
||||
public bool FailedPasswordLock { get; set; } = false;
|
||||
public int PasswordAttempts { get; set; } = 5;
|
||||
public int CurrentPasswordAttempts { get; set; } = 0;
|
||||
public string Role { get; set; } = "Generic";
|
||||
public string EmailToken { get; set; } = "";
|
||||
public string DataServer { get; set; } = "";
|
||||
}
|
||||
}
|
||||
Executable
+153
@@ -0,0 +1,153 @@
|
||||
using Auth.Services;
|
||||
using Auth.Services.DatabaseService;
|
||||
using System.Threading.RateLimiting;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Text;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Disable null warnings becuse string.IsNullOrEmpty checks for NULL or Empty
|
||||
#pragma warning disable CS8604
|
||||
|
||||
////////////////////////////////
|
||||
/////// Database Service ///////
|
||||
////////////////////////////////
|
||||
|
||||
// Address
|
||||
string? _dbserver = Environment.GetEnvironmentVariable("MySQLServer");
|
||||
string dbserver = !string.IsNullOrEmpty(_dbserver) ? _dbserver : "localhost";
|
||||
|
||||
// Database
|
||||
string? _dbdatabase = Environment.GetEnvironmentVariable("MySQLDatabase");
|
||||
string dbdatabase = !string.IsNullOrEmpty(_dbdatabase) ? _dbdatabase : "Auth";
|
||||
|
||||
// UserName
|
||||
string? _dbuser = Environment.GetEnvironmentVariable("MySQLUser");
|
||||
string dbUser = !string.IsNullOrEmpty(_dbuser) ? _dbuser : "root";
|
||||
|
||||
// Password
|
||||
string? _dbpass = Environment.GetEnvironmentVariable("MySQLPass");
|
||||
string dbPass = !string.IsNullOrEmpty(_dbpass) ? _dbpass : "oasv34$8gpv023dd";
|
||||
|
||||
// Create the database serivice
|
||||
DatabaseService databaseService = new DatabaseService(connectionString: "server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;");
|
||||
builder.Services.Add( new ServiceDescriptor( typeof( DatabaseService ), databaseService ) );
|
||||
|
||||
////////////////////////////////
|
||||
////////// Auth Service ////////
|
||||
////////////////////////////////
|
||||
|
||||
// Address
|
||||
string? _jwtSecret = Environment.GetEnvironmentVariable("JWTsecret");
|
||||
string JWTsecret = !string.IsNullOrEmpty(_jwtSecret) ? _jwtSecret : "v0Ftluhdh7Nht8^2b5eaiC^IS^VS1ku0VBs3j*B2";
|
||||
AuthJWT.TokenSecretKey = JWTsecret;
|
||||
|
||||
////////////////////////////////
|
||||
///////// Email Service ////////
|
||||
////////////////////////////////
|
||||
|
||||
// Address
|
||||
string? _eServer = Environment.GetEnvironmentVariable("EmailServer");
|
||||
string EmailServer = !string.IsNullOrEmpty(_eServer) ? _eServer : "mail.mistox.com";
|
||||
|
||||
// Port
|
||||
string? _ePort = Environment.GetEnvironmentVariable("EmailPort");
|
||||
int EmailPort = !string.IsNullOrEmpty(_ePort) ? Convert.ToInt32(_ePort) : 587;
|
||||
|
||||
// User
|
||||
string? _eAddress = Environment.GetEnvironmentVariable("EmailAddress");
|
||||
string EmailAddress = !string.IsNullOrEmpty(_eAddress) ? _eAddress : "no-reply@mistox.com";
|
||||
|
||||
// Password
|
||||
string? _ePassword = Environment.GetEnvironmentVariable("EmailPassword");
|
||||
string EmailPassword = !string.IsNullOrEmpty(_ePassword) ? _ePassword : "";
|
||||
|
||||
// Create the email service
|
||||
EmailService Emailservice = new EmailService( EmailServer, EmailPort, EmailAddress, EmailPassword );
|
||||
builder.Services.Add( new ServiceDescriptor( typeof( EmailService ), Emailservice ));
|
||||
|
||||
// Authentication Service
|
||||
builder.Services.AddAuthentication(options => {
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer(options => {
|
||||
options.TokenValidationParameters = new TokenValidationParameters {
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = AuthJWT.TokenIssuer,
|
||||
ValidAudience = AuthJWT.TokenAudience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AuthJWT.TokenSecretKey)),
|
||||
ClockSkew = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
options.Events = new JwtBearerEvents {
|
||||
OnMessageReceived = context => {
|
||||
context.Token = context.Request.Cookies[AuthJWT.TokenName];
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnTokenValidated = context => {
|
||||
var jwtToken = context.SecurityToken as JwtSecurityToken;
|
||||
if (jwtToken != null) {
|
||||
var exp = jwtToken.ValidTo;
|
||||
var now = DateTime.UtcNow;
|
||||
if ((exp - now) < TimeSpan.FromDays(3)) {
|
||||
int accountID = Convert.ToInt32(context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value);
|
||||
bool isPersistent = bool.Parse(context.Principal?.FindFirst(ClaimTypes.IsPersistent)?.Value);
|
||||
var newJWT = AuthJWT.GenereateJWTToken(accountID, isPersistent);
|
||||
AuthJWT.SignIn(context.HttpContext.Response, isPersistent, newJWT);
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.AddCors(o => o.AddDefaultPolicy(builder => {
|
||||
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); // No CORS
|
||||
}));
|
||||
|
||||
builder.Services.AddRateLimiter(options => {
|
||||
options.AddPolicy("PerUserPolicy", httpContext => {
|
||||
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? httpContext.User.Identity?.Name
|
||||
?? httpContext.Connection.RemoteIpAddress?.ToString();
|
||||
|
||||
return RateLimitPartition.GetTokenBucketLimiter(userId, key => new TokenBucketRateLimiterOptions {
|
||||
TokenLimit = 10, // max 10 requests
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
QueueLimit = 0,
|
||||
ReplenishmentPeriod = TimeSpan.FromSeconds(15),
|
||||
TokensPerPeriod = 2,
|
||||
AutoReplenishment = true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Pages Service
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if( !app.Environment.IsDevelopment() ) {
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.MapControllers().RequireRateLimiting("perUserPolicy");
|
||||
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.Run();
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.3.0" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="MySql.Data" Version="9.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="wwwroot\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
using Auth.Entities;
|
||||
using MySql.Data.MySqlClient;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace Auth.Services.DatabaseService {
|
||||
public partial class DatabaseService {
|
||||
|
||||
public async Task<Account?> GetAccount( string UserNameOrEmail ) {
|
||||
Account? account = null;
|
||||
using( MySqlConnection connection = GetConnection() ) {
|
||||
connection.Open();
|
||||
string command = @"
|
||||
SELECT *
|
||||
FROM Account
|
||||
WHERE UserName = @UorE OR Email = @UorE;
|
||||
";
|
||||
|
||||
MySqlCommand cmd = new MySqlCommand(command, connection);
|
||||
cmd.Parameters.AddWithValue("@UorE", UserNameOrEmail);
|
||||
|
||||
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
|
||||
while( await reader.ReadAsync() ) {
|
||||
if( reader == null ) { break; }
|
||||
int _id = reader.GetInt32("ID");
|
||||
string _username = reader.GetString("UserName");
|
||||
string _email = reader.GetString("Email");
|
||||
bool _emailVerified = reader.GetBoolean("EmailVerified");
|
||||
string _passwordhash = reader.GetString("PasswordHash");
|
||||
bool _failedpasswordlock = reader.GetBoolean( "FailedPasswordLock" );
|
||||
int _passwordattempts = reader.GetInt32( "PasswordAttempts" );
|
||||
int _curpasswordattempts = reader.GetInt32( "CurrentPasswordAttempts" );
|
||||
string _role = reader.GetString( "Role" );
|
||||
string _emailtoken = reader.GetString( "EmailToken" );
|
||||
string _dataserver = reader.GetString( "DataServer" );
|
||||
|
||||
account = new Account() {
|
||||
ID = _id,
|
||||
UserName = _username,
|
||||
Email = _email,
|
||||
EmailVerified = _emailVerified,
|
||||
PasswordHash = _passwordhash,
|
||||
CurrentPasswordAttempts = _curpasswordattempts,
|
||||
PasswordAttempts = _passwordattempts,
|
||||
EmailToken = _emailtoken,
|
||||
FailedPasswordLock = _failedpasswordlock,
|
||||
Role = _role,
|
||||
DataServer = _dataserver
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
public async Task<Account?> GetAccount( int AccountID ) {
|
||||
Account? account = null;
|
||||
using( MySqlConnection connection = GetConnection() ) {
|
||||
connection.Open();
|
||||
string command = @"
|
||||
SELECT *
|
||||
FROM Account
|
||||
WHERE ID = @ID;
|
||||
";
|
||||
|
||||
MySqlCommand cmd = new MySqlCommand(command, connection);
|
||||
cmd.Parameters.AddWithValue("@ID", AccountID);
|
||||
|
||||
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
|
||||
while( await reader.ReadAsync() ) {
|
||||
if( reader == null ) {
|
||||
break;
|
||||
}
|
||||
int _id = reader.GetInt32("ID");
|
||||
string _username = reader.GetString("UserName");
|
||||
string _email = reader.GetString("Email");
|
||||
bool _emailVerified = reader.GetBoolean("EmailVerified");
|
||||
string _passwordhash = reader.GetString("PasswordHash");
|
||||
bool _failedpasswordlock = reader.GetBoolean( "FailedPasswordLock" );
|
||||
int _passwordattempts = reader.GetInt32( "PasswordAttempts" );
|
||||
int _curpasswordattempts = reader.GetInt32( "CurrentPasswordAttempts" );
|
||||
string _role = reader.GetString( "Role" );
|
||||
string _emailtoken = reader.GetString( "EmailToken" );
|
||||
string _dataserver = reader.GetString("DataServer");
|
||||
|
||||
account = new Account() {
|
||||
ID = _id,
|
||||
UserName = _username,
|
||||
Email = _email,
|
||||
EmailVerified = _emailVerified,
|
||||
PasswordHash = _passwordhash,
|
||||
CurrentPasswordAttempts = _passwordattempts,
|
||||
PasswordAttempts = _passwordattempts,
|
||||
EmailToken = _emailtoken,
|
||||
FailedPasswordLock = _failedpasswordlock,
|
||||
Role = _role,
|
||||
DataServer = _dataserver
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
public async Task SetAccount( Account Profile ) {
|
||||
using( MySqlConnection connection = GetConnection() ) {
|
||||
connection.Open();
|
||||
|
||||
string command = @"
|
||||
INSERT INTO Account
|
||||
(ID,UserName,Email,EmailVerified,PasswordHash,FailedPasswordLock,PasswordAttempts,CurrentPasswordAttempts,Role,EmailToken,DataServer)
|
||||
VALUES
|
||||
(@ID,@UserName,@Email,@EmailVerified,@PasswordHash,@FailedPasswordLock,@PasswordAttempts,@CurrentPasswordAttempts,@Role,@EmailToken,@DataServer)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
UserName = @UserName,
|
||||
Email = @Email,
|
||||
EmailVerified = @EmailVerified,
|
||||
PasswordHash = @PasswordHash,
|
||||
FailedPasswordLock = @FailedPasswordLock,
|
||||
PasswordAttempts = @PasswordAttempts,
|
||||
CurrentPasswordAttempts = @CurrentPasswordAttempts,
|
||||
Role = @Role,
|
||||
EmailToken = @EmailToken,
|
||||
DataServer = @DataServer;
|
||||
";
|
||||
|
||||
MySqlCommand cmd = new MySqlCommand( command , connection);
|
||||
cmd.Parameters.AddWithValue("@ID", Profile.ID);
|
||||
cmd.Parameters.AddWithValue("@UserName", Profile.UserName);
|
||||
cmd.Parameters.AddWithValue("@Email", Profile.Email);
|
||||
cmd.Parameters.AddWithValue("@EmailVerified", Profile.EmailVerified);
|
||||
cmd.Parameters.AddWithValue("@PasswordHash", Profile.PasswordHash);
|
||||
cmd.Parameters.AddWithValue("@FailedPasswordLock", Profile.FailedPasswordLock);
|
||||
cmd.Parameters.AddWithValue("@PasswordAttempts", Profile.PasswordAttempts);
|
||||
cmd.Parameters.AddWithValue("@CurrentPasswordAttempts", Profile.CurrentPasswordAttempts);
|
||||
cmd.Parameters.AddWithValue("@Role", Profile.Role);
|
||||
cmd.Parameters.AddWithValue("@EmailToken", Profile.EmailToken);
|
||||
cmd.Parameters.AddWithValue("@DataServer", Profile.DataServer);
|
||||
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAccount( int AccountID ) {
|
||||
using( MySqlConnection connection = GetConnection() ) {
|
||||
MySqlCommand cmd;
|
||||
connection.Open();
|
||||
|
||||
string command = @"
|
||||
DELETE FROM Account WHERE ID = @ID;
|
||||
";
|
||||
cmd = new MySqlCommand( command, connection );
|
||||
cmd.Parameters.AddWithValue("@ID", AccountID);
|
||||
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
using MySql.Data.MySqlClient;
|
||||
|
||||
namespace Auth.Services.DatabaseService {
|
||||
public partial class DatabaseService {
|
||||
public string ConnectionString {
|
||||
get; set;
|
||||
}
|
||||
public DatabaseService( string connectionString ) {
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
MySqlConnection GetConnection() {
|
||||
return new MySqlConnection( ConnectionString );
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace Auth.Services {
|
||||
public partial class EmailService {
|
||||
|
||||
public Dictionary<string, DateTime> _SentEmails = new Dictionary<string, DateTime>();
|
||||
|
||||
public string EmailServer = "";
|
||||
public string EmailAddress = "";
|
||||
public string EmailPassword = "";
|
||||
public int EmailPort;
|
||||
|
||||
public EmailService( string _EmailServer, int _EmailPort, string _EmailAddress, string _EmailPassword ) {
|
||||
EmailServer = _EmailServer;
|
||||
EmailPort = _EmailPort;
|
||||
EmailAddress = _EmailAddress;
|
||||
EmailPassword = _EmailPassword;
|
||||
}
|
||||
|
||||
public string Send( string Destination, string Subject, string Body ) {
|
||||
using (SmtpClient client = new SmtpClient( EmailServer, EmailPort )){
|
||||
client.EnableSsl = true;
|
||||
client.Credentials = new System.Net.NetworkCredential( EmailAddress, EmailPassword );
|
||||
try {
|
||||
MailMessage msg = new MailMessage(){
|
||||
IsBodyHtml = true,
|
||||
Subject = Subject,
|
||||
Body = Body
|
||||
};
|
||||
msg.From = new MailAddress( EmailAddress, "no-reply" );
|
||||
msg.To.Add( new MailAddress( Destination ) );
|
||||
client.Send( msg );
|
||||
return "Success";
|
||||
} catch( Exception e ) {
|
||||
return "An Error Has Occurred Sending Email : " + e.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
namespace Auth.Services {
|
||||
public partial class EmailService {
|
||||
|
||||
// @UserName
|
||||
// @ResetPassWord
|
||||
// https://mistox.com/account/resetpassword?UserName=@UserName&ResetPwd=@ResetPassWord
|
||||
|
||||
public static string ResetPasswordSubject = "Password Reset Request";
|
||||
public static string ResetPasswordEmail = @"
|
||||
<!DOCTYPE html>
|
||||
<html lang=""en"">
|
||||
<head>
|
||||
<meta charset=""UTF-8"">
|
||||
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
<body style=""font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;"">
|
||||
<table role=""presentation"" style=""width: 100%; background-color: #f4f4f4; padding: 20px 0;"">
|
||||
<tr>
|
||||
<td>
|
||||
<table role=""presentation"" style=""max-width: 600px; width: 100%; background-color: #ffffff; margin: 0 auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);"">
|
||||
<tr>
|
||||
<td style=""padding: 20px; text-align: center; background-color: #4CAF50; color: #ffffff; border-top-left-radius: 8px; border-top-right-radius: 8px;"">
|
||||
<h2>Password Reset Request</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=""padding: 20px; text-align: left; font-size: 16px; color: #333333;"">
|
||||
<p>Hi @UserName,</p>
|
||||
<p>We received a request to reset your password. You can reset your password by clicking the button below:</p>
|
||||
<p style=""text-align: center;"">
|
||||
<a href=""https://mistox.com/account/resetpassword?UserName=@UserName&ResetPwd=@ResetPassWord"" style=""background-color: #4CAF50; color: #ffffff; text-decoration: none; padding: 15px 25px; font-size: 16px; border-radius: 5px; display: inline-block;"">Reset Password</a>
|
||||
</p>
|
||||
<p>If you didn't request a password reset, you can safely ignore this email.</p>
|
||||
<p>Best regards</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=""padding: 10px; text-align: center; background-color: #f4f4f4; color: #888888; font-size: 12px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;"">
|
||||
<p>If you have any questions, feel free to <a href=""mailto:webmaster@mistox.com"" style=""color: #4CAF50; text-decoration: none;"">contact support</a>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
";
|
||||
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
namespace Auth.Services {
|
||||
public partial class EmailService {
|
||||
|
||||
// @UserName
|
||||
// @VerifyPassword
|
||||
// https://mistox.com/api/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword
|
||||
|
||||
public static string VerifyEmailSubject = "Verify Your Email Address";
|
||||
public static string VerifyEmailEmail = @"
|
||||
<!DOCTYPE html>
|
||||
<html lang=""en"">
|
||||
<head>
|
||||
<meta charset=""UTF-8"">
|
||||
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||
<title>Verify Your Email</title>
|
||||
</head>
|
||||
<body style=""font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;"">
|
||||
<table role=""presentation"" style=""width: 100%; background-color: #f4f4f4; padding: 20px 0;"">
|
||||
<tr>
|
||||
<td>
|
||||
<table role=""presentation"" style=""max-width: 600px; width: 100%; background-color: #ffffff; margin: 0 auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);"">
|
||||
<tr>
|
||||
<td style=""padding: 20px; text-align: center; background-color: #4CAF50; color: #ffffff; border-top-left-radius: 8px; border-top-right-radius: 8px;"">
|
||||
<h2>Verify Email Request</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=""padding: 20px; text-align: left; font-size: 16px; color: #333333;"">
|
||||
<p>Hi @UserName,</p>
|
||||
<p>Thank you for making an account with us:</p>
|
||||
<p>In order to start using your account we need to verify your email address by clicking the link below:</p>
|
||||
<p style=""text-align: center;"">
|
||||
<a href=""https://mistox.com/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword"" style=""background-color: #4CAF50; color: #ffffff; text-decoration: none; padding: 15px 25px; font-size: 16px; border-radius: 5px; display: inline-block;"">Verify Email</a>
|
||||
</p>
|
||||
<p>If you didn't create an account please ignore this email.</p>
|
||||
<p>Best regards</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style=""padding: 10px; text-align: center; background-color: #f4f4f4; color: #888888; font-size: 12px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;"">
|
||||
<p>If you have any questions, feel free to <a href=""mailto:webmaster@mistox.com"" style=""color: #4CAF50; text-decoration: none;"">contact support</a>.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
";
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Auth.Services {
|
||||
public class AuthJWT {
|
||||
|
||||
public static string TokenAudience = "mistox-llc-auth-token";
|
||||
public static string TokenIssuer = "https://auth.mistox.com";
|
||||
public static string TokenSecretKey = "";
|
||||
public static string TokenName = "mistox_session";
|
||||
|
||||
public static string GenereateJWTToken(int accountID, bool StayLoggedIn) {
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.UTF8.GetBytes(TokenSecretKey);
|
||||
|
||||
var tokenDiscriptor = new SecurityTokenDescriptor {
|
||||
Subject = new ClaimsIdentity([
|
||||
new Claim(ClaimTypes.NameIdentifier, accountID.ToString()),
|
||||
new Claim(ClaimTypes.IsPersistent, StayLoggedIn.ToString())
|
||||
]),
|
||||
Expires = DateTime.UtcNow.AddDays(7),
|
||||
IssuedAt = DateTime.UtcNow,
|
||||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256),
|
||||
Audience = TokenAudience,
|
||||
Issuer = TokenIssuer
|
||||
};
|
||||
|
||||
var token = tokenHandler.CreateToken(tokenDiscriptor);
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
|
||||
public static void SignIn(HttpResponse Response, bool StayLoggedIn, string jwt) {
|
||||
if (StayLoggedIn) {
|
||||
// Stay logged in cookie
|
||||
Response.Cookies.Append(TokenName, jwt, new CookieOptions {
|
||||
Secure = true,
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Expires = DateTime.UtcNow.AddDays(7)
|
||||
});
|
||||
} else {
|
||||
// Session cookie
|
||||
Response.Cookies.Append(TokenName, jwt, new CookieOptions {
|
||||
Secure = true,
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void SignOut(HttpResponse Response) {
|
||||
Response.Cookies.Delete(TokenName);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.002.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", ".\Server.csproj", "{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B413876B-4048-47F1-B8B8-B974DF5E9E2A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 587 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 311 KiB |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
Reference in New Issue
Block a user