Copy base from mistoxwebsite
Docker Build and Release Upload / build (push) Has been cancelled

This commit is contained in:
2025-07-13 19:04:43 -07:00
parent a0e106c2bc
commit 22a30933ca
103 changed files with 13926 additions and 457 deletions
+16
View File
@@ -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
+15
View File
@@ -0,0 +1,15 @@
Payment_Service=StripeIntent # Options are [ StripeIntent ]
Stripe_PublicKey=
Stripe_PublicKey=
Stripe_Endpoint_Secret=
MySQL_Server=mistox-database
MySQL_User=root
MySQL_Database=mistox
MySQL_Pass=oasv34$8gpv023dd # Random value for the server and MySQL to communicate with
Email_Server= # Hostname of email server
Email_Port= # SMTP port used
Email_Address= # Email Address to send from
Email_Password= # Password for the email address
+33
View File
@@ -0,0 +1,33 @@
name: Docker Build and Release Upload
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
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 \
.
Regular → Executable
+46 -3
View File
@@ -1,3 +1,46 @@
debug
obj
bin
# 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
+2 -2
View File
@@ -5,8 +5,8 @@
"name": "Launch ASP.NET Core backend",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "server-build",
"program": "boredcareers.dll",
"preLaunchTask": "build-all",
"program": "Server.dll",
"args": [],
"cwd": "${workspaceFolder}/debug/",
"stopAtEntry": false,
+31 -1
View File
@@ -7,11 +7,41 @@
"type": "process",
"args": [
"build",
"${workspaceFolder}/boredcareers/boredcareers.csproj",
"${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
View File
@@ -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
+53
View File
@@ -0,0 +1,53 @@
Server:
AccountInventory.cs
SetInventory isnt fully implimented
ProjectMistData.cs
Data inside the sql doesnt match what is inside the database
Emails:
Dont follow theme of website
Admin Functions:
Dont inforce Admin on the API side
Authentication ProductController
When the create account is called. right after the getaccount is called.
Have all New for database return the object they create
Update API
Split apart the different routes and Functions
No more new / update -> only get / set
Make all apis return statuscodes
make all input types form's
make all getLoggedInUsers() -> make sure that i cant just call getLoggedInUserID
Need to timeout email reset tokens
Client:
Program
Probably need to turn on cors at some point
Account
Need to add in settings / data pages
After a new account is created notify a user that they need to verify their email before logging in
ProductController
Need to figure out new way to download purchased items as there is currently no way
Store
Edit product needs created
Need to add cart back
Need to add in payment page
Need to add in payment success/failed
Need to add in Receipt page
TopBar
No way to minimize the UI topbar on mobile
Not themed on mobile
API
Some of the API's Changed. Need to go back and update the client API calls
database
Need to create all the forign key policies
-34
View File
@@ -1,34 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "boredcareers", "boredcareers\boredcareers.csproj", "{05900A3D-9780-47A4-90D9-D99673FA5EDB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Debug|x64.ActiveCfg = Debug|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Debug|x64.Build.0 = Debug|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Debug|x86.ActiveCfg = Debug|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Debug|x86.Build.0 = Debug|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Release|Any CPU.Build.0 = Release|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Release|x64.ActiveCfg = Release|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Release|x64.Build.0 = Release|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Release|x86.ActiveCfg = Release|Any CPU
{05900A3D-9780-47A4-90D9-D99673FA5EDB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
-12
View File
@@ -1,12 +0,0 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
-3
View File
@@ -1,3 +0,0 @@
@inherits LayoutComponentBase
<main> @Body </main>
-32
View File
@@ -1,32 +0,0 @@
@page "/"
<style>
</style>
<div id="PageFrame">
<div id="Header">
<div id="LoginButtons">
<button @onclick="LoginClick" class="LoginButton">Login</button>
<button @onclick="RegisterClick" class="LoginButton">Register</button>
</div>
</div>
<div id="Body">
</div>
<div id="Footer">
</div>
</div>
@code{
void LoginClick(){
}
void RegisterClick(){
}
}
-30
View File
@@ -1,30 +0,0 @@
@page "/"
@using Microsoft.AspNetCore.Components.Web
@namespace boredcareers.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="~/" />
<link href="css/site.css" rel="stylesheet" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
<component type="typeof(App)" render-mode="ServerPrerendered" />
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
-25
View File
@@ -1,25 +0,0 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
@@ -1,35 +0,0 @@
{
"iisSettings": {
"iisExpress": {
"applicationUrl": "http://localhost:1409",
"sslPort": 44381
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5019",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7233;http://localhost:5019",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
-4
View File
@@ -1,4 +0,0 @@
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using boredcareers
@@ -1,9 +0,0 @@
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
-9
View File
@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
-9
View File
@@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
@@ -1,71 +0,0 @@
{
"format": 1,
"restore": {
"/home/derek/Desktop/boredcareers/boredcareers/boredcareers.csproj": {}
},
"projects": {
"/home/derek/Desktop/boredcareers/boredcareers/boredcareers.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "/home/derek/Desktop/boredcareers/boredcareers/boredcareers.csproj",
"projectName": "boredcareers",
"projectPath": "/home/derek/Desktop/boredcareers/boredcareers/boredcareers.csproj",
"packagesPath": "/home/derek/.nuget/packages/",
"outputPath": "/home/derek/Desktop/boredcareers/boredcareers/obj/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/home/derek/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net9.0"
],
"sources": {
"/usr/share/dotnet/library-packs": {},
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "9.0.300"
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48",
"net481"
],
"assetTargetFallback": true,
"warn": true,
"frameworkReferences": {
"Microsoft.AspNetCore.App": {
"privateAssets": "none"
},
"Microsoft.NETCore.App": {
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/9.0.301/PortableRuntimeIdentifierGraph.json"
}
}
}
}
}
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/derek/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/derek/.nuget/packages/</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="/home/derek/.nuget/packages/" />
</ItemGroup>
</Project>
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
-76
View File
@@ -1,76 +0,0 @@
{
"version": 3,
"targets": {
"net9.0": {}
},
"libraries": {},
"projectFileDependencyGroups": {
"net9.0": []
},
"packageFolders": {
"/home/derek/.nuget/packages/": {}
},
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "/home/derek/Desktop/boredcareers/boredcareers/boredcareers.csproj",
"projectName": "boredcareers",
"projectPath": "/home/derek/Desktop/boredcareers/boredcareers/boredcareers.csproj",
"packagesPath": "/home/derek/.nuget/packages/",
"outputPath": "/home/derek/Desktop/boredcareers/boredcareers/obj/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/home/derek/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net9.0"
],
"sources": {
"/usr/share/dotnet/library-packs": {},
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "9.0.300"
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48",
"net481"
],
"assetTargetFallback": true,
"warn": true,
"frameworkReferences": {
"Microsoft.AspNetCore.App": {
"privateAssets": "none"
},
"Microsoft.NETCore.App": {
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "/usr/share/dotnet/sdk/9.0.301/PortableRuntimeIdentifierGraph.json"
}
}
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"version": 2,
"dgSpecHash": "re43MfcLS0M=",
"success": true,
"projectFilePath": "/home/derek/Desktop/boredcareers/boredcareers/boredcareers.csproj",
"expectedPackageFiles": [],
"logs": []
}
-77
View File
@@ -1,77 +0,0 @@
* {
border: 0;
padding: 0;
margin: 0;
}
#Header {
width: 100%;
height: 200px;
background-color: blue;
display: flex;
align-items: center;
justify-content: right;
}
#LoginButtons {
width: 400px;
height: 100px;
background-color: #b32121;
margin-right: 50px;
display: flex;
}
.LoginButton {
width: 125px;
margin: 25px 0 25px 50px;
border-radius: 5px;
background-color: #aaa;
color: #000;
font-size: 20px;
}
.LoginButton:hover {
background-color: #555;
color: #fff;
}
#Body {
background-color: green;
min-height: calc( 100vh - 400px );
}
#Footer {
width: 100%;
height: 200px;
background-color: blue;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 3.5rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
+8
View File
@@ -0,0 +1,8 @@
FROM mysql
ENV MYSQL_DATABASE=boredcareers
ENV MYSQL_ROOT_PASSWORD=90pa8pav89h4g08hads
ADD mistox.sql /docker-entrypoint-initdb.d
EXPOSE 3306
+200
View File
@@ -0,0 +1,200 @@
CREATE DATABASE IF NOT EXISTS `boredcareers`;
USE `boredcareers`;
-- 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,
`ServerRegion` varchar(10) DEFAULT NULL,
PRIMARY KEY (`ID`)
) AUTO_INCREMENT=1;
-- Resume Section
CREATE TABLE IF NOT EXISTS `Resume` (
`ID` int NOT NULL AUTO_INCREMENT,
`Name` varchar(100) NOT NULL,
`Field` varchar(100) DEFAULT NULL,
`Email` varchar(255) NOT NULL,
`PhoneNumber` varchar(20) NOT NULL,
`PostalCode` varchar(20) NOT NULL,
`Country` char(2) NOT NULL,
`StateOrRegion` varchar(100) NOT NULL,
`City` varchar(100) NOT NULL,
`IsActive` boolean DEFAULT 0,
`AccountID` int NOT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`AccountID`) REFERENCES `Account`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `ResumeExperience` (
`ID` int NOT NULL AUTO_INCREMENT,
`ResumeID` int NOT NULL,
`JobTitle` varchar(150) NOT NULL,
`Company` varchar(100) NOT NULL,
`PostalCode` varchar(20) NOT NULL,
`Country` char(2) NOT NULL,
`StateOrRegion` varchar(100) NOT NULL,
`City` varchar(100) NOT NULL,
`DateStarted` date NOT NULL,
`StillEmployed` boolean DEFAULT 0,
`DateEnded` date DEFAULT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`ResumeID`) REFERENCES `Resume`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `ResumeExperienceBullet` (
`ID` int NOT NULL AUTO_INCREMENT,
`ResumeExperienceID` int NOT NULL,
`JobFunction` text NOT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`ResumeExperienceID`) REFERENCES `ResumeExperience`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `ResumeMillitary` (
`ID` int NOT NULL AUTO_INCREMENT,
`ResumeID` int NOT NULL,
`Country` char(2) NOT NULL,
`Rank` varchar(50) NOT NULL,
`DateStarted` date NOT NULL,
`StillServing` boolean DEFAULT 0,
`DateEnded` date DEFAULT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`ResumeID`) REFERENCES `Resume`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `ResumeMillitaryBullet` (
`ID` int NOT NULL AUTO_INCREMENT,
`ResumeMillitaryID` int NOT NULL,
`Achevement` varchar(100) NOT NULL,
`Description` text DEFAULT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`ResumeMillitaryID`) REFERENCES `ResumeMillitary`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `ResumeEducation` (
`ID` int NOT NULL AUTO_INCREMENT,
`ResumeID` int NOT NULL,
`DegreeType` varchar(150) NOT NULL,
`DegreeField` varchar(150) NOT NULL,
`School` varchar(150) NOT NULL,
`PostalCode` varchar(20) NOT NULL,
`Country` char(2) NOT NULL,
`StateOrRegion` varchar(100) NOT NULL,
`City` varchar(100) NOT NULL,
`DateStarted` date NOT NULL,
`StillStudying` boolean DEFAULT 0,
`DateEnded` date DEFAULT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`ResumeID`) REFERENCES `Resume`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `ResumeSkill` (
`ID` int NOT NULL AUTO_INCREMENT,
`ResumeID` int NOT NULL,
`Name` varchar(150) NOT NULL,
`Description` text DEFAULT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`ResumeID`) REFERENCES `Resume`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `ResumeLanguage` (
`ID` int NOT NULL AUTO_INCREMENT,
`ResumeID` int NOT NULL,
`Language` varchar(150) NOT NULL,
`Proficiency` text DEFAULT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`ResumeID`) REFERENCES `Resume`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `ResumeCertification` (
`ID` int NOT NULL AUTO_INCREMENT,
`ResumeID` int NOT NULL,
`Name` varchar(150) NOT NULL,
`VerificationURL` varchar(200) DEFAULT NULL,
`Description` text DEFAULT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`ResumeID`) REFERENCES `Resume`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `ResumeProject` (
`ID` int NOT NULL AUTO_INCREMENT,
`ResumeID` int NOT NULL,
`Name` varchar(150) NOT NULL,
`URL` varchar(200) DEFAULT NULL,
`Description` text DEFAULT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`ResumeID`) REFERENCES `Resume`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
-- Company Section
CREATE TABLE IF NOT EXISTS `Company` (
`ID` int NOT NULL AUTO_INCREMENT,
`Name` varchar(100) DEFAULT NULL,
`Email` varchar(255) DEFAULT NULL,
`EmailVerified` boolean DEFAULT 0,
`Website` VARCHAR(255) DEFAULT NULL,
`LogoURL` VARCHAR(2048) DEFAULT NULL,
`Phone` VARCHAR(20) DEFAULT NULL,
`PostalCode` varchar(20) NOT NULL,
`Country` CHAR(2) NOT NULL,
`StateOrRegion` varchar(100) NOT NULL,
`City` VARCHAR(100),
`Description` TEXT,
PRIMARY KEY (`ID`)
) AUTO_INCREMENT=1;
CREATE TABLE IF NOT EXISTS `JobListing` (
`ID` int NOT NULL AUTO_INCREMENT,
`CompanyID` int NOT NULL,
`Title` varchar(100) NOT NULL,
`PostalCode` varchar(20) NOT NULL,
`Country` char(2) NOT NULL,
`StateOrRegion` varchar(100) NOT NULL,
`City` varchar(100) NOT NULL,
`SalaryMin` int NOT NULL,
`SalaryMax` int NOT NULL,
`JobType` varchar(20) NOT NULL,
`Remote` boolean DEFAULT 0,
`URL` varchar(2048) DEFAULT NULL,
`Description` text NOT NULL,
PRIMARY KEY (`ID`),
FOREIGN KEY (`CompanyID`) REFERENCES `Company`(`ID`) ON DELETE CASCADE
) AUTO_INCREMENT=1;
-- Default Account
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',
''
);
+32
View File
@@ -0,0 +1,32 @@
services:
mistox-server:
container_name: mistox_server
image: docker.mistox.net/boredcareers-website:latest
restart: always
environment:
- PaymentService=${Payment_Service}
- StripePublicKey=${Stripe_PublicKey}
- StripeApiKey=${Stripe_ApiKey}
- StripeEndpointSecret=&{Stripe_Endpoint_Secret}
- MySQLServer=${MySQL_Server}
- MySQLUser=${MySQL_User}
- MySQLPass=${MySQL_Pass}
- MySQLDatabase=${MySQL_Database}
- EmailServer=${Email_Server}
- EmailPort=${Email_Port}
- EmailAddress=${Email_Address}
- EmailPassword=${Email_Password}
ports:
- 5000:5000
depends_on:
- mistox-database
mistox-database:
container_name: mistox_database
image: docker.mistox.net/boredcareers-sql:latest
restart: always
volumes:
- ./data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${MySQL_Pass}
+97
View File
@@ -0,0 +1,97 @@
{
"$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"
]
}
}
}
}
}
}
+8931
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
{
"name": "mistox-frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"@stripe/stripe-js": "^7.4.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular/build": "^20.0.2",
"@angular/cli": "^20.0.2",
"@angular/compiler-cli": "^20.0.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.7.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.8.2"
}
}
+13
View File
@@ -0,0 +1,13 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi())
]
};
+200
View File
@@ -0,0 +1,200 @@
.navbar-toggler {
background-color: var(--Mistox-Dark);
color: var(--Mistox-White);
width: 100%;
height: 40px;
border: none;
font-size: 20px;
transition-duration: 0.5s;
}
.navbar-toggler:hover {
background-color: #410a04;
}
.top-row {
background-color: var(--Mistox-Offset);
height: 200px;
}
.bottom-row {
height: calc(100% - 200px);
background: linear-gradient(0deg, var(--Mistox-Dark), var(--Mistox-Offset) );
}
.navbar-brand img {
width: 200px;
height: 200px;
padding: 0 25px;
}
.oi {
width: 2rem;
font-size: 1.1rem;
color: var(--Mistox-White);
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
text-decoration: none;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item a {
color: var(--Mistox-White);
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
transition-duration: 0.5s;
text-decoration: none;
padding-left: 20px;
}
.nav-item a.active {
background-color: rgba(255,255,255,0.25);
color: var(--Mistox-White);
}
.nav-item a:hover {
background-color: rgba(255,255,255,0.1);
color: var(--Mistox-White);
}
.nav-login {
position: relative;
bottom: 10px;
left: 10px;
width: calc(100% - 20px);
padding-top: 10px;
}
.collapse {
display: none;
}
.nav-login-button {
display: inline-block;
width: calc(50% - 2.5px);
background-color: transparent;
border-radius: 5px;
border-color: transparent;
color: var(--Mistox-White);
transition-duration: 0.5s;
padding: 5px 0;
padding-top: 9px;
text-align: center;
text-decoration: none;
font-size: 15;
}
.nav-login-button:hover {
background-color: #FFFFFF50;
}
article{
padding: 0 !important;
}
.page {
position: relative;
display: flex;
flex-direction: column;
background-color: var(--Mistox-Black);
}
body{
background-color: var(--Mistox-Black);
}
main {
flex: 1;
background-color: var(--Mistox-Black);
color: var(--Mistox-White);
}
.sidebar {
border-right: var(--Mistox-Background) 2px solid;
}
@media (max-width: 640.98px) {
.top-row:not(.auth) {
display: none;
}
.top-row.auth {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
min-width: 250px;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block !important;
}
.nav-login {
position: absolute;
}
}
@media (max-width: 640px){
.navbar-brand img {
position: relative !important;
padding-left: calc(50% - 80px) !important;
height: 160px;
width: 160px;
}
.top-gradient {
background: linear-gradient(0deg, var(--Mistox-Dark), var(--Mistox-Offset) );
}
.bottom-row {
background: var(--Mistox-Medium);
}
}
+63
View File
@@ -0,0 +1,63 @@
<div class="page">
<!-- Sidebar Start -->
<div class="sidebar">
<div class="top-row">
<div class="top-gradient">
<a class="navbar-brand" href="">
<img src="img/logo.png" />
</a>
</div>
<button title="DropDownButtonMobile" class="navbar-toggler">
<span class="navbar-toggler-icon">MENU</span>
</button>
</div>
<div class="bottom-row">
<div class="@NavMenuCssClass">
<nav class="flex-column">
<!-- Home -->
<div class="nav-item">
<a #homeLink class="nav-link" href="">
<span>Home</span>
</a>
</div>
<!-- Project Mist -->
<div class="nav-item">
<a #mistLink class="nav-link" href="/project/mist">
<span>Project-Mist</span>
</a>
</div>
<!-- Store -->
<div class="nav-item">
<a #storeLink class="nav-link" href="/store/catalog">
<span>Store</span>
</a>
</div>
<!-- About -->
<div class="nav-item">
<a #aboutLink class="nav-link" href="/about">
<span>About</span>
</a>
</div>
<!-- Login Stuff -->
<div class="nav-login">
<div *ngIf="auth.isLoggedIn">
<a class="nav-login-button" href="/account/settings"><span>{{ auth.loggedInUser.userName }}</span></a>
<a class="nav-login-button" href="/account/logout"><span>Logout</span></a>
</div>
<div *ngIf="!auth.isLoggedIn">
<a class="nav-login-button" href="/account/login"><span>Login</span></a>
<a class="nav-login-button" href="/account/register"><span>Register</span></a>
</div>
</div>
</nav>
</div>
</div>
</div>
<!-- Sidebar End -->
<main>
<article class="content px-4">
<router-outlet />
</article>
</main>
</div>
+38
View File
@@ -0,0 +1,38 @@
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 { 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 { EditItemComponent } from './pages/store/admin/edititem/edit.component';
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
{ path: "project/mist", component: MistComponent },
// Store
{ path: "store/catalog", component: CatalogComponent },
// AdminPages
{ path: "store/admin/new", component: NewItemComponent },
{ path: "store/admin/edit", component: EditItemComponent },
// Legal
{ path: "about", component: AboutComponent },
]
+30
View File
@@ -0,0 +1,30 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { Router, RouterOutlet } from '@angular/router';
import { Authentication } from './services/Authentication';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
imports: [RouterOutlet, CommonModule],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
@ViewChild('homeLink') homeLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('mistLink') mistLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('storeLink') storeLink!: ElementRef<HTMLAnchorElement>;
@ViewChild('aboutLink') aboutLink!: ElementRef<HTMLAnchorElement>;
constructor(public auth: Authentication, private router: Router){}
ngAfterViewInit(){
let ViewLinks = [ this.homeLink, this.mistLink, this.storeLink, this.aboutLink ];
ViewLinks.forEach(link => {
if (new URL(link.nativeElement.href).pathname === new URL(window.location.href).pathname){
link.nativeElement.classList.add("active");
}
});
}
}
+15
View File
@@ -0,0 +1,15 @@
import { WebSiteData } from "./WebsiteData";
export class Account {
public id: number = -1;
public userName: string = "";
public email: string = "";
public emailVerified: boolean = false;
public passwordHash: string = "";
public siteData: WebSiteData = new WebSiteData();
public error: string = "";
constructor(init?: Partial<Account>) {
Object.assign(this, init);
}
}
+15
View File
@@ -0,0 +1,15 @@
export class Product {
public id: number = -1;
public name: string = "";
public description: string = "";
public curShowingIMG: number = 0;
public images: ProductImage[] = [];
public cost: number = 0;
public url: string = "";
}
export class ProductImage {
imageID: number = 0;
productID: number = 0;
imageSrc: string = "";
}
+12
View File
@@ -0,0 +1,12 @@
export class WebSiteData {
public accountID: number = -1;
public failedPasswordLock: boolean = false;
public passwordAttempts: number = 5;
public currentPasswordAttempts: number = 0;
public role: string = "Generic";
public emailToken: string = "";
constructor(init?: Partial<WebSiteData>) {
Object.assign(this, init);
}
}
@@ -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,57 @@
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);
}
}
)
}
}
@@ -0,0 +1,27 @@
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(["/"]);
}
}
@@ -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,85 @@
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);
}
});
}
}
@@ -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,28 @@
<div class="center">
<div class="big-frame background-border text-frame">
<p>Welcome to Mistox LLC. A project and hobby of Derek Holloway.</p>
<br />
<p>I am an indi-developer who has been making small projects since I was 13. I originally learned lua and spent 4 years mastering it. Then I moved onto C# which is my preferred language</p>
<p>My programming catalog consist of C#, Lua, SQL, C++, C, and JavaScript in the order of knowledge from best to passiable.</p>
<p>Im currently in college for computer sciences and should honestly be doing that instead of this but I find working on this website and hobby games to be way more enjoyable.</p>
<br />
<p>I would love to learn how to use Blender in order to make all the models for my games but with the amount of work ive already made for myself im going to hold off for now.</p>
<p>This website and everything on it are the long countless hours of my time and motivation to create something that I can be proud of and share that with the world.</p>
<p>So if you would like to support me as a small creator please feel free to leave a donation from on the store page. It would means a lot to me.</p>
<br />
<p>For the nerds out there, this website is a blazor webassembly app, hosted on an ubuntu webserver, with a mysql backend.</p>
<p>All the passwords are encrypted using bcrypt for your safety and all the data is only allowed through SSL.</p>
<p>After you make your account. All the data in the database is easily accessable through the account settings and</p>
<p>you can delete your account at any time. Including all your data with it so there is no risk.</p>
<p>I wont show ads and never will and I refuse to use trackers on this site.</p>
<br />
<br />
<p>If you have any questions, concerns, or would like to suggest a feature, bug-fix, or request to help. Please feel</p>
<p>free to reach out to me at <a href="mailto://derek@mistox.net">derek&commat;mistox.net</a></p>
<a href='https://ko-fi.com/A0A3TSI2D' target='_blank'>
<img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi6.png?v=6' alt='Buy Me a Coffee at ko-fi.com' />
</a>
</div>
</div>
@@ -0,0 +1,19 @@
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");
};
}
@@ -0,0 +1,18 @@
<div class="center">
<div class="big-frame background-border text-frame">
<p><strong>What is the game</strong></p>
<p style="padding-left: 30px;">Project-Mist is a survival game. Kind of like a battle royal in a sense but, think of it backwards. And no I know what your thinking. Its not the first person to die wins. No instead its a never ending survival game where you can free roam and build structures. The catch is, the person who has the highest stats [i.e A combination of kills, survival time] has a marker placed on their forhead.</p>
<p><strong>How will the game play</strong></p>
<p style="padding-left: 30px;">When you join the game you will be able to customize your character. There you can set a default loadout for your player. This will be the spawn weapon and gear. After that you will drop into the map with other players to fend for your life. The kill-leader will be marked loosely on the mini-map. You can choose to go after the kill leader or you can choose to loot first. The choice is yours. But be aware that if you survive long enough you will become the new kill leader.</p>
<p><strong>Current Idea Board *SUBJECT TO CHANGE*</strong></p>
<p>Survival Game<br />look at item to pick up 'e' for third person and click for third [No nearby]<br />normal weapons with bullet drop bullet travel time<br />snipers but rare [Maybe special]</p>
<p>Abilities selectable at spawn<br /> a max 20 credit slider where you can spend them on traits<br /> Stamina -&gt; run for longer distances<br /> Strength -&gt; carry more weight<br /> Vitality -&gt; Have more base health<br /> Stealth -&gt; Approximate location on map is bigger</p>
<p>More weight slows player some<br />Backpacks -&gt; Add slots but not weight</p>
<p>Oddball style game<br /> Map that shows the relitive area of the top player</p>
<p>spawn with classes<br /> 2 mags<br /> no attachments<br /> unlock guns with experience</p>
<p>no health regen<br />final hit headshots = 20 credits<br />final hit bodyshots = 10 credits</p>
<p>classes require credits to spawn with better stuff<br />inventory and credits are transferrable between servers and sessions<br />combat loggging - if leave in combat start from scratch<br />one dynamicly roaming entity of the night ( Impossible to kill, when near heart starts pumping and vinegrette )<br /> goes after people possible to get away<br />Dyanmic day and night cycle<br />Dynamic weather ( rain, fog, thunder, lightning )<br />floods that cause roaring rivers to fill that cannot be swam<br />Fires that char trees(no leaves), regrows in 3ish days<br />Master leaderboard in the main menu of top players per rank<br />Ranked lobby ( Disabled until player base )<br />small towns around a main centralized area( ie city or temple )<br />large servers<br />random spawned skin boxes that require credits to open<br />purchasable skins<br />bullet penatration on certain materials<br />bullet reflection on certain materials<br />No kill leader until you get at least 2 kills minimum</p>
<p>Tournament mode<br /> all players spawn at the same time<br /> hold oddball for 30mins total</p>
<p>server quits introducing people into game after 5 hrs. (last man standing mode)<br /> last person in server wins<br /> on death quit to new server<br /> everyone becomes oddball<br /> less players alive equal less oddball area</p>
</div>
</div>
@@ -0,0 +1,19 @@
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: 'project-mist',
templateUrl: './mist.component.html',
imports: [ FormsModule, CommonModule ]
})
export class MistComponent {
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title ) {
this.title.setTitle("Mist | Mistox");
};
}
@@ -0,0 +1,50 @@
<div class="center">
<div class="column-content">
<form class="big-frame background-border" style="width: 520px;" #accountForm="ngForm" (ngSubmit)="onSubmit()">
<h3>Edit Item</h3>
<div class="frame-item">
<input type="text" [(ngModel)]="newItem.name" name="ItemName" placeholder=" " />
<label>Item Name</label>
</div>
<div class="frame-item">
<input type="number" [(ngModel)]="newItem.cost" name="ItemCost" placeholder=" " />
<label>Cost</label>
</div>
<div class="frame-item">
<input type="text" [(ngModel)]="newItem.url" name="itemURL" placeholder=" " />
<label>URL</label>
</div>
<div class="frame-item">
<textarea [(ngModel)]="newItem.description" name="ItemDesc" cols="40" placeholder=" " ></textarea>
<label>Description</label>
</div>
<div class="frame-item">
<div id="FileUploadPlaceholder" ></div>
<input #FileUpload type="file" (change)="onFileSelected($event)" accept="image/*" multiple />
</div>
<div class="flex-row">
<div class="frame-button">
<input class="submit" type="submit" value="Update Item" />
</div>
</div>
<ul *ngIf="errorMsgs.length > 0" >
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
</ul>
</form>
<!-- Finish file preview for uploads -->
<div class="img-frame">
<div *ngFor="let cur of imagePreviews" class="image-holder">
<input class="delete-button" type="button" value="X" (click)="RemovePhoto(cur)" />
<img [src]="cur" alt="Image Preview" style="max-width: 200px;"/>
</div>
</div>
</div>
</div>
@@ -0,0 +1,128 @@
import { Component, ElementRef, ViewChild } 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';
import { Product } from 'app/models/Product';
@Component({
selector: 'item-edit',
templateUrl: './edit.component.html',
imports: [ FormsModule, CommonModule ]
})
export class EditItemComponent {
@ViewChild('FileUpload') InputDOM!: ElementRef<HTMLInputElement>;
readonly maxFileMB = 16;
newItem: Product = new Product();
errorMsgs: string[] = [];
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Edit | ADMIN");
this.route.queryParams.subscribe(params => {
this.newItem.id = params['ProductID'] || '';
});
// If user is not logged in -> route home
if (!auth.isLoggedIn){
router.navigate(["/"]);
}
// If user is not Admin -> route home
if (auth.loggedInUser.siteData.role != "Admin"){
router.navigate(["/"]);
}
// Load product
const formData = new FormData();
formData.append("productID", this.newItem.id.toString());
this.http.post<Product>( "api/product/get", formData ).subscribe({
next: async (data) => {
this.newItem = data;
this.newItem.images.forEach(img => {
http.get("api/productimage/get?ProductID=" + img.productID + "&ImageID=" + img.imageID, { responseType: 'blob' }).subscribe(blob => {
img.imageSrc = URL.createObjectURL(blob);
this.imagePreviews.push(img.imageSrc);
this.selectedFiles.push(new File([blob], "EmptyName", {type: "image/jpeg"}));
});
});
},
error: err => {
console.log("Err loading product: ", err);
}
});
};
sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
selectedFiles: File[] = [];
imagePreviews: string[] = [];
onFileSelected(event: Event){
const fileInput = event.target as HTMLInputElement;
if (!fileInput.files?.length){
return;
}
for (let i=0; i<fileInput.files.length; i++){
let file = fileInput.files[i];
if (file.size > this.maxFileMB * 1024 * 1024){
this.errorMsgs.push("File exceeds max file size of 16MB");
continue;
}
// No issues add file to the list
this.selectedFiles.push( file );
const reader = new FileReader();
reader.onload= () => {
this.imagePreviews.push(reader.result as string);
}
reader.readAsDataURL(file);
this.InputDOM.nativeElement.value = '';
}
}
RemovePhoto(imagePreview: string){
let i = this.imagePreviews.indexOf(imagePreview);
this.imagePreviews.splice(i, 1);
this.selectedFiles.splice(i, 1);
}
onSubmit(){
const formData = new FormData();
// Append non-file fields
formData.append("Name", this.newItem.name);
formData.append("Description", this.newItem.description);
formData.append("Cost", this.newItem.cost.toString());
formData.append("Url", this.newItem.url);
// Add image fileds
if (this.selectedFiles.length > 0){
for(let i=0; i<this.selectedFiles.length; i++){
formData.append("images", this.selectedFiles[i], this.selectedFiles[i].name);
};
}
// Proccess data
this.http.post<boolean>( "api/product/create", formData ).subscribe({
next: async (data) => {
if (data == true){
this.errorMsgs = ["Product Created Successfully"];
await this.sleep(3000);
this.router.navigate(["/catalog"]);
}else{
this.errorMsgs = ["Error has ocurred"];
}
},
error: err => {
console.log("New Product Err: ", err);
}
});
}
}
@@ -0,0 +1,50 @@
<div class="center">
<div class="column-content">
<form class="big-frame background-border" style="width: 520px;" #accountForm="ngForm" (ngSubmit)="onSubmit()">
<h3>Create New Item</h3>
<div class="frame-item">
<input type="text" [(ngModel)]="newItem.name" name="ItemName" placeholder=" " />
<label>Item Name</label>
</div>
<div class="frame-item">
<input type="number" [(ngModel)]="newItem.cost" name="ItemCost" placeholder=" " />
<label>Cost</label>
</div>
<div class="frame-item">
<input type="text" [(ngModel)]="newItem.url" name="itemURL" placeholder=" " />
<label>URL</label>
</div>
<div class="frame-item">
<textarea [(ngModel)]="newItem.description" name="ItemDesc" cols="40" placeholder=" " ></textarea>
<label>Description</label>
</div>
<div class="frame-item">
<!-- Need to fix for image file upload -->
<div id="FileUploadPlaceholder" ></div>
<input type="file" (change)="onFileSelected($event)" accept="image/*" multiple />
</div>
<div class="flex-row">
<div class="frame-button">
<input class="submit" type="submit" value="Create Item" />
</div>
</div>
<ul *ngIf="errorMsgs.length > 0" >
<li *ngFor="let msg of errorMsgs" >{{ msg }}</li>
</ul>
</form>
<!-- Finish file preview for uploads -->
<div class="img-frame">
<div *ngFor="let cur of imagePreviews" class="image-holder">
<img [src]="cur" alt="Image Preview" style="max-width: 200px;"/>
</div>
</div>
</div>
</div>
@@ -0,0 +1,101 @@
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';
import { Product } from 'app/models/Product';
@Component({
selector: 'item-new',
templateUrl: './new.component.html',
imports: [ FormsModule, CommonModule ]
})
export class NewItemComponent {
readonly maxFileMB = 16;
newItem: Product = new Product();
errorMsgs: string[] = [];
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("New | ADMIN");
// If user is not logged in -> route home
if (!auth.isLoggedIn){
router.navigate(["/"]);
}
// If user is not Admin -> route home
if (auth.loggedInUser.siteData.role != "Admin"){
router.navigate(["/"]);
}
};
sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
selectedFiles: File[] = [];
imagePreviews: string[] = [];
onFileSelected(event: Event){
const fileInput = event.target as HTMLInputElement;
this.imagePreviews = [];
this.selectedFiles = [];
if (!fileInput.files?.length){
return;
}
for (let i=0; i<fileInput.files.length; i++){
let file = fileInput.files[i];
if (file.size > this.maxFileMB * 1024 * 1024){
this.errorMsgs.push("File exceeds max file size of 16MB");
continue;
}
// No issues add file to the list
this.selectedFiles.push( file );
const reader = new FileReader();
reader.onload= () => {
this.imagePreviews.push(reader.result as string);
}
reader.readAsDataURL(file);
}
}
onSubmit(){
const formData = new FormData();
// Append non-file fields
formData.append("Name", this.newItem.name);
formData.append("Description", this.newItem.description);
formData.append("Cost", this.newItem.cost.toString());
formData.append("Url", this.newItem.url);
// Add image fileds
if (this.selectedFiles.length > 0){
for(let i=0; i<this.selectedFiles.length; i++){
formData.append("images", this.selectedFiles[i], this.selectedFiles[i].name);
};
}
// Proccess data
this.http.post<boolean>( "api/product/create", formData ).subscribe({
next: async (data) => {
if (data == true){
this.errorMsgs = ["Product Created Successfully"];
await this.sleep(3000);
this.router.navigate(["store/catalog"]);
}else{
this.errorMsgs = ["Error has ocurred"];
}
},
error: err => {
console.log("New Product Err: ", err);
}
});
}
}
@@ -0,0 +1,145 @@
.gameCard {
position: relative;
background-color: var(--Mistox-Black);
float: left;
box-sizing: border-box;
margin: 0;
padding: 0;
width: 100%;
border-radius: 10px;
break-inside: avoid;
margin-bottom: 2rem;
border: solid 2px var(--Mistox-Background);
transition-duration: 1s;
}
.gameCard :hover{
border-color: var(--Mistox-Light);
}
.gameCard-Name {
width: 100%;
text-align: left;
font-size: 25px;
padding: 5px 0 0 5px;
background-color: rgba(0,0,0,.1);
}
.gameCard-Grid {
column-count: 4;
column-gap: 2rem;
padding-top: 20px;
width: calc(100% - 40px);
margin-left: 20px;
}
@media (max-width: 1400px) {
.gameCard-Grid {
column-count: 3;
padding-top: 20px;
width: calc(100% - 40px);
margin-left: 20px;
}
}
@media (max-width: 1100px) {
.gameCard-Grid {
column-count: 2;
padding-top: 20px;
width: calc(100% - 40px);
margin-left: 20px;
}
}
@media (max-width: 900px) {
.gameCard-Grid {
column-count: 1;
padding-top: 20px;
width: calc(100% - 40px);
margin-left: 20px;
}
}
.gameCard-Img {
width: 100%;
border-radius: 10px 10px 0 0;
}
.gameCard-Next,
.gameCard-Prev {
background-color: transparent;
color: var(--Mistox-White);
padding: 16px;
margin-top: -22px;
font-size: 18px;
font-weight: bold;
border: none;
transition: background-color 0.6s ease;
}
.gameCard-Next:hover,
.gameCard-Prev:hover {
background-color: rgba(0, 0, 0, 0.5);
}
.gameCard-Prev {
position: absolute;
top: 50%;
}
.gameCard-Next {
position: absolute;
top: 50%;
right: 0;
}
.gameCard-Desc {
font-size: 13px;
margin: 5px;
color: var(--Mistox-Light);
}
.gameCard-Price {
width: calc(50% - 10px);
float: left;
margin: 5px;
text-align: center;
margin-bottom: 10px;
}
.gameCard-Button {
width: 40%;
margin: 5px 5%;
height: 38.4px;
color: var(--Mistox-Black);
background-color: var(--Mistox-Light);
font-size: 16px;
text-decoration: none;
text-transform: uppercase;
overflow: hidden;
transition: 0.5s;
letter-spacing: 2px;
border: 1px solid var(--Mistox-Light);
border-radius: 5px;
}
.gameCard-Button :hover{
background-color: var(--Mistox-Light);
color: var(--Mistox-White);
box-shadow: 4px 3px 6px var(--Mistox-Dark);
}
.cartopen {
position: absolute;
background: var(--Mistox-Offset);
right: 10px;
top: 55px;
width: 400px;
border-radius: 5px;
backdrop-filter: blur(3px);
border: 1px solid var(--Mistox-Light);
}
.cartclosed {
display: none;
}
@@ -0,0 +1,32 @@
<div class="gameCard-Grid">
<div class="gameCard" *ngFor="let product of Products">
<div style="position: relative;">
<div *ngIf="product.images.length > 1">
<button class="gameCard-Prev" (click)="nextImg(product)"></button>
<button class="gameCard-Next" (click)="prevImg(product)"></button>
</div>
<div *ngIf="product.images.length > 0" >
<img class="gameCard-Img" [src]="product.images[product.curShowingIMG].imageSrc">
</div>
</div>
<h1 class ="gameCard-Name">{{ product.name }}</h1>
<div *ngFor="let line of product.description.split('\n')" >
<h2 class="gameCard-Desc">{{ line }}</h2>
</div>
<h2 class="gameCard-Price">${{ (product.cost/100).toFixed(2) }}</h2>
<button class="gameCard-Button" >Add To Cart</button>
<div *ngIf="auth.loggedInUser.siteData.role == 'Admin'">
<button style="width: calc(50% - 10px); margin: 5px;" [routerLink]="['/store/admin/edit']" [queryParams]="{ ProductID: product.id }" >
Edit
</button>
<button style="width: calc(50% - 10px); margin: 5px;" (click)="DeleteItem(product.id)" >
Delete
</button>
</div>
</div>
<div *ngIf="auth.loggedInUser.siteData.role == 'Admin'">
<button style="width: calc(100% - 10px); margin: 5px;" [routerLink]="['/store/admin/new']" >
New
</button>
</div>
</div>
@@ -0,0 +1,71 @@
import { Component, NgZone } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { FormsModule, NgModel } from '@angular/forms';
import { Router, ActivatedRoute, RouterModule } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { Authentication } from '../../../services/Authentication';
import { Product } from 'app/models/Product';
@Component({
selector: 'store-catalog',
templateUrl: './catalog.component.html',
styleUrl: './catalog.component.css',
imports: [ FormsModule, CommonModule, RouterModule ]
})
export class CatalogComponent {
public Products: Product[] = [];
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Store | Mistox");
// load each product
http.post<Product[]>("api/product/getall", null).subscribe(
response => {
this.Products = response;
// Load each image
this.Products.forEach(item => {
item.curShowingIMG = 0;
item.images.forEach(img => {
http.get("api/productimage/get?ProductID=" + img.productID + "&ImageID=" + img.imageID, { responseType: 'blob' }).subscribe(blob => {
img.imageSrc = URL.createObjectURL(blob);
console.log(img.imageSrc);
});
});
});
}
)
};
nextImg( prod: Product ){
prod.curShowingIMG += 1;
if (prod.curShowingIMG == prod.images.length){
prod.curShowingIMG = 0;
}
}
prevImg( prod: Product ){
prod.curShowingIMG -= 1;
if (prod.curShowingIMG == -1){
prod.curShowingIMG = prod.images.length -1;
}
}
DeleteItem( ProductID: number ) {
const body = new HttpParams()
.set("productID", ProductID);
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded',
});
this.http.post<boolean>( "api/product/delete", body, { headers } ).subscribe({
next: data => {
if (data){
window.location.reload();
}
}
})
}
}
@@ -0,0 +1,24 @@
#payment-form {
max-width: 500px;
margin: 0 auto;
}
.form-group {
margin-bottom: 20px;
}
#card-element {
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
}
#submit {
background-color: #5469d4;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
@@ -0,0 +1,8 @@
<form id="payment-form">
<div class="form-group">
<label for="card-element">Card Details</label>
<div id="card-element"></div>
<div id="card-errors" role="alert"></div>
</div>
<button id="submit">Pay</button>
</form>
@@ -0,0 +1,62 @@
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute, RouterModule } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { Authentication } from '../../../services/Authentication';
import { loadStripe, Stripe, StripeElements } from '@stripe/stripe-js';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'store-payment',
templateUrl: './payment.component.html',
styleUrl: './payment.component.css',
imports: [ FormsModule, CommonModule, RouterModule ]
})
export class PaymentComponent {
stripe: Stripe | null = null;
elements: StripeElements | null = null;
async ngOnInit(){
let ApiKey = await firstValueFrom(this.http.get<string>("/api/payment/publickey"));
this.stripe = await loadStripe(ApiKey);
if (this.stripe){
this.elements = this.stripe?.elements();
}
}
ngAfterViewInit(){
if (this.elements){
const cardStyle = {
base: {
color: '#32325d',
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSize: '16px',
'::placeholder': {
color: '#aab7c4',
}
},
invalid: {
color: '#fa755a',
}
}
const card = this.elements.create('card', { style: cardStyle });
card.mount('#card-element');
}
}
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Payment | Mistox");
http.post<void[]>("api/product/getall", null).subscribe(
response => {
}
)
};
}
@@ -0,0 +1,84 @@
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 => {
if (data.error.length === 0){
this._user.next(data);
this.setUserToStorage(data, StayLoggedIn == true ? SessionType.Forever : SessionType.Session);
}
},
error: err => {
console.log("HTTP Error Signing In: ", err);
}
});
return sub;
}
Logout(){
this.http.post<Account>( "api/account/logout", {}, { responseType: 'json' } ).subscribe( );
this._user.next( new Account );
this.delUserFromStorage();
}
get isLoggedIn(): boolean {
return this._user.value.id != -1 ? true : false;
}
get loggedInUser(): Account {
return this._user.value;
}
private getUserFromStorage(): Account {
const foreverUser = localStorage.getItem('user');
const sessionUser = sessionStorage.getItem('user');
let user = null;
if (foreverUser != null){
user = JSON.parse(foreverUser)
} else if (sessionUser != null){
user = JSON.parse(sessionUser)
} else {
user = new Account();
user.id = -1;
}
return user;
}
private setUserToStorage(user: Account, session: SessionType): void {
if (session == SessionType.Forever){
localStorage.setItem('user', JSON.stringify(user));
}else if(session == SessionType.Session){
sessionStorage.setItem('user', JSON.stringify(user));
}
}
private delUserFromStorage(): void {
localStorage.removeItem('user');
sessionStorage.removeItem('user');
}
}
export enum SessionType {
Forever,
Session
}
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Mistox</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body style="border: 0; padding: 0; margin: 0;">
<app-root></app-root>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
+219
View File
@@ -0,0 +1,219 @@
/* 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-Offset: #443B75;
--Mistox-Background: #320000;
--Mistox-White: #FFF;
--Mistox-Black: #000;
}
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
main {
background-color: #000000;
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='%23ff0000' 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");
}
/* CSS used for the Account Activity Pages */
.center {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.column-content {
columns: 2;
}
@media (max-width: 1620px) {
.column-content {
columns: 1;
}
}
.text-frame {
margin: 50px !important;
max-width: 2200px !important;
padding: 10px !important;
width: auto !important;
}
.background-border {
border: var(--Mistox-Background) 2px solid;
border-radius: 6px;
}
.big-frame {
background-color: var(--Mistox-Black);
padding: 4px;
width: 400px;
color: var(--Mistox-White);
margin: 40px;
break-inside: avoid;
}
.big-frame h3{
margin: 15px 0 30px 0;
color: var(--Mistox-White);
text-align: center;
}
.big-frame h2{
text-align: center;
position: relative;
margin: 0;
top: -20px;
font-size: 15px;
color: var(--Mistox-Bright);
}
.big-frame .frame-item label{
position: relative;
padding: 10px 0;
font-size: 16px;
color: var(--Mistox-White);
pointer-events: none;
transition: .5s;
top: -70px;
left: 20px;
}
.big-frame .frame-item input:autofill,
.big-frame .frame-item input:-webkit-autofill,
.big-frame .frame-item input {
position: relative;
width: calc(100% - 40px);
margin: 0 20px;
padding: 10px 0;
font-size: 15px;
color: var(--Mistox-White);
margin-bottom: 30px;
border: none;
border-bottom: 1px solid var(--Mistox-White);
outline: none;
background: transparent;
}
.big-frame .frame-item input:focus ~ label,
.big-frame .frame-item input:not(:placeholder-shown) ~ label {
top: -95px;
left: 10px;
color: var(--Mistox-Light);
font-size: 12px;
}
.big-frame .frame-item textarea:autofill,
.big-frame .frame-item textarea:-webkit-autofill,
.big-frame .frame-item textarea {
position: relative;
width: calc(100% - 40px);
margin: 0 20px;
padding: 10px 0;
font-size: 15px;
color: var(--Mistox-White);
margin-bottom: 30px;
border: none;
border-bottom: 1px solid var(--Mistox-White);
outline: none;
background: transparent;
height: 80px;
}
.big-frame .frame-item textarea:focus ~ label,
.big-frame .frame-item textarea:not(:placeholder-shown) ~ label {
top: -150px;
left: 10px;
color: var(--Mistox-Light);
font-size: 12px;
}
.flex-row{
display: flex;
flex-direction: row;
justify-content: space-around;
padding-bottom: 15px;
}
.sub-frame {
text-align: center;
padding: 1px 0;
}
.sub-frame a {
text-decoration: none;
color: var(--Mistox-Light);
}
.sub-frame a :hover {
color: var(--Mistox-Bright);
}
.img-frame {
width: 610px;
}
.image-holder{
float: left;
border: 1px solid white;
width: fit-content;
border-radius: 6px;
}
.delete-button {
position: absolute;
width: 20px;
height: 20px;
text-align: center;
color: #000;
background-color: red;
border: none;
border-radius: 4px;
transform: translate(180px, 0);
}
.delete-button:hover{
background-color: #890620;
}
.submit{
position: relative;
padding: 10px 20px;
color: var(--Mistox-Black);
background-color: var(--Mistox-Light);
font-size: 16px;
text-decoration: none;
text-transform: uppercase;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.5s ease;
letter-spacing: 4px;
border: 1px solid var(--Mistox-Light);
margin: auto;
border-radius: 5px;
}
.submit:hover {
background-color: var(--Mistox-Light);
color: var(--Mistox-White);
box-shadow: 4px 3px 6px var(--Mistox-Dark);
}
.submit:active {
transform: translate( 4px, 2px );
background-color: var(--Mistox-Dark);
border: none;
color: var(--Mistox-White);
box-shadow: none;
}
ul {
list-style: none;
color: var(--Mistox-Bright);
}
+15
View File
@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
]
}
+35
View File
@@ -0,0 +1,35 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve",
"baseUrl": "src",
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+14
View File
@@ -0,0 +1,14 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}
+285
View File
@@ -0,0 +1,285 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using BoredCareers.Services;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
namespace BoredCareers.Controllers {
[ApiController]
[Route("api/account/[controller]")]
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 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()) {
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 true;
}
}
return false;
} catch {
return false;
}
}
[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 "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();
}
}
[Route("logout")]
[HttpPost]
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()) {
Account user = await getLoggedInUser();
if (BCrypt.Net.BCrypt.Verify(Password, user.PasswordHash)) {
await _databaseService.DeleteAccount(user.ID);
return true;
}
}
return false;
} catch {
return false;
}
}
}
}
+69
View File
@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Entities;
using BoredCareers.Services.DatabaseService;
namespace BoredCareers.Controllers {
[ApiController]
[Route("api/cart/[controller]")]
public class CartController : MistoxControllerBase {
CartController(DatabaseService db) : base(db) { }
[Route("get")]
[HttpPost]
public async Task<ActionResult<Cart[]>> GetCart() {
try {
if (isLoggedIn()) {
return Ok(await _databaseService.GetCart(getLoggedInUserID()));
}
return StatusCode(500);
} catch {
return StatusCode(500);
}
}
[Route("add")]
[HttpPost]
public async Task<IActionResult> AddCart([FromBody] Cart cart) {
try {
if (isLoggedIn()) {
cart.AccountID = getLoggedInUserID();
await _databaseService.AddToCart(cart);
return Ok();
}
return StatusCode(500);
} catch {
return StatusCode(500);
}
}
[Route("remove")]
[HttpPost]
public async Task<IActionResult> RemoveCart([FromBody] Cart cart) {
try {
if (isLoggedIn()) {
cart.AccountID = getLoggedInUserID();
await _databaseService.RemoveFromCart(cart);
return Ok();
}
return StatusCode(500);
} catch {
return StatusCode(500);
}
}
[Route("clear")]
[HttpPost]
public async Task<IActionResult> ClearCart() {
try {
if (isLoggedIn()) {
await _databaseService.ClearCart(getLoggedInUserID());
return Ok();
}
return StatusCode(500);
} catch {
return StatusCode(500);
}
}
}
}
@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Entities;
using BoredCareers.Services.DatabaseService;
namespace BoredCareers.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.FindFirst("ID")?.Value);
}
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;
}
}
}
+68
View File
@@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Controllers.Payment;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
namespace BoredCareers.Controllers {
[ApiController]
[Route("api/payment/[controller]")]
public class PaymentController : MistoxControllerBase {
IPayment _paymentService;
public PaymentController(DatabaseService db) : base(db) {
if (IPayment._PaymentType == PaymentType.StripeIntent) {
_paymentService = new StripeIntent(_databaseService);
} else {
// Fallback
_paymentService = new StripeIntent(_databaseService);
}
// Add new payment plugins here
}
[Route("getcheckouttoken")]
[HttpPost]
public async Task<string> GetCheckoutToken() {
string OrderNumber = Guid.NewGuid().ToString().Substring(0, 10);
if (isLoggedIn()) {
Cart[] carts = await _databaseService.GetCart(getLoggedInUserID());
(bool, string) PaymentResponse = await _paymentService.TryGetCheckoutToken(OrderNumber, getLoggedInUserID(), carts);
if (PaymentResponse.Item1) {
// Returns client secret
return PaymentResponse.Item2;
} else {
Console.WriteLine("An error has occured in the payment plugin\n\n");
Console.WriteLine(PaymentResponse.Item2);
Console.WriteLine("\n");
return "An error has occured in the payment plugin";
}
}
return "You must be logged in";
}
[Route("getpublickey")]
[HttpPost]
public IActionResult GetPublicKey() {
try {
return Ok(IPayment._PublicKey);
} catch (Exception ex) {
return NotFound(ex.ToString());
}
}
[Route("response")]
[HttpPost]
public async Task<IActionResult> paymentWebhook() {
try {
string body = await new StreamReader(Request.Body).ReadToEndAsync();
await _paymentService.ValidatePurchase(body, Request.Headers["Stripe-Signature"].ToString());
return Ok();
} catch (Exception ex) {
return NotFound(ex.ToString());
}
}
}
}
@@ -0,0 +1,20 @@
using BoredCareers.Entities;
namespace BoredCareers.Controllers.Payment {
public interface IPayment {
public static PaymentType _PaymentType;
public static string _EndpointSecret = "";
public static string _PublicKey = "";
public Task<(bool, string)> TryGetCheckoutToken(string OrderNumber, int userID, Cart[] cart);
public Task ValidatePurchase(string WebHookData, string Headers);
}
public enum PaymentType {
StripeIntent
}
}
@@ -0,0 +1,139 @@
using BoredCareers.Controllers.Payment;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
namespace BoredCareers.Controllers {
public class StripeIntent : IPayment {
DatabaseService _databaseService;
public StripeIntent(DatabaseService databaseService) {
_databaseService = databaseService;
}
public async Task<(bool, string)> TryGetCheckoutToken(string OrderNumber, int userID, Cart[] cart) {
try {
// build Recipt and calculate Tax
var options = new Stripe.Tax.CalculationCreateOptions {
Currency = "usd",
CustomerDetails = new Stripe.Tax.CalculationCustomerDetailsOptions {
AddressSource = "billing",
},
Expand = new List<string>() { "line_items" },
LineItems = new List<Stripe.Tax.CalculationLineItemOptions>()
};
List<int> prods = new List<int>();
// Add items to receipt
int subtotal = 0;
foreach (Cart items in cart) {
Product? product = await _databaseService.GetProduct(items.ProductID);
if (product != null) {
prods.Add(product.ID);
if (product != null) {
subtotal += product.Cost;
options.LineItems.Add(new Stripe.Tax.CalculationLineItemOptions {
Amount = product.Cost,
TaxCode = "txcd_10201000", // Tax code for downloadable digital games
Quantity = 1,
Reference = product.Name,
TaxBehavior = "exclusive"
});
}
}
}
var service = new Stripe.Tax.CalculationService();
Stripe.Tax.Calculation result = service.Create(options);
string csv = "";
foreach (int cur in prods) {
csv = csv + cur + ",";
}
// Crate Payment Intent
Stripe.PaymentIntentCreateOptions paymentIntent = new Stripe.PaymentIntentCreateOptions() {
Amount = result.AmountTotal,
Currency = "usd",
Metadata = new Dictionary<string, string> {
{ "ordernumber", OrderNumber },
{ "user", userID.ToString() },
{ "products", csv },
{ "subtotal", subtotal.ToString() },
{ "total", result.AmountTotal.ToString() }
},
StatementDescriptor = "Mistox.Net #" + OrderNumber
};
Stripe.PaymentIntentService intentService = new Stripe.PaymentIntentService();
Stripe.PaymentIntent x = await intentService.CreateAsync(paymentIntent);
return (true, x.ClientSecret);
} catch (Exception e) {
return (false, e.ToString());
}
}
public async Task ValidatePurchase(string WebHookData, string Headers) {
Stripe.Event e = Stripe.EventUtility.ConstructEvent( WebHookData, Headers, IPayment._EndpointSecret );
if (e.Type == "payment_intent.succeeded") {
// Extract Data from payment confirm
Stripe.PaymentIntent intent = (Stripe.PaymentIntent)e.Data.Object;
string orderNumber = "";
int userID = 0;
List<int> productIDs = new List<int>();
int subtotal = 0;
int total = 0;
KeyValuePair<string, string>[] y = intent.Metadata.ToArray();
foreach (KeyValuePair<string, string> cur in y) {
string val = cur.Key;
if (val == "ordernumber") {
orderNumber = cur.Value;
}
else if (val == "user") {
userID = int.Parse(cur.Value);
}
else if (val == "products") {
string[] products = cur.Value.Split(',');
foreach (string product in products) {
if (!string.IsNullOrEmpty(product)) {
productIDs.Add(Convert.ToInt32(product));
}
}
}
else if (val == "subtotal") {
subtotal = int.Parse(cur.Value);
}
else if (val == "total") {
total = int.Parse(cur.Value);
}
}
// Clear the cart
await _databaseService.ClearCart(userID);
// Add data to misox receipt
for (int i = 0; i < productIDs.Count; i++) {
int product = productIDs[i];
await _databaseService.NewReceipt(new Receipt {
AccountID = userID,
ProductID = product,
ReceiptID = orderNumber,
Time = DateTime.Now,
TaxAmount = total - subtotal,
TotalCost = total,
LineItem = i
});
}
} else {
Console.WriteLine("Unhandled event type: {0}", e.Type);
}
}
}
}
+143
View File
@@ -0,0 +1,143 @@
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
namespace BoredCareers.Controllers {
[ApiController]
[Route("api/product/[controller]")]
public class ProductController : MistoxControllerBase {
public ProductController(DatabaseService db) : base(db) { }
[Route("set")]
[HttpPost]
public async Task<ActionResult<bool>> CreateProduct([FromForm] Product obj, [FromForm] IFormFile[] images) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
if (user.Role == "Admin") {
List<ProductImage> building = new List<ProductImage>();
foreach (var file in images) {
using (var stream = new MemoryStream()) {
await file.CopyToAsync(stream);
var bytes = stream.ToArray();
// Convert to your image model or whatever your logic is
ProductImage img = new ProductImage { Image = bytes, Name = file.FileName };
building.Add(img);
}
}
obj.Images = building.ToArray();
await _databaseService.SetProduct(obj);
return true;
}
}
return false;
} catch (Exception e) {
Console.WriteLine(e);
return false;
}
}
[Route("get")]
[HttpPost]
public async Task<ActionResult<Product>> GetProduct([FromForm] int productID) {
try {
Product? product = await _databaseService.GetProduct(productID);
if (product != null) {
return product;
}
else {
return NotFound();
}
} catch {
return NotFound();
}
}
[Route("getall")]
[HttpPost]
public async Task<Product[]> GetAllProducts() {
try {
return await _databaseService.GetAllProducts();
} catch {
return Array.Empty<Product>();
}
}
[Route("delete")]
[HttpPost]
public async Task<ActionResult<bool>> DeleteProduct([FromForm] int productID) {
try {
if (isLoggedIn()) {
Account user = await getLoggedInUser();
if (user.Role == "Admin") {
await _databaseService.DeleteProduct(productID);
return true;
}
}
return false;
} catch {
return false;
}
}
[Route("getimage")]
[HttpPost]
public async Task<IActionResult> GetProductImage([FromForm] int ProductID, [FromForm] int ImageID) {
try {
ProductImage? img = await _databaseService.GetImage(ProductID, ImageID);
if (img != null) {
return File(img.Image, "Image/*");
}
else {
return NotFound();
}
} catch {
return NotFound();
}
}
[Route("getowned")]
[HttpPost]
public async Task<ActionResult<Receipt[]>> GetOwnedProduct() {
try {
if (isLoggedIn()) {
Receipt[] returned = await _databaseService.GetAllReceipts(getLoggedInUserID());
return returned;
}
return new Receipt[0];
} catch {
return new Receipt[0];
}
}
[Route("download")]
[HttpGet]
public async Task<ActionResult> Download([FromQuery] string Product) {
try {
if (isLoggedIn()) {
Product[] games = await _databaseService.GetAllProducts();
foreach (Product product in games) {
if (contains(Product, product.URL)) {
Receipt? receipt = await _databaseService.GetReceipt(getLoggedInUserID(), product.ID);
if (receipt != null) {
//FileStream fileStream = new FileStream(_FolderRoot + Product, FileMode.Open, FileAccess.Read);
//return new FileStreamResult( fileStream, "application/octet-stream" ) {
// FileDownloadName = fileStream.Name
//};
}
break;
}
}
return Unauthorized();
}
return Unauthorized();
} catch {
return NotFound();
}
}
}
}
+59
View File
@@ -0,0 +1,59 @@
// Reflections of SQL Database objects
namespace BoredCareers.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 Error { get; set; } = "";
}
public class Product {
public int ID { get; set; } // PK
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public ProductImage[] Images { get; set; } = [];
public int Cost { get; set; }
public string URL { get; set; } = "";
}
public class ProductImage {
public int ImageID { get; set; } // PK
public int ProductID { get; set; } // PK
public byte[] Image { get; set; } = Array.Empty<byte>();
public string Name { get; set; } = "";
}
public class ProductInventory {
public int AccountID { get; set; } // PK
public int ProductID { get; set; } // PK
public string Key { get; set; } = string.Empty; // PK
public string Value { get; set; } = string.Empty;
}
public class Cart {
public int ID { get; set; } // PK
public int AccountID { get; set; }
public int ProductID { get; set; }
}
public class Receipt {
public int AccountID { get; set; } // PK
public int ProductID { get; set; } // PK
public string ReceiptID { get; set; } = string.Empty; // PK
public int LineItem { get; set; }
public DateTime Time { get; set; }
public int TaxAmount { get; set; }
public int TotalCost { get; set; }
}
}
+120
View File
@@ -0,0 +1,120 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using BoredCareers.Controllers.Payment;
using BoredCareers.Services;
using BoredCareers.Services.DatabaseService;
using Stripe;
var builder = WebApplication.CreateBuilder(args);
// Disable null warnings becuse string.IsNullOrEmpty checks for NULL or Empty
#pragma warning disable CS8600
#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 : "mistox";
// 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 ) );
////////////////////////////////
///////// 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 ));
////////////////////////////////
/////// Payment Service ////////
////////////////////////////////
// Payment service name -> must be name of PaymentType enum
string? PaymentService = Environment.GetEnvironmentVariable("PaymentService");
IPayment._PaymentType = (PaymentType)Enum.Parse(typeof(PaymentType), PaymentService, true);
if (IPayment._PaymentType == PaymentType.StripeIntent) {
// Get PublicKey
string? StripePublicKey = Environment.GetEnvironmentVariable("StripePublicKey");
IPayment._PublicKey = string.IsNullOrEmpty(StripePublicKey) ? "" : StripePublicKey;
// Get PrivateKey
string? StripeAPIKey = Environment.GetEnvironmentVariable("StripeApiKey");
StripeConfiguration.ApiKey = StripeAPIKey;
// Get Endpoint secret
string? StripeEndpointKey = Environment.GetEnvironmentVariable("StripeEndpointSecret");
IPayment._EndpointSecret = string.IsNullOrEmpty(StripeEndpointKey) ? "" : StripeEndpointKey;
}
// Authentication Service
builder.Services.AddAuthentication( options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
} ).AddCookie(options => {
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.LoginPath = "/account/login";
options.LogoutPath = "/account/logout";
options.SlidingExpiration = true;
});
builder.Services.AddCors( o => o.AddDefaultPolicy( builder => {
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); // No CORS
} ) );
// 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();
app.MapFallbackToFile("index.html");
app.Run();
+27
View File
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.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.Mvc.WebApiCompatShim" Version="2.3.0" />
<PackageReference Include="MySql.Data" Version="9.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Stripe.net" Version="48.2.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
+160
View File
@@ -0,0 +1,160 @@
using BoredCareers.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
namespace BoredCareers.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();
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
using BoredCareers.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService {
public async Task<Cart[]> GetCart( int accountID ) {
List<Cart> list = new List<Cart>();
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT * FROM Cart
WHERE AccountID = @AccountID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@AccountID", accountID);
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
while( await reader.ReadAsync() ) {
if( reader == null ) {
break;
}
int _id = reader.GetInt32("ID");
int _accountid = reader.GetInt32("AccountID");
int _productid = reader.GetInt32("ProductID");
list.Add( new Cart() {
ID = _id,
AccountID = _accountid,
ProductID = _productid
} );
}
}
}
return list.ToArray();
}
public async Task AddToCart( Cart item ) {
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
INSERT INTO Cart
(AccountID, ProductID)
VALUES
(@AccountID, @ProductID);
";
MySqlCommand cmd = new MySqlCommand( command , connection);
cmd.Parameters.AddWithValue("@AccountID", item.AccountID);
cmd.Parameters.AddWithValue("@ProductID", item.ProductID);
await cmd.ExecuteNonQueryAsync();
}
}
public async Task RemoveFromCart( Cart item ) {
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = "DELETE FROM Cart WHERE AccountID=" + item.AccountID + " AND ProductID=" + item.ProductID + ";";
MySqlCommand cmd = new MySqlCommand( command , connection);
await cmd.ExecuteNonQueryAsync();
}
}
public async Task ClearCart( int accountID ) {
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
DELETE FROM Cart
WHERE AccountID = @AccountID;
";
MySqlCommand cmd = new MySqlCommand( command , connection);
cmd.Parameters.AddWithValue("@AccountID", accountID);
await cmd.ExecuteNonQueryAsync();
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
using MySql.Data.MySqlClient;
namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService {
public string ConnectionString {
get; set;
}
public DatabaseService( string connectionString ) {
ConnectionString = connectionString;
}
MySqlConnection GetConnection() {
return new MySqlConnection( ConnectionString );
}
}
}
+136
View File
@@ -0,0 +1,136 @@
using BoredCareers.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService {
public async Task<Product?> GetProduct(int ID) {
Product? items = null;
using (MySqlConnection connection = GetConnection()) {
connection.Open();
string command = @"
SELECT * FROM Product
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 _name = reader.GetString("Name");
string _description = reader.GetString("Description");
int _cost = reader.GetInt32("Cost");
string _url = reader.GetString("URL");
ProductImage[] images = await GetAllImages(_id);
items = new Product() {
ID = _id,
Name = _name,
Images = images,
Description = _description,
Cost = _cost,
URL = _url
};
}
}
}
return items;
}
public async Task<Product[]> GetAllProducts() {
List<Product> items = new List<Product>();
using (MySqlConnection connection = GetConnection()) {
connection.Open();
MySqlCommand cmd = new MySqlCommand("SELECT * FROM Product", connection);
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
if (reader == null) {
break;
}
int _id = reader.GetInt32("ID");
string _name = reader.GetString("Name");
string _description = reader.GetString("Description");
int _cost = reader.GetInt32("Cost");
string _url = reader.GetString("URL");
ProductImage[] images = await GetAllImages(_id);
items.Add(new Product() {
ID = _id,
Name = _name,
Images = images,
Description = _description,
Cost = _cost,
URL = _url
});
}
}
}
return items.ToArray();
}
public async Task SetProduct(Product Item) {
using (MySqlConnection connection = GetConnection()) {
connection.Open();
string command = @"
INSERT INTO Product
(ID,Name,Description,Cost,URL)
VALUES
(@ID,@Name,@Description,@Cost,@URL)
ON DUPLICATE KEY UPDATE
Name = @Name,
Description = @Description,
Cost = @Cost,
URL = @URL
WHERE ID = @ID;
SELECT ID FROM Product
WHERE Name = @Name;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ID", Item.ID);
cmd.Parameters.AddWithValue("@Name", Item.Name);
cmd.Parameters.AddWithValue("@Description", Item.Description);
cmd.Parameters.AddWithValue("@Cost", Item.Cost);
cmd.Parameters.AddWithValue("@URL", Item.URL);
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
if (reader == null) {
break;
}
Item.ID = reader.GetInt32("ID");
}
}
await AddAllImages(Item);
}
}
public async Task DeleteProduct(int ID) {
using (MySqlConnection connection = GetConnection()) {
await DeleteAllImages(ID);
connection.Open();
string command = @"
DELETE FROM Product
WHERE ID = @ID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ID", ID);
await cmd.ExecuteNonQueryAsync();
}
}
}
}
@@ -0,0 +1,112 @@
using BoredCareers.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService {
public async Task<ProductImage?> GetImage(int ProductID, int ImageID) {
ProductImage? item = null;
using (MySqlConnection connection = GetConnection()) {
connection.Open();
string command = @"
SELECT * FROM ProductImage
WHERE ProductID = @ProductID AND ImageID = @ImageID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ProductID", ProductID);
cmd.Parameters.AddWithValue("@ImageID", ImageID);
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
if (reader == null) {
break;
}
int _ImageID = reader.GetInt32("ImageID");
int _ProductID = reader.GetInt32("ProductID");
byte[] _Image = (byte[])reader["Image"];
string _Name = reader.GetString("Name");
item = new ProductImage() {
ImageID = _ImageID,
ProductID = _ProductID,
Image = _Image,
Name = _Name
};
break;
}
}
}
return item;
}
public async Task<ProductImage[]> GetAllImages(int ProductID) {
List<ProductImage> items = new List<ProductImage>();
using (MySqlConnection connection = GetConnection()) {
connection.Open();
string command = @"
SELECT * FROM ProductImage
WHERE ProductID = @ProductID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ProductID", ProductID);
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
if (reader == null) {
break;
}
int _ImageID = reader.GetInt32("ImageID");
int _ProductID = reader.GetInt32("ProductID");
string _Name = reader.GetString("Name");
items.Add(new ProductImage() {
ImageID = _ImageID,
ProductID = _ProductID,
Name = _Name
});
}
}
}
return items.ToArray();
}
public async Task AddAllImages(Product Item) {
using (MySqlConnection connection = GetConnection()) {
connection.Open();
foreach (ProductImage cur in Item.Images) {
if (cur.Image != null) {
string command = @"
INSERT INTO ProductImage
(ProductID, Image, Name)
VALUES
(@ProductID, @Image, @Name);
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ProductID", Item.ID);
cmd.Parameters.AddWithValue("@Image", cur.Image );
cmd.Parameters.AddWithValue("@Name", cur.Name );
await cmd.ExecuteNonQueryAsync();
}
}
}
}
public async Task DeleteAllImages(int ItemID) {
using (MySqlConnection connection = GetConnection()) {
connection.Open();
string command = @"
DELETE FROM ProductImage
WHERE ProductID = @ProductID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ProductID", ItemID);
await cmd.ExecuteNonQueryAsync();
}
}
}
}
+100
View File
@@ -0,0 +1,100 @@
using BoredCareers.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService {
public async Task<ProductInventory[]> GetAllProductInventory( int accountID, int productID ) {
List<ProductInventory> list = new List<ProductInventory>();
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT * FROM ProductInventory
WHERE AccountID = @AccountID AND ProductID = @ProductID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@AccountID", accountID);
cmd.Parameters.AddWithValue("@ProductID", productID);
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
while( await reader.ReadAsync() ) {
if( reader == null ) {
break;
}
string _Key = reader.GetString("Key");
string _Value = reader.GetString("Value");
list.Add( new ProductInventory() {
AccountID = accountID,
ProductID = productID,
Key = _Key,
Value = _Value
} );
}
}
}
return list.ToArray();
}
public async Task<ProductInventory> GetProductInventory( int accountID, int productID, string Key ) {
ProductInventory item = new ProductInventory();
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT * FROM ProductInventory
WHERE AccountID = @AccountID AND ProductID = @ProductID AND Key = @Key;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@AccountID", accountID);
cmd.Parameters.AddWithValue("@ProductID", productID);
cmd.Parameters.AddWithValue("@Key", Key);
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
if (reader == null) {
break;
}
string _Key = reader.GetString("Key");
string _Value = reader.GetString("Value");
item = new ProductInventory() {
AccountID = accountID,
ProductID = productID,
Key = _Key,
Value = _Value
};
}
}
}
return item;
}
async Task SetProductInventory(ProductInventory item) {
using (MySqlConnection connection = GetConnection()) {
string command = @"
INSERT INTO ProductInventory
(AccountID, ProductID, `Key`, `Value`)
Values
(@AccountID, @ProductID, @Key, @Value)
ON DUPLICATE KEY UPDATE
`Value` = @Value;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@AccountID", item.AccountID);
cmd.Parameters.AddWithValue("@ProductID", item.ProductID);
cmd.Parameters.AddWithValue("@Key", item.Key);
cmd.Parameters.AddWithValue("@Value", item.Value ?? (object)DBNull.Value);
await cmd.ExecuteNonQueryAsync();
}
}
}
}
+171
View File
@@ -0,0 +1,171 @@
using BoredCareers.Entities;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService {
public async Task<Receipt[]> GetAllReceipts( int accountID ) {
List<Receipt> receipts = new List<Receipt> ();
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT * FROM Receipt
WHERE AccountID = @AccountID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@AccountID", accountID);
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
while( await reader.ReadAsync() ) {
if( reader == null ) {
break;
}
int _accountid = reader.GetInt32("AccountID");
int _gameid = reader.GetInt32("ProductID");
string _receiptid = reader.GetString("ReceiptID");
int _lineitem = reader.GetInt32("LineItem");
DateTime _receiptdate = reader.GetDateTime("Time");
int _taxamount = reader.GetInt32("TaxAmount");
int _totalcost = reader.GetInt32("TotalCost");
receipts.Add( new Receipt() {
AccountID = _accountid,
ProductID = _gameid,
ReceiptID = _receiptid,
Time = _receiptdate,
TotalCost = _totalcost,
TaxAmount = _taxamount,
LineItem = _lineitem
} );
}
}
}
return receipts.ToArray();
}
public async Task<( Receipt, Product )[]> GetAllReceiptsJoinedToProduct( int accountID ) {
List<( Receipt, Product )> join = new();
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT * FROM Receipt
LEFT JOIN Product
ON Receipt.ProductID = Product.ID
WHERE AccountID = @AccountID
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@AccountID", accountID);
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
while( await reader.ReadAsync() ) {
if( reader == null ) {
break;
}
int _accountid = !reader.IsDBNull( "AccountID" ) ? reader.GetInt32("AccountID") : -1;
int _gameid = !reader.IsDBNull( "ProductID" ) ? reader.GetInt32("ProductID") : 0;
string _receiptid = !reader.IsDBNull( "ReceiptID" ) ? reader.GetString("ReceiptID") : "";
int _lineitem = !reader.IsDBNull( "LineItem" ) ? reader.GetInt32("LineItem") : 0;
DateTime _receiptdate = !reader.IsDBNull( "Time" ) ? reader.GetDateTime("Time") : DateTime.Now;
int _taxamount = !reader.IsDBNull( "TaxAmount" ) ? reader.GetInt32("TaxAmount") : 0;
int _totalcost = !reader.IsDBNull( "TotalCost" ) ? reader.GetInt32("TotalCost") : 0;
int _id = !reader.IsDBNull( "ID" ) ? reader.GetInt32("ID") : 0;
string _name = !reader.IsDBNull( "Name" ) ? reader.GetString("Name") : "";
string _desc = !reader.IsDBNull( "Description" ) ? reader.GetString("Description") : "";
int _cost = !reader.IsDBNull( "Cost" ) ? reader.GetInt32("Cost") : 0;
string _url = !reader.IsDBNull( "URL" ) ? reader.GetString("URL") : "Something Random That Wont Ever Be In A URL";
Receipt r = new() {
AccountID = _accountid,
ProductID = _gameid,
ReceiptID = _receiptid,
Time = _receiptdate,
TotalCost = _totalcost,
TaxAmount = _taxamount,
LineItem = _lineitem
};
Product p = new() {
ID = _id,
Cost = _cost,
Description = _desc,
Name = _name,
URL = _url
};
join.Add( (r, p) );
}
}
}
return join.ToArray();
}
public async Task<Receipt?> GetReceipt( int accountID, int gameID ) {
Receipt? receipt = null;
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
SELECT * FROMReceipt
WHERE AccountID = @AccountID AND ProductID = @ProductID;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@AccountID", accountID);
cmd.Parameters.AddWithValue("@ProductID", gameID);
using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) {
while( await reader.ReadAsync() ) {
if( reader == null ) {
break;
}
int _accountid = reader.GetInt32("AccountID");
int _gameid = reader.GetInt32("ProductID");
string _receiptid = reader.GetString("ReceiptID");
int _lineitem = reader.GetInt32("LineItem");
DateTime _receiptdate = reader.GetDateTime("Time");
int _taxamount = reader.GetInt32("TaxAmount");
int _totalcost = reader.GetInt32("TotalCost");
receipt = new Receipt() {
AccountID = _accountid,
ProductID = _gameid,
ReceiptID = _receiptid,
Time = _receiptdate,
TotalCost = _totalcost,
TaxAmount = _taxamount,
LineItem = _lineitem
};
}
}
}
return receipt;
}
public async Task NewReceipt( Receipt receipt ) {
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
string command = @"
INSERT INTO Receipt
(AccountID, ProductID, ReceiptID, LineItem, TaxAmount, TotalCost, Time)
VALUES
(@AccountID, @ProductID, @ReceiptID, @LineItem, @TaxAmount, @TotalCost, @Time)
";
MySqlCommand cmd = new MySqlCommand( command , connection);
cmd.Parameters.AddWithValue("@AccountID", receipt.AccountID);
cmd.Parameters.AddWithValue("@ProductID", receipt.ProductID);
cmd.Parameters.AddWithValue("@ReceiptID", receipt.ReceiptID);
cmd.Parameters.AddWithValue("@LineItem", receipt.LineItem);
cmd.Parameters.AddWithValue("@TaxAmount", receipt.TaxAmount);
cmd.Parameters.AddWithValue("@TotalCost", receipt.TotalCost);
cmd.Parameters.AddWithValue("@Time", receipt.Time); // Just incase i need this in the future | receipt.Time.ToString( "yyyy-MM-dd hh:mm:ss" )
await cmd.ExecuteNonQueryAsync();
}
}
}
}
+41
View File
@@ -0,0 +1,41 @@
using System.Net.Mail;
namespace BoredCareers.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();
}
}
}
}
}
+53
View File
@@ -0,0 +1,53 @@
using System.Net.Mail;
namespace BoredCareers.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>
";
}
}
+54
View File
@@ -0,0 +1,54 @@
using System.Net.Mail;
namespace BoredCareers.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>
";
}
}
+33
View File
@@ -0,0 +1,33 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", ".\Server.csproj", "{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Release|Any CPU.Build.0 = Release|Any CPU
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Release|Any CPU.Build.0 = Release|Any CPU
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B413876B-4048-47F1-B8B8-B974DF5E9E2A}
EndGlobalSection
EndGlobal
+179
View File
@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Stripe-Payments</title>
<script src="https://js.stripe.com/v3/"></script>
<style>
#submit {
width: 200px;
height: 40px;
margin-left: calc(50% - 100px);
margin-top: 30px;
color: #fff;
background-color: #393;
font-size: 16px;
text-decoration: none;
text-transform: uppercase;
overflow: hidden;
transition: .5s;
letter-spacing: 4px;
border: 1px solid #8F7CEC;
}
#submit:hover {
background: #353;
color: #fff;
border-radius: 5px;
border-color: #353;
}
</style>
</head>
<body>
<form id="payment-form">
<div id="link-authentication-element">
<!--Stripe.js injects the Link Authentication Element-->
</div>
<div id="payment-element">
<!--Stripe.js injects the Payment Element-->
</div>
<button id="submit">
<div class="spinner hidden" id="spinner"></div>
<span id="button-text">Pay now</span>
</button>
<div id="payment-message" class="hidden"></div>
</form>
<script>
// This is your test publishable API key.
const stripe = Stripe('pk_live_51LBODxCozZzTNCNhFdVbzm93F1N3Kk5sEiOyUYeU8GlqxF8AkS6h1JOkIqmFJ1hBmkBCEEa8cfBCY7RotHlweS7g00UzyxkUnO');
let elements;
initialize();
checkStatus();
document.querySelector("#payment-form").addEventListener("submit", handleSubmit);
let emailAddress = '';
// Fetches a payment intent and captures the client secret
async function initialize() {
const response = await fetch("/api/getCheckoutToken?userID=" + new URL(window.location.href).searchParams.get("userID"), {
method: "POST",
headers: { "Content-Type": "text/plain" },
body: ""
});
const clientSecret = await response.text();
const appearance = {
theme: 'night',
};
elements = stripe.elements({ appearance, clientSecret });
const linkAuthenticationElement = elements.create("linkAuthentication");
linkAuthenticationElement.mount("#link-authentication-element");
linkAuthenticationElement.on('change', (event) => {
emailAddress = event.value.email;
});
const paymentElementOptions = {
layout: "tabs",
};
const paymentElement = elements.create("payment", paymentElementOptions);
paymentElement.mount("#payment-element");
}
async function handleSubmit(e) {
e.preventDefault();
setLoading(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: "https://mistox.net/store/payment/success",
receipt_email: emailAddress,
},
});
// This point will only be reached if there is an immediate error when
// confirming the payment. Otherwise, your customer will be redirected to
// your `return_url`. For some payment methods like iDEAL, your customer will
// be redirected to an intermediate site first to authorize the payment, then
// redirected to the `return_url`.
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message);
} else {
showMessage("An unexpected error occurred.");
}
setLoading(false);
}
// Fetches the payment intent status after payment submission
async function checkStatus() {
const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);
if (!clientSecret) {
return;
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
switch (paymentIntent.status) {
case "succeeded":
showMessage("Payment succeeded!");
break;
case "processing":
showMessage("Your payment is processing.");
break;
case "requires_payment_method":
showMessage("Your payment was not successful, please try again.");
break;
default:
showMessage("Something went wrong.");
break;
}
}
// ------- UI helpers -------
function showMessage(messageText) {
const messageContainer = document.querySelector("#payment-message");
messageContainer.classList.remove("hidden");
messageContainer.textContent = messageText;
setTimeout(function () {
messageContainer.classList.add("hidden");
messageText.textContent = "";
}, 4000);
}
// Show a spinner on payment submission
function setLoading(isLoading) {
if (isLoading) {
// Disable the button and show a spinner
document.querySelector("#submit").disabled = true;
document.querySelector("#spinner").classList.remove("hidden");
document.querySelector("#button-text").classList.add("hidden");
} else {
document.querySelector("#submit").disabled = false;
document.querySelector("#spinner").classList.add("hidden");
document.querySelector("#button-text").classList.remove("hidden");
}
}
</script>
</body>
</html>
+192
View File
@@ -0,0 +1,192 @@
<html>
<head>
<title>HTML_Snake</title>
</head>
<body onkeydown='return keyDown(event)'; style="background-color: #333;">
<h1 id="Score" style="width: 100%; text-align: center; color:#fff;">Score : 0</h1>
<div id="BODY" style="position: relative; margin-bottom: 5px; margin-left: 50%; right: 300px; background-color: #666; width: 600px; height: 600px;">
<script>
var snake = [];
var score = 0;
var posX = 12;
var posY = 15;
var colX, colY;
var saves = "";
var paused = false;
var direction = 2;
var wasCollected = false;
var body = document.getElementById("BODY");
function set(name,value) {
var expires = "";
var date = new Date();
date.setTime(date.getTime() + (9999*24*60*60*1000));
expires = "; expires=" + date.toUTCString();
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function get(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
function hsl2rgb(h,s,l) {
let a=s*Math.min(l,1-l);
let f= (n,k=(n+h/30)%12) => l - a*Math.max(Math.min(k-3,9-k,1),-1);
return [f(0),f(8),f(4)];
}
var degree = 0;
function newTail(x, y){
degree += 5;
if(degree > 359){
degree = 0;
}
var color = hsl2rgb(degree, .6, .5);
var r = Math.floor(color[0] * 255).toString(16);
var g = Math.floor(color[1] * 255).toString(16);
var b = Math.floor(color[2] * 255).toString(16);
var nX = (10*x)-10;
var nY = (10*y)-10;
var id = x + "," + y;
if (x == colX){
if (y == colY){
var item = document.getElementById("collectable");
item.parentNode.removeChild(item);
wasCollected = true;
newCollectable();
score += 1;
document.getElementById("Score").innerHTML = "Score : " + score;
}
}
body.innerHTML = body.innerHTML + "<div id='" + id + "'; style='position: absolute; left: " + nX + "px; top:" + nY + "px; width: 10px; height: 10px; background-color: #" + r+g+b + "; border: 0; padding: 0; margin: 0;'></div>";
return id;
}
function randInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function newCollectable(){
colX = randInt(1, 59);
colY = randInt(1, 59);
body.innerHTML = body.innerHTML + "<div id='collectable'; style='position: absolute; left: " + (colX*10-10) + "px; top:" + (colY*10-10) + "px; width: 10px; height: 10px; background-color: #f00; border: 0; padding: 0; margin: 0;'></div>";
}
function isBound(x, y){
if (x > 0 && x < 61){
if (y > 0 && y < 61){
return true;
}
}
return false;
}
function die(){
set("data", saves + "|" + score );
location.reload();
}
function update(){
if (paused == false){
if (direction == 1){
posY -= 1;
}else if(direction == 2){
posX += 1;
}else if(direction == 3){
posY += 1;
}else if(direction == 4){
posX -= 1;
}
if (isBound(posX, posY)){
function func(item, index, arr){
var x = item.split(",")[0];
var y = item.split(",")[1];
if(posX == x){
if(posY == y){
die();
}
}
}
snake.forEach(func);
snake.push(newTail(posX, posY));
if(wasCollected == false){
var rem = snake.shift();
var remObj = document.getElementById(rem);
remObj.parentNode.removeChild(remObj);
}else{
wasCollected = false;
}
}else{
die();
}
}
}
function keyDown(event){
if (event.key == "w"){
direction = 1;
}else if(event.key == "d"){
direction = 2;
}else if(event.key == "s"){
direction = 3;
}else if(event.key == "a"){
direction = 4;
}else if(event.key == "p"){
if (paused == true){
paused = false;
document.getElementById("PauseScreen").style.display = "none";
}else{
paused = true;
document.getElementById("PauseScreen").style.display = "";
}
}
}
function loadLeaderboard(){
if (get("data") != null){
saves = get("data");
function func(item, index, arr){
document.getElementById("Scoreboard").innerHTML += '<div><h2 id="' + item + '"; style="text-align: center; font-size: 20px; padding: 0; margin: 0; border: 0;">' + item.toString() + '</h2></div>';
}
saves.split("|").forEach(func);
}
}
function start(){
newCollectable();
snake.push(newTail(10, 15));
snake.push(newTail(11, 15));
snake.push(newTail(12, 15));
setTimeout(loadLeaderboard, 100);
setInterval(update, 100);
}
start();
</script>
</div>
<div id="PauseScreen"style="position: relative; display: none; width: 500px; margin-left: 50%; right: 250px;">
<h2 style="text-align: center; color: #f00; ">Game Paused</h2>
</div>
<div style="margin: 0 40px; width: calc(100% - 80px); background-color: #777;">
<h3 style="text-align: center; font-size: 25px; color: #0f0; padding: 0; margin: 0; border: 0;">LEADERBOARD</h3>
</div>
<hr style="margin: 0px; width:calc(100% - 82px);" />
<div id="Scoreboard" style="margin: 0 40px; width: calc(100% - 80px); background-color: #777;"></div>
<h3 style="position: absolute; right: 10px; bottom: 3px; color: #fff;">Designed by Derek in California</h3>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Some files were not shown because too many files have changed in this diff Show More