This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.cs]
|
||||
csharp_new_line_before_open_brace = none
|
||||
csharp_new_line_before_catch = false
|
||||
csharp_new_line_before_finally = false
|
||||
csharp_new_line_after_else = false
|
||||
Executable
+15
@@ -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
|
||||
@@ -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
@@ -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
|
||||
Vendored
+2
-2
@@ -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,
|
||||
|
||||
Vendored
+31
-1
@@ -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
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<main> @Body </main>
|
||||
@@ -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(){
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -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" />
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "re43MfcLS0M=",
|
||||
"success": true,
|
||||
"projectFilePath": "/home/derek/Desktop/boredcareers/boredcareers/boredcareers.csproj",
|
||||
"expectedPackageFiles": [],
|
||||
"logs": []
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
FROM mysql
|
||||
|
||||
ENV MYSQL_DATABASE=boredcareers
|
||||
ENV MYSQL_ROOT_PASSWORD=90pa8pav89h4g08hads
|
||||
|
||||
ADD mistox.sql /docker-entrypoint-initdb.d
|
||||
|
||||
EXPOSE 3306
|
||||
Executable
+200
@@ -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',
|
||||
''
|
||||
);
|
||||
Executable
+32
@@ -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}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+8931
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "mistox-frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^20.0.0",
|
||||
"@angular/compiler": "^20.0.0",
|
||||
"@angular/core": "^20.0.0",
|
||||
"@angular/forms": "^20.0.0",
|
||||
"@angular/platform-browser": "^20.0.0",
|
||||
"@angular/router": "^20.0.0",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^20.0.2",
|
||||
"@angular/cli": "^20.0.2",
|
||||
"@angular/compiler-cli": "^20.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.7.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { routes } from './app.routes';
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(withInterceptorsFromDi())
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 },
|
||||
]
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 = "";
|
||||
}
|
||||
@@ -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@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 -> run for longer distances<br /> Strength -> carry more weight<br /> Vitality -> Have more base health<br /> Stealth -> Approximate location on map is bigger</p>
|
||||
<p>More weight slows player some<br />Backpacks -> 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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve",
|
||||
"baseUrl": "src",
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
+285
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+68
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+143
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+59
@@ -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; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+120
@@ -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();
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Executable
+82
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
";
|
||||
|
||||
}
|
||||
}
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.002.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Server", ".\Server.csproj", "{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9E4D64F9-2F56-4AC5-85CE-51EFEE1513C0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{76F2B6C1-FF9A-4BD8-AB7A-7456E8122C44}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{19C67017-8C26-439B-95B3-FE346D1AC7D5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B413876B-4048-47F1-B8B8-B974DF5E9E2A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Executable
+179
@@ -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>
|
||||
Executable
+192
@@ -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>
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Executable
BIN
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
Reference in New Issue
Block a user