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

Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
2025-08-12 04:28:30 +00:00
21 changed files with 540 additions and 45 deletions
+6 -3
View File
@@ -21,6 +21,9 @@ Server:
Need to update notification email Need to update notification email
Create page to notify cx that their work email has been verified Create page to notify cx that their work email has been verified
Server.csproj:
Find a way to keep all the libraries up to date
Client: Client:
jobs/editor: jobs/editor:
Job Listing Skills exists but isn't implimented in the UI Job Listing Skills exists but isn't implimented in the UI
@@ -45,14 +48,14 @@ Client:
Setup QueryParam's for Edit and New Setup QueryParam's for Edit and New
Edit employees not implimented yet Edit employees not implimented yet
resume/editor:
Not fully tested yet
Company: Company:
Need to impliment Add employee Need to impliment Add employee
Need to impliment Remove employee Need to impliment Remove employee
Edit Company -> Dont allow edit of company email due to it being verified Edit Company -> Dont allow edit of company email due to it being verified
All:
Make sure im using the new NG format for for and if
database: database:
Add Applied Jobs Table Add Applied Jobs Table
+2
View File
@@ -6,6 +6,7 @@ USE `boredcareers`;
CREATE TABLE IF NOT EXISTS `Resume` ( CREATE TABLE IF NOT EXISTS `Resume` (
`ID` int NOT NULL AUTO_INCREMENT, `ID` int NOT NULL AUTO_INCREMENT,
`AccountID` int NOT NULL, `AccountID` int NOT NULL,
`Title` varchar(100) NOT NULL,
`Name` varchar(100) NOT NULL, `Name` varchar(100) NOT NULL,
`Field` varchar(100) DEFAULT NULL, `Field` varchar(100) DEFAULT NULL,
`Email` varchar(255) NOT NULL, `Email` varchar(255) NOT NULL,
@@ -47,6 +48,7 @@ CREATE TABLE IF NOT EXISTS `Resume` (
CREATE TABLE IF NOT EXISTS `ResumeMilitary` ( CREATE TABLE IF NOT EXISTS `ResumeMilitary` (
`ID` int NOT NULL AUTO_INCREMENT, `ID` int NOT NULL AUTO_INCREMENT,
`ResumeID` int NOT NULL, `ResumeID` int NOT NULL,
`Veteran` boolean DEFAULT 0,
`Country` char(2) NOT NULL, `Country` char(2) NOT NULL,
`Rank` varchar(50) NOT NULL, `Rank` varchar(50) NOT NULL,
`DateStarted` date NOT NULL, `DateStarted` date NOT NULL,
+12 -7
View File
@@ -1,25 +1,30 @@
<div class="top-bar"> <div class="top-bar">
<div class="top-bar-buttons"> <div class="top-bar-buttons">
<a class="nav-button" routerLink="/jobs" routerLinkActive="active">JOB BOARD</a> <a class="nav-button" routerLink="/jobs" routerLinkActive="active">JOB BOARD</a>
@if (auth.isLoggedIn){
<a class="nav-button" routerLink="/resumes" routerLinkActive="active">RESUMES</a> <a class="nav-button" routerLink="/resumes" routerLinkActive="active">RESUMES</a>
<a class="nav-button" routerLink="/company" routerLinkActive="active">COMPANIES</a> <a class="nav-button" routerLink="/company" routerLinkActive="active">COMPANIES</a>
}
</div> </div>
<a class="top-bar-logo" routerLink=""> <a class="top-bar-logo" routerLink="">
<img class="top-bar-logo" style="margin: 0;" src="img/logo-full.png" /> <img class="top-bar-logo" style="margin: 0;" src="img/logo-full.png" />
</a> </a>
<div *ngIf="auth.isLoggedIn" class="top-bar-buttons flex-right"> @if (auth.isLoggedIn){
<div class="top-bar-buttons flex-right">
<a class="nav-button nav-button-login" href="https://auth.mistox.com/"><span>{{ auth.loggedInUser.userName.toUpperCase() }}</span></a> <a class="nav-button nav-button-login" href="https://auth.mistox.com/"><span>{{ auth.loggedInUser.userName.toUpperCase() }}</span></a>
<a class="nav-button nav-button-login" href="/api/account/logout"><span>LOGOUT</span></a> <a class="nav-button nav-button-login" href="/api/account/logout"><span>LOGOUT</span></a>
</div> </div>
<div *ngIf="!auth.isLoggedIn" class="top-bar-buttons flex-right"> } @else {
<div class="top-bar-buttons flex-right">
<!-- Testing Login --> @if (devMode){
<a *ngIf="devMode" class="nav-button nav-button-login" href="https://auth.mistox.com/account/login?returnURL=http://localhost:5000/"><span>Testing Login</span></a> <a class="nav-button nav-button-login" href="https://auth.mistox.com/account/login?returnURL=http://localhost:5000/"><span>Testing Login</span></a>
<!-- Testing Login --> <a class="nav-button nav-button-login" href="https://auth.mistox.com/account/register?returnURL=http://localhost:5000/"><span>REGISTER</span></a>
} @else {
<a class="nav-button nav-button-login" href="https://auth.mistox.com/account/login?returnURL=https://boredcareers.com/"><span>LOGIN</span></a> <a class="nav-button nav-button-login" href="https://auth.mistox.com/account/login?returnURL=https://boredcareers.com/"><span>LOGIN</span></a>
<a class="nav-button nav-button-login" href="https://auth.mistox.com/account/register?returnURL=https://boredcareers.com/"><span>REGISTER</span></a> <a class="nav-button nav-button-login" href="https://auth.mistox.com/account/register?returnURL=https://boredcareers.com/"><span>REGISTER</span></a>
}
</div> </div>
}
</div> </div>
<div class="content"> <div class="content">
<router-outlet></router-outlet> <router-outlet></router-outlet>
+2
View File
@@ -7,6 +7,7 @@ import { JobEditorComponent } from './pages/jobs/editor/jobeditor.component';
import { CompanyEditorComponent } from './pages/company/editor/editor.component'; import { CompanyEditorComponent } from './pages/company/editor/editor.component';
import { JobViewerComponent } from './pages/jobs/viewer/jobviewer.component'; import { JobViewerComponent } from './pages/jobs/viewer/jobviewer.component';
import { CompanyComponent } from './pages/company/company.component'; import { CompanyComponent } from './pages/company/company.component';
import { ResumesEditorComponent } from './pages/resumes/editor/editor.component';
export const routes: Routes = [ export const routes: Routes = [
@@ -15,6 +16,7 @@ export const routes: Routes = [
// Resumes // Resumes
{ path: "resumes", component: ResumesComponent }, { path: "resumes", component: ResumesComponent },
{ path: "resumes/editor", component: ResumesEditorComponent },
// Jobs // Jobs
{ path: "jobs", component: JobsComponent }, { path: "jobs", component: JobsComponent },
+35 -12
View File
@@ -1,6 +1,9 @@
export class Resume { export class Resume {
public id: number | null = null; public id: number | null = null;
public accountID: number = 0; public accountID: number | null = null;
public title: string = "";
public name: string = ""; public name: string = "";
public field: string = ""; public field: string = "";
public email: string = ""; public email: string = "";
@@ -17,11 +20,13 @@ export class Resume {
public languages: ResumeLanguage[] = []; public languages: ResumeLanguage[] = [];
public certification: ResumeCertification[] = []; public certification: ResumeCertification[] = [];
public projects: ResumeProject[] = []; public projects: ResumeProject[] = [];
public trackUUID: string = crypto.randomUUID();
} }
export class ResumeExperience { export class ResumeExperience {
public id: number | null = null; public id: number | null = null;
public resumeID: number = 0; public resumeID: number | null = null;
public jobTitle: string = ""; public jobTitle: string = "";
public company: string = ""; public company: string = "";
public postalCode: string = ""; public postalCode: string = "";
@@ -32,37 +37,46 @@ export class ResumeExperience {
public stillEmployed: boolean = false; public stillEmployed: boolean = false;
public dateEnded: Date = new Date(); public dateEnded: Date = new Date();
public experienceBullets: ResumeExperienceBullet[] = []; public experienceBullets: ResumeExperienceBullet[] = [];
public trackUUID: string = crypto.randomUUID();
} }
export class ResumeExperienceBullet { export class ResumeExperienceBullet {
public id: number | null = null; public id: number | null = null;
public resumeID: number = 0; public resumeID: number | null = null;
public resumeExperienceID: number = 0; public resumeExperienceID: number | null = null;
public jobFunction: string = ""; public jobFunction: string = "";
public trackUUID: string = crypto.randomUUID();
} }
export class ResumeMilitary { export class ResumeMilitary {
public id: number | null = null; public id: number | null = null;
public resumeID: number = 0; public resumeID: number | null = null;
public veteran: boolean = false;
public country: string = ""; public country: string = "";
public rank: string = ""; public rank: string = "";
public dateStarted: Date = new Date(); public dateStarted: Date = new Date();
public stillServing: boolean = false; public stillServing: boolean = false;
public dateEnded: Date = new Date(); public dateEnded: Date = new Date();
public millitaryBullets: ResumeMilitaryBullet[] = []; public millitaryBullets: ResumeMilitaryBullet[] = [];
public trackUUID: string = crypto.randomUUID();
} }
export class ResumeMilitaryBullet { export class ResumeMilitaryBullet {
public id: number | null = null; public id: number | null = null;
public resumeID: number = 0; public resumeID: number | null = null;
public resumeMilitaryID: number = 0; public resumeMilitaryID: number | null = null;
public achievement: string = ""; public achievement: string = "";
public description: string = ""; public description: string = "";
public trackUUID: string = crypto.randomUUID();
} }
export class ResumeEducation { export class ResumeEducation {
public id: number | null = null; public id: number | null = null;
public resumeID: number = 0; public resumeID: number | null = null;
public degreeType: string = ""; public degreeType: string = "";
public degreeField: string = ""; public degreeField: string = "";
public school: string = ""; public school: string = "";
@@ -73,34 +87,43 @@ export class ResumeEducation {
public dateStarted: Date = new Date(); public dateStarted: Date = new Date();
public stillStudying: boolean = false; public stillStudying: boolean = false;
public dateEnded: Date = new Date(); public dateEnded: Date = new Date();
public trackUUID: string = crypto.randomUUID();
} }
export class ResumeSkill { export class ResumeSkill {
public id: number | null = null; public id: number | null = null;
public resumeID: number = 0; public resumeID: number | null = null;
public name: string = ""; public name: string = "";
public description: string = ""; public description: string = "";
public trackUUID: string = crypto.randomUUID();
} }
export class ResumeLanguage { export class ResumeLanguage {
public id: number | null = null; public id: number | null = null;
public resumeID: number = 0; public resumeID: number | null = null;
public language: string = ""; public language: string = "";
public proficiency: string = ""; public proficiency: string = "";
public trackUUID: string = crypto.randomUUID();
} }
export class ResumeCertification { export class ResumeCertification {
public id: number | null = null; public id: number | null = null;
public resumeID: number = 0; public resumeID: number | null = null;
public name: string = ""; public name: string = "";
public verificationURL: string = ""; public verificationURL: string = "";
public description: string = ""; public description: string = "";
public trackUUID: string = crypto.randomUUID();
} }
export class ResumeProject { export class ResumeProject {
public id: number | null = null; public id: number | null = null;
public resumeID: number = 0; public resumeID: number | null = null;
public name: string = ""; public name: string = "";
public url: string = ""; public url: string = "";
public description: string = ""; public description: string = "";
public trackUUID: string = crypto.randomUUID();
} }
@@ -28,6 +28,11 @@ export class CompanyComponent {
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Companies | BoredCareers"); this.title.setTitle("Companies | BoredCareers");
if (!auth.isLoggedIn){
router.navigate(["/"]);
}
http.get<Employee[]>("api/employee/").subscribe({ http.get<Employee[]>("api/employee/").subscribe({
next: data => { next: data => {
this.Employers = data; this.Employers = data;
@@ -25,6 +25,10 @@ export class CompanyEditorComponent {
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Company - Editor | BoredCareers"); this.title.setTitle("Company - Editor | BoredCareers");
if (!auth.isLoggedIn){
router.navigate(["/"]);
}
// Query param CompanyID -> Edit // Query param CompanyID -> Edit
// Query param null -> New // Query param null -> New
}; };
@@ -27,6 +27,10 @@ export class JobEditorComponent {
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Jobs - Editor | BoredCareers"); this.title.setTitle("Jobs - Editor | BoredCareers");
if (!auth.isLoggedIn){
router.navigate(["/"]);
}
this.route.queryParams.subscribe(params => { this.route.queryParams.subscribe(params => {
const CompanyID = params['CompanyID'] ? +params['CompanyID'] : null; const CompanyID = params['CompanyID'] ? +params['CompanyID'] : null;
const JobID = params['JobID'] ? +params['JobID'] : null; const JobID = params['JobID'] ? +params['JobID'] : null;
@@ -0,0 +1,10 @@
.resume-section {
background-color: #DDDDDD;
margin: 10px;
}
.resume-sub-section {
border: 1px solid #666666;
margin: 5px;
padding: 10px;
}
@@ -0,0 +1,145 @@
<form (ngSubmit)="SubmitForm(resume)">
<!-- Resume Header -->
<div class="resume-section">
<input name="resumetitle" [(ngModel)]="resume.title" type="text" placeholder="Resume 1" />
<input name="resumename" [(ngModel)]="resume.name" type="text" placeholder="John Doe" />
<input name="resumefield" [(ngModel)]="resume.field" type="text" placeholder="Data Scientist" />
<input name="resumeemail" [(ngModel)]="resume.email" type="email" placeholder="no-reply@mistox.com" />
<input name="resumephoneNumber" [(ngModel)]="resume.phoneNumber" type="tel" placeholder="+1 800-000-0000" />
<input name="resumepostalCode" [(ngModel)]="resume.postalCode" type="text" placeholder="92020" />
<input name="resumestateOrRegion" [(ngModel)]="resume.stateOrRegion" type="text" placeholder="CA" />
<input name="resumecity" [(ngModel)]="resume.city" type="text" placeholder="San Diego" />
<h1>Public: </h1><input name="active" [(ngModel)]="resume.isActive" type="checkbox" />
</div>
<!-- Experience -->
<div class="resume-section">
<button type="button" (click)="addExperience()">ADD Experience</button>
@for(experience of resume.experience; track experience.trackUUID ){
<div class="resume-sub-section">
<input name="experiencejobTitle" [(ngModel)]="experience.jobTitle" type="text" placeholder="Data Entry Clerk" />
<input name="experiencecompany" [(ngModel)]="experience.company" type="text" placeholder="San Diego Gas Electric" />
<input name="experiencepostalCode" [(ngModel)]="experience.postalCode" type="text" placeholder="92020" />
<input name="experiencecountry" [(ngModel)]="experience.country" type="text" placeholder="US" />
<input name="experiencestateOrRegion" [(ngModel)]="experience.stateOrRegion" type="text" placeholder="CA" />
<input name="experiencecity" [(ngModel)]="experience.city" type="text" placeholder="San Diego" />
<input name="experiencedateStarted" [(ngModel)]="experience.dateStarted" type="date" />
<input name="experiencestillEmployed" [(ngModel)]="experience.stillEmployed" type="checkbox" />
@if(!experience.stillEmployed){
<input name="experiencedateEnded" [(ngModel)]="experience.dateEnded" type="date" />
}
<button type="button" (click)="addJobFunction(experience)">Add jobFunction</button>
@for(bullet of experience.experienceBullets; track bullet.trackUUID){
<div>
<textarea name="bulletjobFunction" [(ngModel)]="bullet.jobFunction" placeholder="Processed database transactions" ></textarea>
<button type="button" (click)="delJobFunction(experience, bullet)">DEL jobFunction</button>
</div>
}
<button type="button" (click)="delExperience(experience)">DEL Experience</button>
</div>
}
</div>
<!-- Military -->
<div class="resume-section">
<h1>Is Veteran: </h1><input name="veteran" [(ngModel)]="resume.military.veteran" type="checkbox" />
@if(resume.military.veteran){
<input name="militarycountry" [(ngModel)]="resume.military.country" type="text" placeholder="US" />
<input name="militaryrank" [(ngModel)]="resume.military.rank" type="text" placeholder="PVT" />
<input name="militarydateStarted" [(ngModel)]="resume.military.dateStarted" type="date" />
<h1>Still Serving: </h1><input name="stillServing" [(ngModel)]="resume.military.stillServing" type="checkbox" />
@if (!resume.military.stillServing){
<input name="dateEnded" [(ngModel)]="resume.military.dateEnded" type="date" />
}
<button type="button" (click)="addMillitaryBullet()">Add Millitary Task</button>
@for(military of resume.military.millitaryBullets; track military.trackUUID ){
<div>
<input name="militaryachievement" [(ngModel)]="military.achievement" type="text" placeholder="Deployed Kuwait" />
<textarea name="militarydescription" [(ngModel)]="military.description" placeholder="Delivered goods line-hall" ></textarea>
<button type="button" (click)="delMillitaryBullet(military)">DEL Military Task</button>
</div>
}
}
</div>
<!-- Education -->
<div class="resume-section">
<button type="button" (click)="addEducation()">ADD Education</button>
@for(education of resume.education; track education.trackUUID){
<div>
<input name="educationdegreeType" [(ngModel)]="education.degreeType" type="text" placeholder="Masters" />
<input name="educationdegreeField" [(ngModel)]="education.degreeField" type="text" placeholder="Computer Science" />
<input name="educationschool" [(ngModel)]="education.school" type="text" placeholder="WGU" />
<input name="educationpostalCode" [(ngModel)]="education.postalCode" type="text" placeholder="84107" />
<input name="educationcountry" [(ngModel)]="education.country" type="text" placeholder="US" />
<input name="educationstateOrRegion" [(ngModel)]="education.stateOrRegion" type="text" placeholder="UT" />
<input name="educationcity" [(ngModel)]="education.city" type="text" placeholder="Salt Lake City" />
<input name="educationdateStarted" [(ngModel)]="education.dateStarted" type="date" />
<input name="educationstillStudying" [(ngModel)]="education.stillStudying" type="checkbox" />
@if (!education.stillStudying){
<input name="educationdateEnded" [(ngModel)]="education.dateEnded" type="date" />
}
<button type="button" (click)="delEducation(education)">DEL Education</button>
</div>
}
</div>
<!-- Skill -->
<div class="resume-section">
<button type="button" (click)="addSkill()">ADD Skill</button>
@for(skill of resume.skills; track skill.trackUUID){
<div>
<input name="skillname" [(ngModel)]="skill.name" type="text" placeholder="Angular JS" />
<textarea name="skilldescription" [(ngModel)]="skill.description" placeholder="Built this entire website using Angular JS"></textarea>
<button type="button" (click)="delSkill(skill)">DEL Skill</button>
</div>
}
</div>
<!-- Language -->
<div class="resume-section">
<button type="button" (click)="addLanguage()">ADD Language</button>
@for(language of resume.languages; track language.trackUUID){
<div>
<input name="languagelanguage" [(ngModel)]="language.language" type="text" placeholder="Spanish" />
<input name="languageproficiency" [(ngModel)]="language.proficiency" type="text" placeholder="casual speaking" />
<button type="button" (click)="delLanguage(language)">DEL Language</button>
</div>
}
</div>
<!-- Certification -->
<div class="resume-section">
<button type="button" (click)="addCert()">ADD Certification</button>
@for(cert of resume.certification; track cert.trackUUID){
<div>
<input name="certname" [(ngModel)]="cert.name" type="text" placeholder="Comptia A+" />
<input name="certverificationURL" [(ngModel)]="cert.verificationURL" type="text" placeholder="https://certmaster.com/certid" />
<textarea name="certdescription" [(ngModel)]="cert.description" placeholder="Into to information technology"></textarea>
<button type="button" (click)="delCert(cert)">DEL Certification</button>
</div>
}
</div>
<!-- Project -->
<div class="resume-section">
<button type="button" (click)="addProject()">ADD Project</button>
@for(proj of resume.projects; track proj.trackUUID){
<div>
<input name="projname" [(ngModel)]="proj.name" type="text" placeholder="boredcareers" />
<input name="projurl" [(ngModel)]="proj.url" type="text" placeholder="mistox.com" />
<textarea name="projdescription" [(ngModel)]="proj.description" placeholder="the project that your currently viewing"></textarea>
<button type="button" (click)="delProject(proj)">DEL Project</button>
</div>
}
</div>
@if (isNewResume){
<button type="submit">CREATE NEW RESUME</button>
} @else {
<button type="submit">UPDATE RESUME</button>
}
</form>
@@ -0,0 +1,162 @@
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { Router, ActivatedRoute, RouterModule } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { Resume, ResumeCertification, ResumeEducation, ResumeExperience, ResumeExperienceBullet, ResumeLanguage, ResumeMilitaryBullet, ResumeProject, ResumeSkill } from 'app/models/Resume';
import { Authentication } from 'app/services/Authentication';
import { HomeComponent } from "app/pages/home/home.component";
@Component({
selector: 'main-resume-editor',
templateUrl: './editor.component.html',
styleUrls: [ './editor.component.css' ],
imports: [FormsModule, CommonModule, RouterModule]
})
export class ResumesEditorComponent {
public resume: Resume = new Resume;
public isNewResume: boolean = true;
public ErrorMsg: string = "";
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Resume - Editor | BoredCareers");
if (!auth.isLoggedIn){
router.navigate(["/"]);
}
this.route.queryParams.subscribe(params => {
const ResumeID = params['ResumeID'] ? +params['ResumeID'] : null;
if (ResumeID !== null){
this.http.get<Resume>("api/resume?ResumeID=" + ResumeID).subscribe({
next: data => {
this.resume = data;
this.isNewResume = false;
},
error: err => {
this.ErrorMsg = err.error;
}
});
}
});
};
SubmitForm(resume: Resume){
resume.accountID = this.auth.loggedInUser.id;
this.http.post("api/resume", resume).subscribe({
next: data => {
this.router.navigate(["/"]);
},
error: err => {
this.ErrorMsg = err.error;
}
});
}
addExperience(){
this.resume.experience.push( new ResumeExperience );
}
delExperience(self: ResumeExperience){
for(let i=0; i<this.resume.experience.length; i++){
let cur = this.resume.experience[i];
if (cur.trackUUID === self.trackUUID){
this.resume.experience.splice( i, 1 );
break;
}
}
}
addJobFunction(self: ResumeExperience){
self.experienceBullets.push( new ResumeExperienceBullet );
}
delJobFunction(self: ResumeExperience, me: ResumeExperienceBullet){
for(let i=0; i<self.experienceBullets.length; i++){
let cur = self.experienceBullets[i];
if (cur.trackUUID === me.trackUUID){
self.experienceBullets.splice( i, 1 );
break;
}
}
}
addMillitaryBullet(){
this.resume.military.millitaryBullets.push( new ResumeMilitaryBullet );
}
delMillitaryBullet(self: ResumeMilitaryBullet){
for(let i=0; i<this.resume.military.millitaryBullets.length; i++){
let cur = this.resume.military.millitaryBullets[i];
if (cur.trackUUID === self.trackUUID){
this.resume.military.millitaryBullets.splice( i, 1 );
break;
}
}
}
addEducation(){
this.resume.education.push( new ResumeEducation );
}
delEducation(self: ResumeEducation){
for(let i=0; i<this.resume.education.length; i++){
let cur = this.resume.education[i];
if (cur.trackUUID === self.trackUUID){
this.resume.education.splice( i, 1 );
break;
}
}
}
addSkill(){
this.resume.skills.push( new ResumeSkill );
}
delSkill(self: ResumeSkill){
for(let i=0; i<this.resume.skills.length; i++){
let cur = this.resume.skills[i];
if (cur.trackUUID === self.trackUUID){
this.resume.skills.splice( i, 1 );
break;
}
}
}
addLanguage(){
this.resume.languages.push( new ResumeLanguage );
}
delLanguage(self: ResumeLanguage){
for(let i=0; i<this.resume.languages.length; i++){
let cur = this.resume.languages[i];
if (cur.trackUUID === self.trackUUID){
this.resume.languages.splice( i, 1 );
break;
}
}
}
addCert(){
this.resume.certification.push( new ResumeCertification );
}
delCert(self: ResumeCertification){
for(let i=0; i<this.resume.certification.length; i++){
let cur = this.resume.certification[i];
if (cur.trackUUID === self.trackUUID){
this.resume.certification.splice( i, 1 );
break;
}
}
}
addProject(){
this.resume.projects.push( new ResumeProject );
}
delProject(self: ResumeProject){
for(let i=0; i<this.resume.projects.length; i++){
let cur = this.resume.projects[i];
if (cur.trackUUID === self.trackUUID){
this.resume.projects.splice( i, 1 );
break;
}
}
}
}
@@ -1,6 +1,11 @@
<div class="top-bar">
@for(resume of myResumes; track myResumes.length){
<button (click)="changeSelectedResume(5)">{{ resume.title }}</button>
}
<button routerLink="/resumes/editor" >NEW RESUME</button>
</div>
<!-- My Resumes --> <!-- My Resumes -->
@if (auth.isLoggedIn){
<div class="jobs-frame"> <div class="jobs-frame">
</div> </div>
}
@@ -15,14 +15,19 @@ import { Authentication } from 'app/services/Authentication';
}) })
export class ResumesComponent { export class ResumesComponent {
public ResumePage: Resume[] = []; public myResumes: Resume[] = [];
public currentResume: Resume | null = null;
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Resumes | BoredCareers"); this.title.setTitle("Resumes | BoredCareers");
if (!auth.isLoggedIn){
router.navigate(["/"]);
}
this.http.get<Resume[]>("api/resume").subscribe({ this.http.get<Resume[]>("api/resume").subscribe({
next: data => { next: data => {
this.ResumePage = data; this.myResumes = data;
}, },
error: err => { error: err => {
console.log("Error fetching resumes: " + err.error); console.log("Error fetching resumes: " + err.error);
@@ -30,4 +35,15 @@ export class ResumesComponent {
}); });
}; };
changeSelectedResume(ResumeID: number){
this.http.get<Resume>("api/resume?ResumeID=" + ResumeID).subscribe({
next: data => {
this.currentResume = data;
},
error: err => {
console.log("Error fetching resume ID: " + err.error);
}
});
}
} }
@@ -1,7 +1,7 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { Account } from "../models/Account"; import { Account } from "../models/Account";
import { BehaviorSubject, Observable } from "rxjs"; import { BehaviorSubject, Observable } from "rxjs";
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class Authentication{ export class Authentication{
+2
View File
@@ -2,6 +2,7 @@ namespace BoredCareers.Entities {
public class Resume { public class Resume {
public int? ID { get; set; } // PK public int? ID { get; set; } // PK
public int AccountID { get; set; } // FK public int AccountID { get; set; } // FK
public string Title { get; set; } = "";
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string Field { get; set; } = ""; public string Field { get; set; } = "";
public string Email { get; set; } = ""; public string Email { get; set; } = "";
@@ -45,6 +46,7 @@ namespace BoredCareers.Entities {
public class ResumeMilitary { public class ResumeMilitary {
public int? ID { get; set; } // PK public int? ID { get; set; } // PK
public int ResumeID { get; set; } // FK public int ResumeID { get; set; } // FK
public bool Veteran { get; set; } = false;
public string Country { get; set; } = ""; // 2 Letter Country Code public string Country { get; set; } = ""; // 2 Letter Country Code
public string Rank { get; set; } = ""; public string Rank { get; set; } = "";
public DateTime DateStarted { get; set; } = new DateTime(); public DateTime DateStarted { get; set; } = new DateTime();
+1
View File
@@ -175,6 +175,7 @@ builder.Services.AddRateLimiter(options => {
//////////////////////////////// ////////////////////////////////
builder.Services.AddHostedService<JobCleanupService>(); builder.Services.AddHostedService<JobCleanupService>();
ResumeService.init();
//////////////////////////////// ////////////////////////////////
///// ASPNET Core Function ///// ///// ASPNET Core Function /////
+6 -3
View File
@@ -8,15 +8,18 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.3" /> <PackageReference Include="Microsoft.AspNetCore.Components.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.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="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Stripe.net" Version="48.2.0" /> <PackageReference Include="Stripe.net" Version="48.2.0" />
<PackageReference Include="MySql.Data" Version="9.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Net.Http" Version="4.3.4" /> <PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="HtmlSanitizer" Version="9.0.886" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -27,6 +27,7 @@ namespace BoredCareers.Services.DatabaseService {
int _id = reader.GetInt32("ID"); int _id = reader.GetInt32("ID");
int _accountid = reader.GetInt32("AccountID"); int _accountid = reader.GetInt32("AccountID");
string _title = reader.GetString("Title");
string _name = reader.GetString("Name"); string _name = reader.GetString("Name");
string _field = reader.GetString("Field"); string _field = reader.GetString("Field");
string _email = reader.GetString("Email"); string _email = reader.GetString("Email");
@@ -40,6 +41,7 @@ namespace BoredCareers.Services.DatabaseService {
resumes.Add( new Resume() { resumes.Add( new Resume() {
ID = _id, ID = _id,
AccountID = _accountid, AccountID = _accountid,
Title = _title,
Name = _name, Name = _name,
Field = _field, Field = _field,
Email = _email, Email = _email,
@@ -10,6 +10,7 @@ namespace BoredCareers.Services.DatabaseService {
if (reader == null) { break; } if (reader == null) { break; }
int _id = reader.GetInt32("ID"); int _id = reader.GetInt32("ID");
int _accountid = reader.GetInt32("AccountID"); int _accountid = reader.GetInt32("AccountID");
string _title = reader.GetString("Title");
string _name = reader.GetString("Name"); string _name = reader.GetString("Name");
string _field = reader.GetString("Field"); string _field = reader.GetString("Field");
string _email = reader.GetString("Email"); string _email = reader.GetString("Email");
@@ -22,6 +23,7 @@ namespace BoredCareers.Services.DatabaseService {
return new Resume() { return new Resume() {
ID = _id, ID = _id,
AccountID = _accountid, AccountID = _accountid,
Title = _title,
Name = _name, Name = _name,
Field = _field, Field = _field,
Email = _email, Email = _email,
@@ -112,6 +114,7 @@ namespace BoredCareers.Services.DatabaseService {
if (reader == null) { break; } if (reader == null) { break; }
int _id = reader.GetInt32("ID"); int _id = reader.GetInt32("ID");
int _resumeid = reader.GetInt32("ResumeID"); int _resumeid = reader.GetInt32("ResumeID");
bool _veteran = reader.GetBoolean("Veteran");
string _country = reader.GetString("Country"); string _country = reader.GetString("Country");
string _rank = reader.GetString("Rank"); string _rank = reader.GetString("Rank");
DateTime _datestarted = reader.GetDateTime("DateStarted"); DateTime _datestarted = reader.GetDateTime("DateStarted");
@@ -120,6 +123,7 @@ namespace BoredCareers.Services.DatabaseService {
military = new ResumeMilitary() { military = new ResumeMilitary() {
ID = _id, ID = _id,
ResumeID = _resumeid, ResumeID = _resumeid,
Veteran = _veteran,
Country = _country, Country = _country,
Rank = _rank, Rank = _rank,
DateStarted = _datestarted, DateStarted = _datestarted,
@@ -7,11 +7,12 @@ namespace BoredCareers.Services.DatabaseService {
public async Task SetResume(MySqlConnection connection, Resume resume) { public async Task SetResume(MySqlConnection connection, Resume resume) {
string command = @" string command = @"
INSERT INTO Resume INSERT INTO Resume
(ID,AccountID,Name,Field,Email,PhoneNumber,PostalCode,Country,StateOrRegion,City,IsActive) (ID,AccountID,Title,Name,Field,Email,PhoneNumber,PostalCode,Country,StateOrRegion,City,IsActive)
VALUES VALUES
(@ID,@AccountID,@Name,@Field,@Email,@PhoneNumber,@PostalCode,@Country,@StateOrRegion,@City,@IsActive) (@ID,@AccountID,@Title,@Name,@Field,@Email,@PhoneNumber,@PostalCode,@Country,@StateOrRegion,@City,@IsActive)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
AccountID = @AccountID, AccountID = @AccountID,
Title = @Title,
Name = @Name, Name = @Name,
Field = @Field, Field = @Field,
Email = @Email, Email = @Email,
@@ -26,6 +27,7 @@ namespace BoredCareers.Services.DatabaseService {
MySqlCommand cmd = new MySqlCommand(command, connection); MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ID", resume.ID); cmd.Parameters.AddWithValue("@ID", resume.ID);
cmd.Parameters.AddWithValue("@AccountID", resume.AccountID); cmd.Parameters.AddWithValue("@AccountID", resume.AccountID);
cmd.Parameters.AddWithValue("@Title", resume.Title);
cmd.Parameters.AddWithValue("@Name", resume.Name); cmd.Parameters.AddWithValue("@Name", resume.Name);
cmd.Parameters.AddWithValue("@Field", resume.Field); cmd.Parameters.AddWithValue("@Field", resume.Field);
cmd.Parameters.AddWithValue("@Email", resume.Email); cmd.Parameters.AddWithValue("@Email", resume.Email);
@@ -124,11 +126,12 @@ namespace BoredCareers.Services.DatabaseService {
public async Task SetResumeMilitary(MySqlConnection connection, ResumeMilitary military) { public async Task SetResumeMilitary(MySqlConnection connection, ResumeMilitary military) {
string command = @" string command = @"
INSERT INTO Resume INSERT INTO Resume
(ID,ResumeID,Country,Rank,DateStarted,StillServing,DateEnded) (ID,ResumeID,Veteran,Country,Rank,DateStarted,StillServing,DateEnded)
VALUES VALUES
(@ID,@ResumeID,@Country,@Rank,@DateStarted,@StillServing,@DateEnded) (@ID,@ResumeID,@Veteran,@Country,@Rank,@DateStarted,@StillServing,@DateEnded)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
ResumeID = @ResumeID, ResumeID = @ResumeID,
Veteran = @Veteran,
Country = @Country, Country = @Country,
Rank = @Rank, Rank = @Rank,
DateStarted = @DateStarted, DateStarted = @DateStarted,
@@ -139,6 +142,7 @@ namespace BoredCareers.Services.DatabaseService {
MySqlCommand cmd = new MySqlCommand(command, connection); MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ID", military.ID); cmd.Parameters.AddWithValue("@ID", military.ID);
cmd.Parameters.AddWithValue("@ResumeID", military.ResumeID); cmd.Parameters.AddWithValue("@ResumeID", military.ResumeID);
cmd.Parameters.AddWithValue("@Veteran", military.Veteran);
cmd.Parameters.AddWithValue("@Country", military.Country); cmd.Parameters.AddWithValue("@Country", military.Country);
cmd.Parameters.AddWithValue("@Rank", military.Rank); cmd.Parameters.AddWithValue("@Rank", military.Rank);
cmd.Parameters.AddWithValue("@DateStarted", military.DateStarted.ToUniversalTime()); cmd.Parameters.AddWithValue("@DateStarted", military.DateStarted.ToUniversalTime());
+93
View File
@@ -0,0 +1,93 @@
using Ganss.Xss;
namespace BoredCareers.Services {
public class ResumeService {
static HtmlSanitizer _self = new HtmlSanitizer();
public static void init() {
// Clear default allowed tags and attributes
_self.AllowedAttributes.Clear();
_self.AllowedSchemes.Clear();
_self.AllowedAtRules.Clear();
_self.AllowedClasses.Clear();
// Allowed HTML Tags
_self.AllowedTags.Clear();
string[] safeTags = [
"b", "strong", "i", "em", "u", "small", "mark", "del", "ins", "sub", "sup",
"p", "br", "hr", "div", "span",
"section", "article", "header", "footer", "aside", "main", "nav",
"ul", "ol", "li", "dl", "dt", "dd",
"h1", "h2", "h3", "h4", "h5", "h6",
"blockquote", "q", "cite",
"code", "pre", "samp", "kbd", "var",
"table", "thead", "tbody", "tfoot", "tr", "td", "th",
];
foreach (string cur in safeTags) {
_self.AllowedTags.Add(cur);
}
// Allow inline styles only
_self.AllowedAttributes.Add("style");
string[] safeCssProperties = [
"align-content", "align-items", "align-self", "all",
"animation", "animation-delay", "animation-direction", "animation-duration",
"animation-fill-mode", "animation-iteration-count", "animation-name", "animation-play-state",
"animation-timing-function", "backface-visibility", "background-color", "background-clip",
"background-origin", "background-position", "background-repeat", "background-size",
"border", "border-bottom", "border-bottom-color", "border-bottom-left-radius",
"border-bottom-right-radius", "border-bottom-style", "border-bottom-width", "border-color",
"border-image-outset", "border-image-repeat", "border-image-slice", "border-image-source",
"border-image-width", "border-left", "border-left-color", "border-left-style",
"border-left-width", "border-radius", "border-right", "border-right-color",
"border-right-style", "border-right-width", "border-spacing", "border-style",
"border-top", "border-top-color", "border-top-left-radius", "border-top-right-radius",
"border-top-style", "border-top-width", "border-width", "bottom",
"box-decoration-break", "box-shadow", "box-sizing", "caption-side",
"clear", "color", "column-count", "column-fill",
"column-gap", "column-rule-color", "column-rule-style", "column-rule-width",
"column-span", "column-width", "columns", "counter-increment",
"counter-reset", "direction", "display", "empty-cells",
"flex", "flex-basis", "flex-direction", "flex-flow",
"flex-grow", "flex-shrink", "flex-wrap", "float",
"font-family", "font-feature-settings", "font-kerning", "font-language-override",
"font-size", "font-size-adjust", "font-stretch", "font-style",
"font-synthesis", "font-variant", "font-variant-alternates", "font-variant-caps",
"font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric", "font-variant-position",
"font-weight", "grid", "grid-area", "grid-auto-columns",
"grid-auto-flow", "grid-auto-rows", "grid-column", "grid-column-end",
"grid-column-gap", "grid-column-start", "grid-gap", "grid-row",
"grid-row-end", "grid-row-gap", "grid-row-start", "grid-template",
"grid-template-areas", "grid-template-columns", "grid-template-rows", "height",
"hyphens", "image-rendering", "isolation", "justify-content",
"left", "letter-spacing", "line-height", "list-style-position",
"list-style-type", "margin", "margin-bottom", "margin-left",
"margin-right", "margin-top", "max-height", "max-width",
"min-height", "min-width", "object-fit", "object-position",
"opacity", "order", "orphans", "outline-color",
"outline-offset", "outline-style", "outline-width", "overflow",
"overflow-wrap", "overflow-x", "overflow-y", "padding",
"padding-bottom", "padding-left", "padding-right", "padding-top",
"page-break-after", "page-break-before", "page-break-inside", "perspective",
"perspective-origin", "pointer-events", "position", "quotes",
"resize", "right", "scroll-behavior", "table-layout",
"tab-size", "text-align", "text-align-last", "text-combine-upright",
"text-indent", "text-justify", "text-orientation", "text-overflow",
"text-shadow", "text-transform", "text-underline-position", "top",
"transform", "transform-origin", "transform-style", "transition",
"transition-delay", "transition-duration", "transition-property", "transition-timing-function",
"unicode-bidi", "user-select", "vertical-align", "visibility",
"white-space", "widows", "width", "word-break",
"word-spacing", "word-wrap", "writing-mode", "z-index"
];
foreach (string cur in safeCssProperties) {
_self.AllowedCssProperties.Add(cur);
}
}
public static string RemoveJavascript(string InputHTML) {
return _self.Sanitize(InputHTML);
}
}
}