working #6

Merged
derek merged 17 commits from working into main 2025-07-29 10:20:18 -07:00
24 changed files with 453 additions and 164 deletions
+1
View File
@@ -14,3 +14,4 @@ csharp_new_line_before_open_brace = none
csharp_new_line_before_catch = false csharp_new_line_before_catch = false
csharp_new_line_before_finally = false csharp_new_line_before_finally = false
csharp_new_line_after_else = false csharp_new_line_after_else = false
csharp_new_line_before_else = false
+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_PublicKey= Stripe_PublicKey=
Stripe_Endpoint_Secret= Stripe_Endpoint_Secret=
MySQL_Server=mistox-database ####################
## Authentication ##
####################
# Random secret token for encrypting JWT contents
JWT_Secret=
##############
## Database ##
##############
MySQL_User=root MySQL_User=root
MySQL_Database=mistox MySQL_Pass=oasv34$8gpv023dd
MySQL_Pass=oasv34$8gpv023dd # Random value for the server and MySQL to communicate with
##############
## Email ##
##############
Email_Server= # Hostname of email server Email_Server= # Hostname of email server
Email_Port= # SMTP port used Email_Port= # SMTP port used
+22 -2
View File
@@ -9,10 +9,30 @@ Server:
Client: Client:
jobs/new: jobs/new:
When remote job is check'd it still asks for location information
Want to add Required skills to help with filtering 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 Want to add completed job listing preview at end of carosel
database: 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 image: docker.mistox.net/boredcareers-website:latest
restart: always restart: always
environment: environment:
- MySQLServer=boredcareers-database
- MySQLDatabase=boredcareers
- PaymentService=${Payment_Service} - PaymentService=${Payment_Service}
- StripePublicKey=${Stripe_PublicKey} - StripePublicKey=${Stripe_PublicKey}
- StripeApiKey=${Stripe_ApiKey} - StripeApiKey=${Stripe_ApiKey}
- StripeEndpointSecret=&{Stripe_Endpoint_Secret} - StripeEndpointSecret=&{Stripe_Endpoint_Secret}
- MySQLServer=${MySQL_Server}
- MySQLUser=${MySQL_User} - MySQLUser=${MySQL_User}
- MySQLPass=${MySQL_Pass} - MySQLPass=${MySQL_Pass}
- MySQLDatabase=${MySQL_Database}
- EmailServer=${Email_Server} - EmailServer=${Email_Server}
- EmailPort=${Email_Port} - EmailPort=${Email_Port}
- EmailAddress=${Email_Address} - EmailAddress=${Email_Address}
- EmailPassword=${Email_Password} - EmailPassword=${Email_Password}
- JWTsecret=${JWT_Secret}
ports: ports:
- 5000:5000 - 5000:5000
depends_on: depends_on:
@@ -23,7 +23,7 @@ export class LogoutComponent {
ngAfterViewInit(){ ngAfterViewInit(){
this.auth.Logout().subscribe({ this.auth.Logout().subscribe({
next: data => { 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 { .title-text {
width: 100%; width: 100%;
text-align: center; text-align: center;
@@ -7,13 +7,12 @@
<div class="center"> <div class="center">
<div class="content-frame"> <div class="content-frame">
<label>Company Name</label> <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> <button type="button" (click)="nextStep()">Next</button>
</div> </div>
<div class="footer-frame"> <div class="footer-frame">
<span> <span>Company Name</span><br />
This should be your actual company name. It will be public on all the job postings you make. <span>Cannot be changed later</span>
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -23,14 +22,12 @@
<div class="center"> <div class="center">
<div class="content-frame"> <div class="content-frame">
<label>Company Website URL</label> <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)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
</div> </div>
<div class="footer-frame"> <div class="footer-frame">
<span>This should be a link to your companies URL</span><br /> <span>Link to your company URL</span><br />
<span>so that people searching for your company</span><br />
<span>can find it with ease</span>
</div> </div>
</div> </div>
</div> </div>
@@ -39,13 +36,13 @@
<div #step class="sub-frame"> <div #step class="sub-frame">
<div class="center"> <div class="center">
<div class="content-frame"> <div class="content-frame">
<label>Company LOGO URL</label> <label>Company Logo URL</label>
<input name="logoURL" [(ngModel)]="newListing.logoURL" type="text" placeholder="https://mistox.com/img/logo.png" /> <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)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
</div> </div>
<div class="footer-frame"> <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 /> <span>This will show on all your job listings</span><br />
</div> </div>
</div> </div>
@@ -54,45 +51,54 @@
<!-- Contact --> <!-- Contact -->
<div #step class="sub-frame"> <div #step class="sub-frame">
<div class="center"> <div class="center">
<div class="content-frame split"> <div class="content-frame">
<div class="split">
<div class="half-frame"> <div class="half-frame">
<label>Company Email</label> <label>Company Email</label>
<input name="email" [(ngModel)]="newListing.email" type="text" /> <input class="input-field" name="email" [(ngModel)]="newListing.email" type="text" placeholder="Questions@mistox.com" />
</div> </div>
<div class="half-frame"> <div class="half-frame">
<label>Company Phone Number</label> <label>Company Phone Number</label>
<input name="email" [(ngModel)]="newListing.phone" type="text" /> <input class="input-field" name="email" [(ngModel)]="newListing.phone" type="text" placeholder="+1 800-000-0000" />
</div> </div>
</div> </div>
<button type="button" (click)="prevStep()">Back</button> <button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
</div> </div>
<div class="footer-frame">
<span>How clients can get in contact with you about the open job listings</span><br />
</div>
</div>
</div> </div>
<!-- Location --> <!-- Location -->
<div #step class="sub-frame"> <div #step class="sub-frame">
<div class="center"> <div class="center">
<h2>Job Location</h2> <div class="content-frame">
<div class="content-frame split"> <div class="split">
<div class="half-frame"> <div class="half-frame">
<label>City</label> <label>City</label>
<input name="city" [(ngModel)]="newListing.city" type="text" /> <input class="input-field" name="city" [(ngModel)]="newListing.city" type="text" placeholder="San Diego" />
</div> </div>
<div class="half-frame"> <div class="half-frame">
<label>2 Letter Country</label> <label>2 Letter Country</label>
<input name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" /> <input class="input-field" name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" placeholder="US" />
</div> </div>
<div class="half-frame"> <div class="half-frame">
<label>2 Letter State/Region</label> <label>2 Letter State/Region</label>
<input name="stateOrRegion" maxlength="2" minlength="2" [(ngModel)]="newListing.stateOrRegion" type="text" /> <input class="input-field" name="stateOrRegion" maxlength="2" minlength="2" [(ngModel)]="newListing.stateOrRegion" type="text" placeholder="CA" />
</div> </div>
<div class="half-frame"> <div class="half-frame">
<label>Postal Code</label> <label>Postal Code</label>
<input name="postalCode" [(ngModel)]="newListing.postalCode" type="text" /> <input class="input-field" name="postalCode" [(ngModel)]="newListing.postalCode" type="text" placeholder="92020" />
</div>
</div> </div>
<button type="button" (click)="prevStep()">Back</button> <button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
</div> </div>
<div class="footer-frame">
<span>The location of your company office or hq</span><br />
</div>
</div> </div>
</div> </div>
@@ -101,10 +107,13 @@
<div class="center"> <div class="center">
<div class="content-frame"> <div class="content-frame">
<label>Description</label> <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)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
</div> </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>
</div> </div>
@@ -134,14 +143,17 @@
<span>postal code: {{ newListing.postalCode }}</span> <span>postal code: {{ newListing.postalCode }}</span>
</div> </div>
</div> </div>
<div> <div *ngFor="let descLine of newListing.description.split('\n')">
<span>{{ newListing.description }}</span> <span>{{ descLine }}</span><br />
</div> </div>
</div> </div>
<div class="content-frame"> <div class="content-frame">
<button type="button" (click)="prevStep()">Back</button> <button type="button" (click)="prevStep()">Back</button>
<button type="submit">CREATE COMPANY</button> <button type="submit">CREATE COMPANY</button>
</div> </div>
<div class="footer-frame">
<span>Does everything look good</span><br />
</div>
</div> </div>
</div> </div>
</form> </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 { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute, RouterModule } from '@angular/router'; import { Router, ActivatedRoute, RouterModule } from '@angular/router';
@@ -26,6 +26,20 @@ export class CompanyConnectComponent {
}; };
ngAfterViewInit(){ 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) => { this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => {
if (i === this.currentStep) { if (i === this.currentStep) {
step.nativeElement.style.left = '0%'; step.nativeElement.style.left = '0%';
@@ -35,35 +49,82 @@ export class CompanyConnectComponent {
step.nativeElement.style.left = '100%'; step.nativeElement.style.left = '100%';
} }
}); });
setTimeout(() => {
(this.formSteps.get(this.currentStep)?.nativeElement.querySelectorAll('.input-field')[subItem] as HTMLElement)?.focus();
}, 500);
} }
nextStep(){ nextStep(){
this.currentStep += 1; this.currentStep += 1;
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => { this.updateUI(0);
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%';
}
});
} }
prevStep(){ prevStep(){
this.currentStep -= 1; this.currentStep -= 1;
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => { this.updateUI(0);
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%';
} }
});
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){ 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({ this.http.post("api/company?newCompany=true", company).subscribe({
next: data => { next: data => {
this.router.navigate([""]); 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 { .full-width {
display: block; display: block;
width: 100%; width: 100%;
@@ -5,14 +23,65 @@
} }
.tile-frame { .tile-frame {
column-count: 4; display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: 20px; column-gap: 20px;
padding: 20px; padding: 20px;
width: calc(100% - 40px); width: calc(100% - 40px);
} }
.tile{ .tile{
background-color: var(--Mistox-Dark)\); background-color: var(--Mistox-Dark);
height: 40px; color: var(--Mistox-White);
break-inside: avoid; 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 --> <!-- 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="posted-jobs-frame" *ngFor="let cur of MyJobListings">
<div class="tile"> <div class="tile">
<h1>{{ cur.title }}</h1> <h1>{{ cur.title }}</h1>
@@ -11,14 +11,13 @@
<h1>{{ cur.stateOrRegion }}</h1> <h1>{{ cur.stateOrRegion }}</h1>
<h1>{{ cur.country }}</h1> <h1>{{ cur.country }}</h1>
<h1>{{ cur.postalCode }}</h1> <h1>{{ cur.postalCode }}</h1>
<h1>{{ cur.description }}</h1>
<h1>Posted: {{ cur.createdTime }}</h1> <h1>Posted: {{ cur.createdTime }}</h1>
<h1>Modified: {{ cur.modifiedTime }}</h1> <h1>Modified: {{ cur.modifiedTime }}</h1>
</div> </div>
<button [routerLink]="['/jobs/edit']" [queryParams]="{ JobID: cur.id }" >EDIT</button> <button [routerLink]="['/jobs/edit']" [queryParams]="{ JobID: cur.id }" >EDIT</button>
<button (click)="RemoveJobListing(cur.id)">DELETE</button> <button (click)="RemoveJobListing(cur.id)">DELETE</button>
</div> </div>
<div> <div class="post-job-frame">
<button [routerLink]="['/jobs/new']">POST JOB</button> <button [routerLink]="['/jobs/new']">POST JOB</button>
</div> </div>
</div> </div>
@@ -26,17 +25,20 @@
<!-- Avaliable Jobs --> <!-- Avaliable Jobs -->
<div class="tile-frame" *ngFor="let cur of JobListingPage"> <div class="tile-frame" *ngFor="let cur of JobListingPage">
<div class="tile"> <div class="tile">
<div class="tile-title">
<h1>{{ cur.title }}</h1> <h1>{{ cur.title }}</h1>
<h2>${{ cur.salaryMax }} - ${{ cur.salaryMin }}</h2>
</div>
<div class="tile-split">
<h1>{{ cur.jobType }}</h1> <h1>{{ cur.jobType }}</h1>
<h1>Is Remote: {{ cur.remote }}</h1> <h1 *ngIf="cur.remote" >Remote</h1>
<h1>{{ cur.salaryMin }}</h1> </div>
<h1>{{ cur.salaryMax }}</h1> <div class="tile-split">
<h1>{{ cur.city }}</h1> <h1>{{ cur.city }}</h1>
<h1>{{ cur.stateOrRegion }}</h1> <h1>{{ cur.stateOrRegion }}</h1>
<h1>{{ cur.country }}</h1> </div>
<h1>{{ cur.postalCode }}</h1> <div class="tile-button">
<h1>{{ cur.description }}</h1> <button [routerLink]="['/jobs/new']">VIEW LISTING</button>
<h1>Posted: {{ cur.createdTime }}</h1> </div>
<h1>Modified: {{ cur.modifiedTime }}</h1>
</div> </div>
</div> </div>
@@ -14,7 +14,7 @@ form {
.center { .center {
width: 100%; width: 100%;
display: flex; display: grid;
justify-content: center; justify-content: center;
} }
@@ -31,6 +31,15 @@ form {
border-radius: 10px; border-radius: 10px;
padding: 40px; padding: 40px;
break-inside: avoid; break-inside: avoid;
margin-bottom: 20px;
}
.footer-frame {
background-color: #00000044;
border-radius: 10px;
padding: 10px;
break-inside: avoid;
margin-bottom: 20px;
} }
.split { .split {
@@ -9,11 +9,16 @@
<div class="content-frame"> <div class="content-frame">
<label>For What Company</label> <label>For What Company</label>
<select name="company" [(ngModel)]="selectedCompany"> <select name="company" [(ngModel)]="selectedCompany">
<option value="">-- Select Company --</option>
<option *ngFor="let cur of employeeOfList" [ngValue]="cur.company">{{ cur.company.name }}</option> <option *ngFor="let cur of employeeOfList" [ngValue]="cur.company">{{ cur.company.name }}</option>
</select> </select>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
</div> </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>
</div> </div>
@@ -36,7 +41,6 @@
<div class="half-frame"> <div class="half-frame">
<label>Job Type</label> <label>Job Type</label>
<select name="jobType" [(ngModel)]="newListing.jobType"> <select name="jobType" [(ngModel)]="newListing.jobType">
<option value="">-- Select Job Type --</option>
<option value="Full-time">Full-time</option> <option value="Full-time">Full-time</option>
<option value="Part-time">Part-time</option> <option value="Part-time">Part-time</option>
<option value="Contract">Contract</option> <option value="Contract">Contract</option>
@@ -55,26 +59,30 @@
</div> </div>
<!-- Location --> <!-- Location -->
<div #step class="sub-frame"> <div #step *ngIf="!newListing.remote" class="sub-frame">
<div class="center"> <div class="center">
<h2>Job Location</h2> <h2>Job Location</h2>
<div class="content-frame split"> <div>
<div class="content-frame split" style="border-radius: 10px 10px 0 0;">
<div class="half-frame"> <div class="half-frame">
<label>City</label> <label>City</label>
<input name="city" [(ngModel)]="newListing.city" type="text" /> <input name="city" [(ngModel)]="newListing.city" type="text" />
</div> </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="content-frame split" style="border-radius: 0 0 10px 10px;">
<div class="half-frame"> <div class="half-frame">
<label>2 Letter Country</label> <label>2 Letter Country</label>
<input name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" /> <input name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" />
</div> </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"> <div class="half-frame">
<label>Postal Code</label> <label>Postal Code</label>
<input name="postalCode" [(ngModel)]="newListing.postalCode" type="text" /> <input name="postalCode" [(ngModel)]="newListing.postalCode" type="text" />
</div> </div>
</div>
<button type="button" (click)="prevStep()">Back</button> <button type="button" (click)="prevStep()">Back</button>
<button type="button" (click)="nextStep()">Next</button> <button type="button" (click)="nextStep()">Next</button>
</div> </div>
@@ -42,6 +42,13 @@ export class JobNewComponent {
}; };
ngAfterViewInit(){ ngAfterViewInit(){
this.formSteps.changes.subscribe(() => {
this.updateUI();
});
this.updateUI();
}
updateUI(){
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => { this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => {
if (i === this.currentStep) { if (i === this.currentStep) {
step.nativeElement.style.left = '0%'; step.nativeElement.style.left = '0%';
@@ -55,28 +62,12 @@ export class JobNewComponent {
nextStep(){ nextStep(){
this.currentStep += 1; this.currentStep += 1;
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => { this.updateUI();
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%';
}
});
} }
prevStep(){ prevStep(){
this.currentStep -= 1; this.currentStep -= 1;
this.formSteps.forEach((step: ElementRef<HTMLDivElement>, i: number) => { this.updateUI();
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%';
}
});
} }
PostJobListing(jobListing: JobListing){ PostJobListing(jobListing: JobListing){
@@ -24,6 +24,7 @@ export class Authentication{
let sub = this.http.post<Account>( "api/account/login", body, { headers } ); let sub = this.http.post<Account>( "api/account/login", body, { headers } );
sub.subscribe({ sub.subscribe({
next: data => { next: data => {
data.passwordHash = "";
this._user.next(data); this._user.next(data);
this.setUserToStorage(data, StayLoggedIn == true ? SessionType.Forever : SessionType.Session); this.setUserToStorage(data, StayLoggedIn == true ? SessionType.Forever : SessionType.Session);
}, },
@@ -1,7 +1,4 @@
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using BoredCareers.Services; using BoredCareers.Services;
using BoredCareers.Services.DatabaseService; using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities; using BoredCareers.Entities;
@@ -34,19 +31,9 @@ namespace BoredCareers.Controllers {
test.CurrentPasswordAttempts = 0; test.CurrentPasswordAttempts = 0;
await _databaseService.SetAccount(test); await _databaseService.SetAccount(test);
List<Claim> claims = new List<Claim>() { string jwt = BoredCareersJWT.GenereateJWTToken(test.ID, StayLoggedIn);
new Claim("ID", test.ID.ToString()), BoredCareersJWT.SignIn(Response, StayLoggedIn, jwt);
new Claim(ClaimTypes.NameIdentifier, 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 Ok(test); return Ok(test);
} else { } else {
test.CurrentPasswordAttempts += 1; test.CurrentPasswordAttempts += 1;
@@ -151,9 +138,9 @@ namespace BoredCareers.Controllers {
[Route("logout")] [Route("logout")]
[HttpPost] [HttpPost]
public async Task<ActionResult> Logout() { public ActionResult Logout() {
if (isLoggedIn()) { if (isLoggedIn()) {
await HttpContext.SignOutAsync(); BoredCareersJWT.SignOut(Response);
return Ok(); return Ok();
} }
return NotFound(); return NotFound();
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using BoredCareers.Entities; using BoredCareers.Entities;
using BoredCareers.Services.DatabaseService; using BoredCareers.Services.DatabaseService;
using System.Security.Claims;
namespace BoredCareers.Controllers { namespace BoredCareers.Controllers {
@@ -20,7 +21,7 @@ namespace BoredCareers.Controllers {
} }
public int getLoggedInUserID() { public int getLoggedInUserID() {
return Convert.ToInt32(User.FindFirst("ID")?.Value); return Convert.ToInt32(User.FindFirstValue(ClaimTypes.NameIdentifier));
} }
public async Task<Account> getLoggedInUser() { public async Task<Account> getLoggedInUser() {
+1 -1
View File
@@ -28,7 +28,7 @@ namespace BoredCareers.Controllers {
public async Task<IActionResult> paymentWebhook() { public async Task<IActionResult> paymentWebhook() {
try { try {
string body = await new StreamReader(Request.Body).ReadToEndAsync(); 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(); return Ok();
} catch (Exception ex) { } catch (Exception ex) {
return NotFound(ex.ToString()); return NotFound(ex.ToString());
@@ -1,5 +1,3 @@
using BoredCareers.Entities;
namespace BoredCareers.Controllers.Payment { namespace BoredCareers.Controllers.Payment {
public interface IPayment { public interface IPayment {
@@ -8,7 +6,7 @@ namespace BoredCareers.Controllers.Payment {
public static string _EndpointSecret = ""; public static string _EndpointSecret = "";
public static string _PublicKey = ""; 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; _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 ); Stripe.Event e = Stripe.EventUtility.ConstructEvent( WebHookData, Headers, IPayment._EndpointSecret );
if (e.Type == "payment_intent.succeeded") { if (e.Type == "payment_intent.succeeded") {
// Extract Data from payment confirm // Extract Data from payment confirm
+46 -9
View File
@@ -1,10 +1,13 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using BoredCareers.Controllers.Payment; using BoredCareers.Controllers.Payment;
using BoredCareers.Services; using BoredCareers.Services;
using BoredCareers.Services.DatabaseService; using BoredCareers.Services.DatabaseService;
using System.Threading.RateLimiting; using System.Threading.RateLimiting;
using Stripe; using Stripe;
using System.Security.Claims; 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); 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;"); DatabaseService databaseService = new DatabaseService(connectionString: "server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;");
builder.Services.Add( new ServiceDescriptor( typeof( DatabaseService ), databaseService ) ); 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 //////// ///////// Email Service ////////
//////////////////////////////// ////////////////////////////////
@@ -82,14 +94,39 @@ if (IPayment._PaymentType == PaymentType.StripeIntent) {
// Authentication Service // Authentication Service
builder.Services.AddAuthentication(options => { builder.Services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
} ).AddCookie(options => { options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.Cookie.HttpOnly = true; }).AddJwtBearer(options => {
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.TokenValidationParameters = new TokenValidationParameters {
options.Cookie.SameSite = SameSiteMode.Strict; ValidateIssuer = true,
options.LoginPath = "/account/login"; ValidateAudience = true,
options.LogoutPath = "/account/logout"; ValidateLifetime = true,
options.SlidingExpiration = true; ValidateIssuerSigningKey = true,
ValidIssuer = 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 => { 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="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.3.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.3.0" />
<PackageReference Include="MySql.Data" Version="9.2.0" /> <PackageReference Include="MySql.Data" Version="9.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
@@ -1,5 +1,3 @@
using System.Net.Mail;
namespace BoredCareers.Services { namespace BoredCareers.Services {
public partial class EmailService { public partial class EmailService {
@@ -1,5 +1,3 @@
using System.Net.Mail;
namespace BoredCareers.Services { namespace BoredCareers.Services {
public partial class EmailService { 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);
}
}
}