working #6
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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="half-frame">
|
<div class="split">
|
||||||
<label>Company Email</label>
|
<div class="half-frame">
|
||||||
<input name="email" [(ngModel)]="newListing.email" type="text" />
|
<label>Company Email</label>
|
||||||
</div>
|
<input class="input-field" name="email" [(ngModel)]="newListing.email" type="text" placeholder="Questions@mistox.com" />
|
||||||
<div class="half-frame">
|
</div>
|
||||||
<label>Company Phone Number</label>
|
<div class="half-frame">
|
||||||
<input name="email" [(ngModel)]="newListing.phone" type="text" />
|
<label>Company Phone Number</label>
|
||||||
|
<input class="input-field" name="email" [(ngModel)]="newListing.phone" type="text" placeholder="+1 800-000-0000" />
|
||||||
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<button type="button" (click)="prevStep()">Back</button>
|
|
||||||
<button type="button" (click)="nextStep()">Next</button>
|
|
||||||
</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) {
|
isNullOrEmpty(str: string | null | undefined): boolean {
|
||||||
step.nativeElement.style.left = '-100%';
|
return !str || str.trim().length === 0;
|
||||||
} else {
|
}
|
||||||
step.nativeElement.style.left = '100%';
|
|
||||||
}
|
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">
|
||||||
<h1>{{ cur.title }}</h1>
|
<div class="tile-title">
|
||||||
<h1>{{ cur.jobType }}</h1>
|
<h1>{{ cur.title }}</h1>
|
||||||
<h1>Is Remote: {{ cur.remote }}</h1>
|
<h2>${{ cur.salaryMax }} - ${{ cur.salaryMin }}</h2>
|
||||||
<h1>{{ cur.salaryMin }}</h1>
|
</div>
|
||||||
<h1>{{ cur.salaryMax }}</h1>
|
<div class="tile-split">
|
||||||
<h1>{{ cur.city }}</h1>
|
<h1>{{ cur.jobType }}</h1>
|
||||||
<h1>{{ cur.stateOrRegion }}</h1>
|
<h1 *ngIf="cur.remote" >Remote</h1>
|
||||||
<h1>{{ cur.country }}</h1>
|
</div>
|
||||||
<h1>{{ cur.postalCode }}</h1>
|
<div class="tile-split">
|
||||||
<h1>{{ cur.description }}</h1>
|
<h1>{{ cur.city }}</h1>
|
||||||
<h1>Posted: {{ cur.createdTime }}</h1>
|
<h1>{{ cur.stateOrRegion }}</h1>
|
||||||
<h1>Modified: {{ cur.modifiedTime }}</h1>
|
</div>
|
||||||
|
<div class="tile-button">
|
||||||
|
<button [routerLink]="['/jobs/new']">VIEW LISTING</button>
|
||||||
|
</div>
|
||||||
</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,25 +59,29 @@
|
|||||||
</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="half-frame">
|
<div class="content-frame split" style="border-radius: 10px 10px 0 0;">
|
||||||
<label>City</label>
|
<div class="half-frame">
|
||||||
<input name="city" [(ngModel)]="newListing.city" type="text" />
|
<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>
|
||||||
<div class="half-frame">
|
<div class="content-frame split" style="border-radius: 0 0 10px 10px;">
|
||||||
<label>2 Letter Country</label>
|
<div class="half-frame">
|
||||||
<input name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" />
|
<label>2 Letter Country</label>
|
||||||
</div>
|
<input name="country" maxlength="2" minlength="2" [(ngModel)]="newListing.country" type="text" />
|
||||||
<div class="half-frame">
|
</div>
|
||||||
<label>2 Letter State/Region</label>
|
<div class="half-frame">
|
||||||
<input name="stateOrRegion" maxlength="2" minlength="2" [(ngModel)]="newListing.stateOrRegion" type="text" />
|
<label>Postal Code</label>
|
||||||
</div>
|
<input name="postalCode" [(ngModel)]="newListing.postalCode" type="text" />
|
||||||
<div class="half-frame">
|
</div>
|
||||||
<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)="prevStep()">Back</button>
|
||||||
<button type="button" (click)="nextStep()">Next</button>
|
<button type="button" (click)="nextStep()">Next</button>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+47
-10
@@ -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 ////////
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
@@ -81,15 +93,40 @@ 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 => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user