Merge pull request 'working' (#6) from working into main
Docker Build and Release Upload / build (push) Successful in 1m29s
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:
+2
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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