Merge pull request 'working' (#6) from working into main
Docker Build and Release Upload / build (push) Successful in 1m29s

Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
2025-07-29 17:20:18 +00:00
24 changed files with 453 additions and 164 deletions
+2 -1
View File
@@ -13,4 +13,5 @@ trim_trailing_whitespace = false
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
csharp_new_line_after_else = false
csharp_new_line_before_else = false
+23 -4
View File
@@ -1,13 +1,32 @@
Payment_Service=StripeIntent # Options are [ StripeIntent ]
#############
## Payment ##
#############
# Options are [ StripeIntent ]
Payment_Service=StripeIntent
# StripeIntent Options
Stripe_PublicKey=
Stripe_PublicKey=
Stripe_Endpoint_Secret=
MySQL_Server=mistox-database
####################
## Authentication ##
####################
# Random secret token for encrypting JWT contents
JWT_Secret=
##############
## Database ##
##############
MySQL_User=root
MySQL_Database=mistox
MySQL_Pass=oasv34$8gpv023dd # Random value for the server and MySQL to communicate with
MySQL_Pass=oasv34$8gpv023dd
##############
## Email ##
##############
Email_Server= # Hostname of email server
Email_Port= # SMTP port used
+23 -3
View File
@@ -9,10 +9,30 @@ Server:
Client:
jobs/new:
When remote job is check'd it still asks for location information
Want to add Required skills to help with filtering
Need to fix some UI bugs.
When enter is pressed it tries to submit the form
Should run the whole carosel on enter before the submit is sent
Need to validate input before allowing next step
Want to add completed job listing preview at end of carosel
database:
Add Applied Jobs Table
Add Applied Jobs Table
Task:
Block API Access as much as possible [ Rate limit | Auth Req | CORS | Disallow AI keyword filters ]
Resume builder minimal user input [ Dont allow AI input ]
Auto unlist jobs after a month of no activity [ Multiple offenders marked ]
Dont allow external applications for users on company sites from the start
Allow company to look up users if their resume is public [ Maybe auto with notify ]
Allow users to look up jobs and apply [ Boost visibility | Completely manual ]
Allow multiple resume's for job specific work
Mark ghost listings to allow users to be informed and put companies on blast
Create advanced filtering tools for company lookup and resume lookup
Create and Auth Database based on the docker compose
Create a server table inside the auth database
Point all requests after auth to the correct regional server. -> Currently only Mistox-West exists
CompanyConnect | need to lookup company before making a new one
+3 -2
View File
@@ -5,18 +5,19 @@ services:
image: docker.mistox.net/boredcareers-website:latest
restart: always
environment:
- MySQLServer=boredcareers-database
- MySQLDatabase=boredcareers
- 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}
- JWTsecret=${JWT_Secret}
ports:
- 5000:5000
depends_on:
@@ -23,7 +23,7 @@ export class LogoutComponent {
ngAfterViewInit(){
this.auth.Logout().subscribe({
next: data => {
this.router.navigate(["/"]);
window.location.href = "";
}
});
}
@@ -1,3 +1,21 @@
button {
width: 150px;
border-radius: 5px;
margin: 10px;
text-align: center;
padding: 15px 0;
transition: .5s;
background-color: #0000;
border: 1px solid var(--Mistox-White);
color: var(--Mistox-White);
text-decoration: none;
}
button:hover {
background-color: #00000044;
color: var(--Mistox-Light);
}
.title-text {
width: 100%;
text-align: center;
@@ -7,13 +7,12 @@
<div class="center">
<div class="content-frame">
<label>Company Name</label>
<input name="name" [(ngModel)]="newListing.name" type="text" placeholder="Mistox" />
<input class="input-field" name="name" [(ngModel)]="newListing.name" type="text" placeholder="Mistox" />
<button type="button" (click)="nextStep()">Next</button>
</div>
<div class="footer-frame">
<span>
This should be your actual company name. It will be public on all the job postings you make.
</span>
<span>Company Name</span><br />
<span>Cannot be changed later</span>
</div>
</div>
</div>
@@ -23,14 +22,12 @@
<div class="center">
<div class="content-frame">
<label>Company Website URL</label>
<input name="url" [(ngModel)]="newListing.websiteURL" type="text" placeholder="https://mistox.com/" />
<input class="input-field" name="url" [(ngModel)]="newListing.websiteURL" type="text" placeholder="https://mistox.com/" />
<button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button>
</div>
<div class="footer-frame">
<span>This should be a link to your companies URL</span><br />
<span>so that people searching for your company</span><br />
<span>can find it with ease</span>
<span>Link to your company URL</span><br />
</div>
</div>
</div>
@@ -39,13 +36,13 @@
<div #step class="sub-frame">
<div class="center">
<div class="content-frame">
<label>Company LOGO URL</label>
<input name="logoURL" [(ngModel)]="newListing.logoURL" type="text" placeholder="https://mistox.com/img/logo.png" />
<label>Company Logo URL</label>
<input class="input-field" name="logoURL" [(ngModel)]="newListing.logoURL" type="text" placeholder="https://mistox.com/img/logo.png" />
<button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button>
</div>
<div class="footer-frame">
<span>This should be a link to your companies Logo</span><br />
<span>Link to your company Logo</span><br />
<span>This will show on all your job listings</span><br />
</div>
</div>
@@ -54,45 +51,54 @@
<!-- Contact -->
<div #step class="sub-frame">
<div class="center">
<div class="content-frame split">
<div class="half-frame">
<label>Company Email</label>
<input name="email" [(ngModel)]="newListing.email" type="text" />
</div>
<div class="half-frame">
<label>Company Phone Number</label>
<input name="email" [(ngModel)]="newListing.phone" type="text" />
<div class="content-frame">
<div class="split">
<div class="half-frame">
<label>Company Email</label>
<input class="input-field" name="email" [(ngModel)]="newListing.email" type="text" placeholder="Questions@mistox.com" />
</div>
<div class="half-frame">
<label>Company Phone Number</label>
<input class="input-field" name="email" [(ngModel)]="newListing.phone" type="text" placeholder="+1 800-000-0000" />
</div>
</div>
<button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button>
</div>
<div class="footer-frame">
<span>How clients can get in contact with you about the open job listings</span><br />
</div>
<button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button>
</div>
</div>
<!-- Location -->
<div #step class="sub-frame">
<div class="center">
<h2>Job Location</h2>
<div class="content-frame split">
<div class="half-frame">
<label>City</label>
<input name="city" [(ngModel)]="newListing.city" type="text" />
</div>
<div class="half-frame">
<label>2 Letter Country</label>
<input name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" />
</div>
<div class="half-frame">
<label>2 Letter State/Region</label>
<input name="stateOrRegion" maxlength="2" minlength="2" [(ngModel)]="newListing.stateOrRegion" type="text" />
</div>
<div class="half-frame">
<label>Postal Code</label>
<input name="postalCode" [(ngModel)]="newListing.postalCode" type="text" />
<div class="content-frame">
<div class="split">
<div class="half-frame">
<label>City</label>
<input class="input-field" name="city" [(ngModel)]="newListing.city" type="text" placeholder="San Diego" />
</div>
<div class="half-frame">
<label>2 Letter Country</label>
<input class="input-field" name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" placeholder="US" />
</div>
<div class="half-frame">
<label>2 Letter State/Region</label>
<input class="input-field" name="stateOrRegion" maxlength="2" minlength="2" [(ngModel)]="newListing.stateOrRegion" type="text" placeholder="CA" />
</div>
<div class="half-frame">
<label>Postal Code</label>
<input class="input-field" name="postalCode" [(ngModel)]="newListing.postalCode" type="text" placeholder="92020" />
</div>
</div>
<button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button>
</div>
<div class="footer-frame">
<span>The location of your company office or hq</span><br />
</div>
</div>
</div>
@@ -101,10 +107,13 @@
<div class="center">
<div class="content-frame">
<label>Description</label>
<textarea name="description" [(ngModel)]="newListing.description" type="text"></textarea>
<textarea class="input-field" name="description" [(ngModel)]="newListing.description" type="text"></textarea>
<button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button>
</div>
<div class="footer-frame">
<span>Describe your buisiness so that people applying can understand the core values and culture</span><br />
</div>
</div>
</div>
@@ -134,14 +143,17 @@
<span>postal code: {{ newListing.postalCode }}</span>
</div>
</div>
<div>
<span>{{ newListing.description }}</span>
<div *ngFor="let descLine of newListing.description.split('\n')">
<span>{{ descLine }}</span><br />
</div>
</div>
<div class="content-frame">
<button type="button" (click)="prevStep()">Back</button>
<button type="submit">CREATE COMPANY</button>
</div>
<div class="footer-frame">
<span>Does everything look good</span><br />
</div>
</div>
</div>
</form>
@@ -1,4 +1,4 @@
import { Component, ElementRef, QueryList, ViewChildren } from '@angular/core';
import { Component, ElementRef, HostListener, QueryList, ViewChildren } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute, RouterModule } from '@angular/router';
@@ -26,6 +26,20 @@ export class CompanyConnectComponent {
};
ngAfterViewInit(){
this.formSteps.changes.subscribe(() => {
this.updateUI(0);
});
this.updateUI(0);
}
@HostListener('window:keydown', ['$event'])
handleGlobalKeyDown(event: KeyboardEvent){
if (event.key === 'Tab'){
event.preventDefault();
}
}
updateUI(subItem: number){
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => {
if (i === this.currentStep) {
step.nativeElement.style.left = '0%';
@@ -35,35 +49,82 @@ export class CompanyConnectComponent {
step.nativeElement.style.left = '100%';
}
});
setTimeout(() => {
(this.formSteps.get(this.currentStep)?.nativeElement.querySelectorAll('.input-field')[subItem] as HTMLElement)?.focus();
}, 500);
}
nextStep(){
this.currentStep += 1;
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => {
if (i === this.currentStep) {
step.nativeElement.style.left = '0%';
} else if (i < this.currentStep) {
step.nativeElement.style.left = '-100%';
} else {
step.nativeElement.style.left = '100%';
}
});
this.updateUI(0);
}
prevStep(){
this.currentStep -= 1;
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => {
if (i === this.currentStep) {
step.nativeElement.style.left = '0%';
} else if (i < this.currentStep) {
step.nativeElement.style.left = '-100%';
} else {
step.nativeElement.style.left = '100%';
}
});
this.updateUI(0);
}
isNullOrEmpty(str: string | null | undefined): boolean {
return !str || str.trim().length === 0;
}
focusFrame(frameNum: number, subItem: number): void {
this.currentStep = frameNum;
this.updateUI(subItem);
}
PostNewCompany(company: Company){
if (this.isNullOrEmpty(company.name)){
this.focusFrame(0, 0);
return;
}
if (this.isNullOrEmpty(company.websiteURL)){
this.focusFrame(1, 0);
return;
}
if (this.isNullOrEmpty(company.logoURL)){
this.focusFrame(2, 0);
return;
}
if (this.isNullOrEmpty(company.email)){
this.focusFrame(3, 0);
return;
}
if (this.isNullOrEmpty(company.phone)){
this.focusFrame(3, 1);
return;
}
if (this.isNullOrEmpty(company.city)){
this.focusFrame(4, 0);
return;
}
if (this.isNullOrEmpty(company.country)){
this.focusFrame(4, 1);
return;
}
if (this.isNullOrEmpty(company.stateOrRegion)){
this.focusFrame(4, 2);
return;
}
if (this.isNullOrEmpty(company.postalCode)){
this.focusFrame(4, 3);
return;
}
if (this.isNullOrEmpty(company.description)){
this.focusFrame(5, 0);
return;
}
this.http.post("api/company?newCompany=true", company).subscribe({
next: data => {
this.router.navigate([""]);
@@ -1,3 +1,21 @@
button {
width: 150px;
border-radius: 5px;
margin: 10px;
text-align: center;
padding: 15px 0;
transition: .5s;
background-color: #0000;
border: 1px solid var(--Mistox-White);
color: var(--Mistox-White);
text-decoration: none;
}
button:hover {
background-color: #00000044;
color: var(--Mistox-Light);
}
.full-width {
display: block;
width: 100%;
@@ -5,14 +23,65 @@
}
.tile-frame {
column-count: 4;
display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: 20px;
padding: 20px;
width: calc(100% - 40px);
}
.tile{
background-color: var(--Mistox-Dark)\);
height: 40px;
background-color: var(--Mistox-Dark);
color: var(--Mistox-White);
break-inside: avoid;
padding: 20px;
border-radius: 20px;
margin-bottom: 20px;
}
.jobs-frame {
width: 100%;
background-color: #8888;
border-top: 2px solid black;
}
.post-job-frame {
display: flex;
justify-content: center;
padding: 10px 0;
}
.tile-title {
text-align: center;
border-bottom: 1px solid;
}
.tile-title h1 {
font-size: 40px;
margin: 5px 0;
}
.tile-title h2 {
font-size: 14px;
}
.tile-split {
columns: 2;
text-align: center;
padding: 10px 0;
}
.tile-split h1 {
margin: 0;
}
.tile-button {
display: flex;
width: 100%;
justify-content: center;
}
.post-job-frame button {
border-color: var(--Mistox-Black);
color: var(--Mistox-Black);
}
@@ -1,5 +1,5 @@
<!-- My Jobs -->
<div class="jobs-frame">
<div *ngIf="auth.isLoggedIn" class="jobs-frame">
<div class="posted-jobs-frame" *ngFor="let cur of MyJobListings">
<div class="tile">
<h1>{{ cur.title }}</h1>
@@ -11,14 +11,13 @@
<h1>{{ cur.stateOrRegion }}</h1>
<h1>{{ cur.country }}</h1>
<h1>{{ cur.postalCode }}</h1>
<h1>{{ cur.description }}</h1>
<h1>Posted: {{ cur.createdTime }}</h1>
<h1>Modified: {{ cur.modifiedTime }}</h1>
</div>
<button [routerLink]="['/jobs/edit']" [queryParams]="{ JobID: cur.id }" >EDIT</button>
<button (click)="RemoveJobListing(cur.id)">DELETE</button>
</div>
<div>
<div class="post-job-frame">
<button [routerLink]="['/jobs/new']">POST JOB</button>
</div>
</div>
@@ -26,17 +25,20 @@
<!-- Avaliable Jobs -->
<div class="tile-frame" *ngFor="let cur of JobListingPage">
<div class="tile">
<h1>{{ cur.title }}</h1>
<h1>{{ cur.jobType }}</h1>
<h1>Is Remote: {{ cur.remote }}</h1>
<h1>{{ cur.salaryMin }}</h1>
<h1>{{ cur.salaryMax }}</h1>
<h1>{{ cur.city }}</h1>
<h1>{{ cur.stateOrRegion }}</h1>
<h1>{{ cur.country }}</h1>
<h1>{{ cur.postalCode }}</h1>
<h1>{{ cur.description }}</h1>
<h1>Posted: {{ cur.createdTime }}</h1>
<h1>Modified: {{ cur.modifiedTime }}</h1>
<div class="tile-title">
<h1>{{ cur.title }}</h1>
<h2>${{ cur.salaryMax }} - ${{ cur.salaryMin }}</h2>
</div>
<div class="tile-split">
<h1>{{ cur.jobType }}</h1>
<h1 *ngIf="cur.remote" >Remote</h1>
</div>
<div class="tile-split">
<h1>{{ cur.city }}</h1>
<h1>{{ cur.stateOrRegion }}</h1>
</div>
<div class="tile-button">
<button [routerLink]="['/jobs/new']">VIEW LISTING</button>
</div>
</div>
</div>
@@ -14,7 +14,7 @@ form {
.center {
width: 100%;
display: flex;
display: grid;
justify-content: center;
}
@@ -31,6 +31,15 @@ form {
border-radius: 10px;
padding: 40px;
break-inside: avoid;
margin-bottom: 20px;
}
.footer-frame {
background-color: #00000044;
border-radius: 10px;
padding: 10px;
break-inside: avoid;
margin-bottom: 20px;
}
.split {
@@ -9,11 +9,16 @@
<div class="content-frame">
<label>For What Company</label>
<select name="company" [(ngModel)]="selectedCompany">
<option value="">-- Select Company --</option>
<option *ngFor="let cur of employeeOfList" [ngValue]="cur.company">{{ cur.company.name }}</option>
</select>
<button type="button" (click)="nextStep()">Next</button>
</div>
<div class="footer-frame">
<span>
Choose the company you want the listing to be created under.
</span>
<button [routerLink]="['/company/connect']">CONNECT A NEW COMPANY</button>
</div>
</div>
</div>
@@ -36,7 +41,6 @@
<div class="half-frame">
<label>Job Type</label>
<select name="jobType" [(ngModel)]="newListing.jobType">
<option value="">-- Select Job Type --</option>
<option value="Full-time">Full-time</option>
<option value="Part-time">Part-time</option>
<option value="Contract">Contract</option>
@@ -55,25 +59,29 @@
</div>
<!-- Location -->
<div #step class="sub-frame">
<div #step *ngIf="!newListing.remote" class="sub-frame">
<div class="center">
<h2>Job Location</h2>
<div class="content-frame split">
<div class="half-frame">
<label>City</label>
<input name="city" [(ngModel)]="newListing.city" type="text" />
<div>
<div class="content-frame split" style="border-radius: 10px 10px 0 0;">
<div class="half-frame">
<label>City</label>
<input name="city" [(ngModel)]="newListing.city" type="text" />
</div>
<div class="half-frame">
<label>2 Letter State/Region</label>
<input name="stateOrRegion" maxlength="2" minlength="2" [(ngModel)]="newListing.stateOrRegion" type="text" />
</div>
</div>
<div class="half-frame">
<label>2 Letter Country</label>
<input name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" />
</div>
<div class="half-frame">
<label>2 Letter State/Region</label>
<input name="stateOrRegion" maxlength="2" minlength="2" [(ngModel)]="newListing.stateOrRegion" type="text" />
</div>
<div class="half-frame">
<label>Postal Code</label>
<input name="postalCode" [(ngModel)]="newListing.postalCode" type="text" />
<div class="content-frame split" style="border-radius: 0 0 10px 10px;">
<div class="half-frame">
<label>2 Letter Country</label>
<input name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" />
</div>
<div class="half-frame">
<label>Postal Code</label>
<input name="postalCode" [(ngModel)]="newListing.postalCode" type="text" />
</div>
</div>
<button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button>
@@ -42,6 +42,13 @@ export class JobNewComponent {
};
ngAfterViewInit(){
this.formSteps.changes.subscribe(() => {
this.updateUI();
});
this.updateUI();
}
updateUI(){
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => {
if (i === this.currentStep) {
step.nativeElement.style.left = '0%';
@@ -55,28 +62,12 @@ export class JobNewComponent {
nextStep(){
this.currentStep += 1;
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => {
if (i === this.currentStep) {
step.nativeElement.style.left = '0%';
} else if (i < this.currentStep) {
step.nativeElement.style.left = '-100%';
} else {
step.nativeElement.style.left = '100%';
}
});
this.updateUI();
}
prevStep(){
this.currentStep -= 1;
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => {
if (i === this.currentStep) {
step.nativeElement.style.left = '0%';
} else if (i < this.currentStep) {
step.nativeElement.style.left = '-100%';
} else {
step.nativeElement.style.left = '100%';
}
});
this.updateUI();
}
PostJobListing(jobListing: JobListing){
@@ -24,6 +24,7 @@ export class Authentication{
let sub = this.http.post<Account>( "api/account/login", body, { headers } );
sub.subscribe({
next: data => {
data.passwordHash = "";
this._user.next(data);
this.setUserToStorage(data, StayLoggedIn == true ? SessionType.Forever : SessionType.Session);
},
@@ -1,7 +1,4 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
@@ -34,19 +31,9 @@ namespace BoredCareers.Controllers {
test.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(test);
List<Claim> claims = new List<Claim>() {
new Claim("ID", test.ID.ToString()),
new Claim(ClaimTypes.NameIdentifier, test.ID.ToString())
};
string jwt = BoredCareersJWT.GenereateJWTToken(test.ID, StayLoggedIn);
BoredCareersJWT.SignIn(Response, StayLoggedIn, jwt);
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 Ok(test);
} else {
test.CurrentPasswordAttempts += 1;
@@ -151,9 +138,9 @@ namespace BoredCareers.Controllers {
[Route("logout")]
[HttpPost]
public async Task<ActionResult> Logout() {
public ActionResult Logout() {
if (isLoggedIn()) {
await HttpContext.SignOutAsync();
BoredCareersJWT.SignOut(Response);
return Ok();
}
return NotFound();
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Entities;
using BoredCareers.Services.DatabaseService;
using System.Security.Claims;
namespace BoredCareers.Controllers {
@@ -20,7 +21,7 @@ namespace BoredCareers.Controllers {
}
public int getLoggedInUserID() {
return Convert.ToInt32(User.FindFirst("ID")?.Value);
return Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
public async Task<Account> getLoggedInUser() {
+1 -1
View File
@@ -28,7 +28,7 @@ namespace BoredCareers.Controllers {
public async Task<IActionResult> paymentWebhook() {
try {
string body = await new StreamReader(Request.Body).ReadToEndAsync();
await _paymentService.ValidatePurchase(body, Request.Headers["Stripe-Signature"].ToString());
_paymentService.ValidatePurchase(body, Request.Headers["Stripe-Signature"].ToString());
return Ok();
} catch (Exception ex) {
return NotFound(ex.ToString());
@@ -1,5 +1,3 @@
using BoredCareers.Entities;
namespace BoredCareers.Controllers.Payment {
public interface IPayment {
@@ -8,7 +6,7 @@ namespace BoredCareers.Controllers.Payment {
public static string _EndpointSecret = "";
public static string _PublicKey = "";
public Task ValidatePurchase(string WebHookData, string Headers);
public void ValidatePurchase(string WebHookData, string Headers);
}
@@ -11,7 +11,7 @@ namespace BoredCareers.Controllers {
_databaseService = databaseService;
}
public async Task ValidatePurchase(string WebHookData, string Headers) {
public void 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
+47 -10
View File
@@ -1,10 +1,13 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using BoredCareers.Controllers.Payment;
using BoredCareers.Services;
using BoredCareers.Services.DatabaseService;
using System.Threading.RateLimiting;
using Stripe;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
@@ -36,6 +39,15 @@ string dbPass = !string.IsNullOrEmpty(_dbpass) ? _dbpass : "oasv34$8gpv023dd";
DatabaseService databaseService = new DatabaseService(connectionString: "server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;");
builder.Services.Add( new ServiceDescriptor( typeof( DatabaseService ), databaseService ) );
////////////////////////////////
////////// Auth Service ////////
////////////////////////////////
// Address
string? _jwtSecret = Environment.GetEnvironmentVariable("JWTsecret");
string JWTsecret = !string.IsNullOrEmpty(_jwtSecret) ? _jwtSecret : "v0Ftluhdh7Nht8^2b5eaiC^IS^VS1ku0VBs3j*B2";
BoredCareersJWT.TokenSecretKey = JWTsecret;
////////////////////////////////
///////// Email Service ////////
////////////////////////////////
@@ -81,15 +93,40 @@ if (IPayment._PaymentType == PaymentType.StripeIntent) {
}
// 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.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = BoredCareersJWT.TokenIssuer,
ValidAudience = BoredCareersJWT.TokenAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(BoredCareersJWT.TokenSecretKey)),
ClockSkew = TimeSpan.FromMinutes(1)
};
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
context.Token = context.Request.Cookies[BoredCareersJWT.TokenName];
return Task.CompletedTask;
},
OnTokenValidated = context => {
var jwtToken = context.SecurityToken as JwtSecurityToken;
if (jwtToken != null) {
var exp = jwtToken.ValidTo;
var now = DateTime.UtcNow;
if ((exp - now) < TimeSpan.FromDays(3)) {
int accountID = Convert.ToInt32(context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value);
bool isPersistent = bool.Parse(context.Principal?.FindFirst(ClaimTypes.IsPersistent)?.Value);
var newJWT = BoredCareersJWT.GenereateJWTToken(accountID, isPersistent);
BoredCareersJWT.SignIn(context.HttpContext.Response, isPersistent, newJWT);
}
}
return Task.CompletedTask;
}
};
});
builder.Services.AddCors(o => o.AddDefaultPolicy(builder => {
+1
View File
@@ -11,6 +11,7 @@
<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.Authentication.JwtBearer" 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" />
@@ -1,5 +1,3 @@
using System.Net.Mail;
namespace BoredCareers.Services {
public partial class EmailService {
@@ -1,5 +1,3 @@
using System.Net.Mail;
namespace BoredCareers.Services {
public partial class EmailService {
+57
View File
@@ -0,0 +1,57 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace BoredCareers.Services {
public class BoredCareersJWT {
public static string TokenAudience = "mistox-llc-auth-token";
public static string TokenIssuer = "https://auth.mistox.com";
public static string TokenSecretKey = "";
public static string TokenName = "mistox_session";
public static string GenereateJWTToken(int accountID, bool StayLoggedIn) {
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(TokenSecretKey);
var tokenDiscriptor = new SecurityTokenDescriptor {
Subject = new ClaimsIdentity([
new Claim(ClaimTypes.NameIdentifier, accountID.ToString()),
new Claim(ClaimTypes.IsPersistent, StayLoggedIn.ToString())
]),
Expires = DateTime.UtcNow.AddDays(7),
IssuedAt = DateTime.UtcNow,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256),
Audience = TokenAudience,
Issuer = TokenIssuer
};
var token = tokenHandler.CreateToken(tokenDiscriptor);
return tokenHandler.WriteToken(token);
}
public static void SignIn(HttpResponse Response, bool StayLoggedIn, string jwt) {
if (StayLoggedIn) {
// Stay logged in cookie
Response.Cookies.Append(TokenName, jwt, new CookieOptions {
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddDays(7)
});
} else {
// Session cookie
Response.Cookies.Append(TokenName, jwt, new CookieOptions {
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Strict,
});
}
}
public static void SignOut(HttpResponse Response) {
Response.Cookies.Delete(TokenName);
}
}
}