From 2c4292da07a66d06f23a0cba10784cdbd21bbe2b Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Fri, 1 Aug 2025 21:32:56 -0700 Subject: [PATCH 01/19] Add Job Listing Skills --- database/mistox.sql | 9 +++ src/Client/src/app/models/JobListing.ts | 8 +++ src/Server/Entities/JobListing.cs | 8 +++ .../Services/DatabaseService/JobListing.cs | 18 +++-- .../DatabaseService/JobListingSkill.cs | 68 +++++++++++++++++++ 5 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 src/Server/Services/DatabaseService/JobListingSkill.cs diff --git a/database/mistox.sql b/database/mistox.sql index da44b17..b7a19a1 100755 --- a/database/mistox.sql +++ b/database/mistox.sql @@ -166,4 +166,13 @@ CREATE TABLE IF NOT EXISTS `JobListing` ( `IsDeleted` boolean Default 0, PRIMARY KEY (`ID`), FOREIGN KEY (`CompanyID`) REFERENCES `Company`(`ID`) ON DELETE CASCADE +) AUTO_INCREMENT=1; + +CREATE TABLE IF NOT EXISTS `JobListingSkill` ( + `ID` int NOT NULL AUTO_INCREMENT, + `JobListingID` int NOT NULL, + `Name` varchar(150) NOT NULL, + `Description` text DEFAULT NULL, + PRIMARY KEY (`ID`), + FOREIGN KEY (`JobListingID`) REFERENCES `JobListing`(`ID`) ON DELETE CASCADE ) AUTO_INCREMENT=1; \ No newline at end of file diff --git a/src/Client/src/app/models/JobListing.ts b/src/Client/src/app/models/JobListing.ts index 52fb396..ffd34e3 100644 --- a/src/Client/src/app/models/JobListing.ts +++ b/src/Client/src/app/models/JobListing.ts @@ -11,7 +11,15 @@ export class JobListing { public jobType: string = ""; public remote: boolean = false; public description: string = ""; + public skills: JobListingSkills[] = []; public createdTime: Date = new Date(); public modifiedTime: Date = new Date(); public isDeleted: boolean = false; +} + +export class JobListingSkills { + public id: number | null = null; + public jobListingID: number = 0; + public name: string = ""; + public Description: string = ""; } \ No newline at end of file diff --git a/src/Server/Entities/JobListing.cs b/src/Server/Entities/JobListing.cs index f4fdc5f..92089e0 100644 --- a/src/Server/Entities/JobListing.cs +++ b/src/Server/Entities/JobListing.cs @@ -13,9 +13,17 @@ namespace BoredCareers.Entities { public string JobType { get; set; } = ""; public bool Remote { get; set; } = false; public string Description { get; set; } = ""; + public JobListingSkill[] Skills { get; set; } = []; public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } public bool IsDeleted { get; set; } = false; } + public class JobListingSkill { + public int? ID { get; set; } // PK + public int JobListingID { get; set; } // FK + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + } + } \ No newline at end of file diff --git a/src/Server/Services/DatabaseService/JobListing.cs b/src/Server/Services/DatabaseService/JobListing.cs index 434fde9..b654062 100644 --- a/src/Server/Services/DatabaseService/JobListing.cs +++ b/src/Server/Services/DatabaseService/JobListing.cs @@ -37,6 +37,7 @@ namespace BoredCareers.Services.DatabaseService { string _jobtype = reader.GetString("JobType"); bool _remote = reader.GetBoolean("Remote"); string _description = reader.GetString("Description"); + JobListingSkill[] _skills = await GetJobListingSkills(_id); DateTime _createtime = reader.GetDateTime("CreatedTime"); DateTime _modifiedtime = reader.GetDateTime("ModifiedTime"); bool _isdeleted = reader.GetBoolean("IsDeleted"); @@ -54,6 +55,7 @@ namespace BoredCareers.Services.DatabaseService { JobType = _jobtype, Remote = _remote, Description = _description, + Skills = _skills, CreatedTime = _createtime, ModifiedTime = _modifiedtime, IsDeleted = _isdeleted @@ -92,6 +94,7 @@ namespace BoredCareers.Services.DatabaseService { string _jobtype = reader.GetString("JobType"); bool _remote = reader.GetBoolean("Remote"); string _description = reader.GetString("Description"); + JobListingSkill[] _skills = await GetJobListingSkills(_id); DateTime _createtime = reader.GetDateTime("CreatedTime"); DateTime _modifiedtime = reader.GetDateTime("ModifiedTime"); bool _isdeleted = reader.GetBoolean("IsDeleted"); @@ -109,6 +112,7 @@ namespace BoredCareers.Services.DatabaseService { JobType = _jobtype, Remote = _remote, Description = _description, + Skills = _skills, CreatedTime = _createtime, ModifiedTime = _modifiedtime, IsDeleted = _isdeleted @@ -147,6 +151,7 @@ namespace BoredCareers.Services.DatabaseService { string _jobtype = reader.GetString("JobType"); bool _remote = reader.GetBoolean("Remote"); string _description = reader.GetString("Description"); + JobListingSkill[] _skills = await GetJobListingSkills(_id); DateTime _createtime = reader.GetDateTime("CreatedTime"); DateTime _modifiedtime = reader.GetDateTime("ModifiedTime"); bool _isdeleted = reader.GetBoolean("IsDeleted"); @@ -164,6 +169,7 @@ namespace BoredCareers.Services.DatabaseService { JobType = _jobtype, Remote = _remote, Description = _description, + Skills = _skills, CreatedTime = _createtime, ModifiedTime = _modifiedtime, IsDeleted = _isdeleted @@ -175,7 +181,7 @@ namespace BoredCareers.Services.DatabaseService { } public async Task SetJobListing( JobListing jobListing ) { - using( MySqlConnection connection = GetConnection() ) { + using (MySqlConnection connection = GetConnection()) { connection.Open(); string command = @" @@ -200,7 +206,7 @@ namespace BoredCareers.Services.DatabaseService { IsDeleted = @IsDeleted; "; - MySqlCommand cmd = new MySqlCommand( command , connection); + MySqlCommand cmd = new MySqlCommand(command, connection); cmd.Parameters.AddWithValue("@ID", jobListing.ID); cmd.Parameters.AddWithValue("@CompanyID", jobListing.CompanyID); cmd.Parameters.AddWithValue("@Title", jobListing.Title); @@ -218,11 +224,15 @@ namespace BoredCareers.Services.DatabaseService { cmd.Parameters.AddWithValue("@IsDeleted", jobListing.IsDeleted); await cmd.ExecuteNonQueryAsync(); + + foreach (JobListingSkill cur in jobListing.Skills) { + await SetJobListingSkills(cur); + } } } public async Task DeleteJobListing( int JobListingID ) { - using( MySqlConnection connection = GetConnection() ) { + using (MySqlConnection connection = GetConnection()) { MySqlCommand cmd; connection.Open(); @@ -231,7 +241,7 @@ namespace BoredCareers.Services.DatabaseService { SET IsDeleted = TRUE WHERE ID = @ID; "; - cmd = new MySqlCommand( command, connection ); + cmd = new MySqlCommand(command, connection); cmd.Parameters.AddWithValue("@ID", JobListingID); await cmd.ExecuteNonQueryAsync(); diff --git a/src/Server/Services/DatabaseService/JobListingSkill.cs b/src/Server/Services/DatabaseService/JobListingSkill.cs new file mode 100644 index 0000000..0d022c7 --- /dev/null +++ b/src/Server/Services/DatabaseService/JobListingSkill.cs @@ -0,0 +1,68 @@ +using BoredCareers.Entities; +using MySql.Data.MySqlClient; +using System.Data; +using System.Data.Common; + +namespace BoredCareers.Services.DatabaseService { + public partial class DatabaseService { + + public async Task GetJobListingSkills(int JobListingID) { + List joblistingskills = new List(); + using (MySqlConnection connection = GetConnection()) { + connection.Open(); + string command = @" + SELECT * + FROM JobListingSkill + WHERE JobListingID = @JobListingID; + "; + + MySqlCommand cmd = new MySqlCommand(command, connection); + cmd.Parameters.AddWithValue("@JobListingID", JobListingID); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { + while (await reader.ReadAsync()) { + if (reader == null) { break; } + int _id = reader.GetInt32("ID"); + int _joblistingid = reader.GetInt32("JobListingID"); + string _name = reader.GetString("Name"); + string _description = reader.GetString("Description"); + + joblistingskills.Add(new JobListingSkill() { + ID = _id, + JobListingID = _joblistingid, + Name = _name, + Description = _description + }); + } + } + } + return joblistingskills.ToArray(); + } + + public async Task SetJobListingSkills( JobListingSkill jobListingSkill ) { + using( MySqlConnection connection = GetConnection() ) { + connection.Open(); + + string command = @" + INSERT INTO JobListing + (ID,JobListingID,Name,Description) + VALUES + (@ID,@JobListingID,@Name,@Description) + ON DUPLICATE KEY UPDATE + JobListingID = @JobListingID, + Name = @Name, + Description = @Description; + "; + + MySqlCommand cmd = new MySqlCommand( command , connection); + cmd.Parameters.AddWithValue("@ID", jobListingSkill.ID); + cmd.Parameters.AddWithValue("@JobListingID", jobListingSkill.JobListingID); + cmd.Parameters.AddWithValue("@Name", jobListingSkill.Name); + cmd.Parameters.AddWithValue("@Description", jobListingSkill.Description); + + await cmd.ExecuteNonQueryAsync(); + } + } + + } +} From 304182772074e12bb83fa6b7aca918793ba79db5 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Fri, 1 Aug 2025 21:33:15 -0700 Subject: [PATCH 02/19] Start work on company page look --- .../app/pages/main/company/company.component.css | 11 +++++++++++ .../app/pages/main/company/company.component.html | 13 +++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Client/src/app/pages/main/company/company.component.css b/src/Client/src/app/pages/main/company/company.component.css index 4263503..290016c 100644 --- a/src/Client/src/app/pages/main/company/company.component.css +++ b/src/Client/src/app/pages/main/company/company.component.css @@ -30,4 +30,15 @@ button { margin: 10px; overflow: scroll; padding: 10px; + color: var(--Mistox-White); +} + +.center-item { + display: flex; + width: 100%; + justify-content: center; +} + +.center-item img { + width: 300px; } \ No newline at end of file diff --git a/src/Client/src/app/pages/main/company/company.component.html b/src/Client/src/app/pages/main/company/company.component.html index 3f3a63d..204c13b 100644 --- a/src/Client/src/app/pages/main/company/company.component.html +++ b/src/Client/src/app/pages/main/company/company.component.html @@ -4,11 +4,16 @@
-

{{ Comp.name }}

-

{{ Comp.email }}

+
+ +

{{ Comp.name }}

+ +
+
+ +

{{ Comp.emailVerified }}

-

{{ Comp.websiteURL }}

-

{{ Comp.logo }}

+

{{ Comp.phone }}

{{ Comp.postalCode }}

{{ Comp.country }}

From f7acfc577e62127fc18c2e1f63a8893ba127f726 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Fri, 1 Aug 2025 21:33:37 -0700 Subject: [PATCH 03/19] make resume items public --- src/Client/src/app/models/Resume.ts | 100 ++++++++++++++-------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/Client/src/app/models/Resume.ts b/src/Client/src/app/models/Resume.ts index 74e8ed9..c917a5a 100644 --- a/src/Client/src/app/models/Resume.ts +++ b/src/Client/src/app/models/Resume.ts @@ -21,86 +21,86 @@ export class Resume { export class ResumeExperience { public id: number | null = null; - resumeID: number = 0; - jobTitle: string = ""; - company: string = ""; - postalCode: string = ""; - country: string = ""; - stateOrRegion: string = ""; - city: string = ""; - dateStarted: Date = new Date(); - stillEmployed: boolean = false; - dateEnded: Date = new Date(); - experienceBullets: ResumeExperienceBullet[] = []; + public resumeID: number = 0; + public jobTitle: string = ""; + public company: string = ""; + public postalCode: string = ""; + public country: string = ""; + public stateOrRegion: string = ""; + public city: string = ""; + public dateStarted: Date = new Date(); + public stillEmployed: boolean = false; + public dateEnded: Date = new Date(); + public experienceBullets: ResumeExperienceBullet[] = []; } export class ResumeExperienceBullet { public id: number | null = null; - resumeID: number = 0; - resumeExperienceID: number = 0; - jobFunction: string = ""; + public resumeID: number = 0; + public resumeExperienceID: number = 0; + public jobFunction: string = ""; } export class ResumeMilitary { public id: number | null = null; - resumeID: number = 0; - country: string = ""; - rank: string = ""; - dateStarted: Date = new Date(); - stillServing: boolean = false; - dateEnded: Date = new Date(); - millitaryBullets: ResumeMilitaryBullet[] = []; + public resumeID: number = 0; + public country: string = ""; + public rank: string = ""; + public dateStarted: Date = new Date(); + public stillServing: boolean = false; + public dateEnded: Date = new Date(); + public millitaryBullets: ResumeMilitaryBullet[] = []; } export class ResumeMilitaryBullet { public id: number | null = null; - resumeID: number = 0; - resumeMilitaryID: number = 0; - achievement: string = ""; - description: string = ""; + public resumeID: number = 0; + public resumeMilitaryID: number = 0; + public achievement: string = ""; + public description: string = ""; } export class ResumeEducation { public id: number | null = null; - resumeID: number = 0; - degreeType: string = ""; - degreeField: string = ""; - school: string = ""; - postalCode: string = ""; - country: string = ""; - stateOrRegion: string = ""; - city: string = ""; - dateStarted: Date = new Date(); - stillStudying: boolean = false; - dateEnded: Date = new Date(); + public resumeID: number = 0; + public degreeType: string = ""; + public degreeField: string = ""; + public school: string = ""; + public postalCode: string = ""; + public country: string = ""; + public stateOrRegion: string = ""; + public city: string = ""; + public dateStarted: Date = new Date(); + public stillStudying: boolean = false; + public dateEnded: Date = new Date(); } export class ResumeSkill { public id: number | null = null; - resumeID: number = 0; - name: string = ""; - description: string = ""; + public resumeID: number = 0; + public name: string = ""; + public description: string = ""; } export class ResumeLanguage { public id: number | null = null; - resumeID: number = 0; - language: string = ""; - proficiency: string = ""; + public resumeID: number = 0; + public language: string = ""; + public proficiency: string = ""; } export class ResumeCertification { public id: number | null = null; - resumeID: number = 0; - name: string = ""; - verificationURL: string = ""; - description: string = ""; + public resumeID: number = 0; + public name: string = ""; + public verificationURL: string = ""; + public description: string = ""; } export class ResumeProject { public id: number | null = null; - resumeID: number = 0; - name: string = ""; - url: string = ""; - description: string = ""; + public resumeID: number = 0; + public name: string = ""; + public url: string = ""; + public description: string = ""; } \ No newline at end of file From 14d7bf75876a1164c27d7c69ad3ed27e4acdbaf2 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Fri, 1 Aug 2025 21:33:56 -0700 Subject: [PATCH 04/19] harden the website --- src/Server/Program.cs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Server/Program.cs b/src/Server/Program.cs index 89b7ca9..62bb7ac 100755 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -12,7 +12,6 @@ using System.Security.Cryptography; var builder = WebApplication.CreateBuilder(args); // Disable null warnings becuse string.IsNullOrEmpty checks for NULL or Empty -#pragma warning disable CS8600 #pragma warning disable CS8604 //////////////////////////////// @@ -135,15 +134,28 @@ builder.Services.AddAuthentication(options => { }; }); -builder.Services.AddCors(o => o.AddDefaultPolicy(builder => { - builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); // No CORS -})); +//////////////////////////////// +/// Rate Limiting Service //// +//////////////////////////////// + +List allowedOrigins = new List{ "https://boredcareers.com", "https://www.boredcareers.com" }; +if (builder.Environment.IsDevelopment()) { + allowedOrigins.Add("http://localhost:5000"); +} + +builder.Services.AddCors(options => { + options.AddDefaultPolicy(policy => { + policy.WithOrigins(allowedOrigins.ToArray()) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); +}); builder.Services.AddRateLimiter(options => { options.AddPolicy("PerUserPolicy", httpContext => { var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value - ?? httpContext.User.Identity?.Name - ?? httpContext.Connection.RemoteIpAddress?.ToString(); + ?? $"ip:{httpContext.Connection.RemoteIpAddress}"; return RateLimitPartition.GetTokenBucketLimiter(userId, key => new TokenBucketRateLimiterOptions { TokenLimit = 10, // max 10 requests @@ -156,9 +168,7 @@ builder.Services.AddRateLimiter(options => { }); }); -// Pages Service builder.Services.AddControllers(); -builder.Services.AddRazorPages(); var app = builder.Build(); @@ -170,12 +180,14 @@ if( !app.Environment.IsDevelopment() ) { app.UseDefaultFiles(); app.UseStaticFiles(); +app.UseRateLimiter(); + app.UseCors(); app.UseRouting(); app.UseAuthentication(); -app.MapControllers().RequireRateLimiting("perUserPolicy"); +app.MapControllers(); app.MapFallbackToFile("index.html"); From 23e3028d62ee76fb2a0982377cb1f30ffb6bdca7 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Fri, 1 Aug 2025 21:34:00 -0700 Subject: [PATCH 05/19] Update TODO --- ToDo.yaml | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/ToDo.yaml b/ToDo.yaml index 8b9c668..8ebdd81 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -5,23 +5,17 @@ Server: When a company is created: Send email -> verify ownership of the email - Need to timeout email reset tokens: - Client: jobs/new: - Want to add Required skills to help with filtering - 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 + Job Listing Skills exists but isn't implimented in the UI + Tab doesnt do anything Want to add completed job listing preview at end of carosel database: Add Applied Jobs Table - - Task: - Block API Access as much as possible [ Rate limit | Auth Req | CORS | Disallow AI keyword filters ] + Block API Access as much as possible [ 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 @@ -31,16 +25,10 @@ Task: 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 Finish Auth setup - Make sure autorenew works + Make sure autorenew works Jobs/editor w/ Querystring JobID=# is not implimented yet - Company -> Edit employees not implimented yet - - Resume fields in angular models need to be public \ No newline at end of file + Company -> Edit employees not implimented yet \ No newline at end of file From bdc5b346a8b7f91fd09da7a9e402cf62a4dbff61 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Fri, 1 Aug 2025 21:39:08 -0700 Subject: [PATCH 06/19] Cleanup todo --- ToDo.yaml | 51 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/ToDo.yaml b/ToDo.yaml index 8ebdd81..3e63620 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -5,30 +5,41 @@ Server: When a company is created: Send email -> verify ownership of the email + Resume: + Block API Access as much as possible [ Disallow AI keyword filters ] + + Jobs: + Auto unlist jobs after a month of no activity [ Multiple offenders marked ] + + Auth: + Make sure autorenew works + Client: jobs/new: Job Listing Skills exists but isn't implimented in the UI Tab doesnt do anything Want to add completed job listing preview at end of carosel + Edit employees not implimented yet + + Jobs/editor: + Jobs/editor w/ Querystring JobID=# is not implimented yet + Edit employees not implimented yet + + Resume: + Resume builder minimal user input [ Dont allow AI input ] + Allow company to look up users if their resume is public [ Maybe auto with notify ] + Allow multiple resume's for job specific work + Create advanced filtering tools for company lookup and resume lookup + + Job Board: + Allow users to look up jobs and apply [ Boost visibility | Completely manual ] + Mark ghost listings to allow users to be informed and put companies on blast + + CompanyConnect: + need to lookup company before making a new one + + Employees: + database: - Add Applied Jobs Table - -Task: - Block API Access as much as possible [ 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 - - CompanyConnect | need to lookup company before making a new one - - Finish Auth setup - Make sure autorenew works - - Jobs/editor w/ Querystring JobID=# is not implimented yet - Company -> Edit employees not implimented yet \ No newline at end of file + Add Applied Jobs Table \ No newline at end of file From 109306dda287f588cf80d6268ef6123d6c8acee8 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 16:27:25 -0700 Subject: [PATCH 07/19] Start Job Cleanup Service --- src/Server/Program.cs | 12 ++++++-- .../TimerServcie/JobCleanupService.cs | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/Server/Services/TimerServcie/JobCleanupService.cs diff --git a/src/Server/Program.cs b/src/Server/Program.cs index 62bb7ac..c6d917f 100755 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; +using BoredCareers.Services.TimerService; var builder = WebApplication.CreateBuilder(args); @@ -35,8 +36,15 @@ string? _dbpass = Environment.GetEnvironmentVariable("MySQLPass"); string dbPass = !string.IsNullOrEmpty(_dbpass) ? _dbpass : "oasv34$8gpv023dd"; // Create the database serivice -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.AddSingleton(sp => + new DatabaseService("server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;") +); + +//////////////////////////////// +/////// Timer Service /////// +//////////////////////////////// + +builder.Services.AddHostedService(); //////////////////////////////// ///////// Email Service //////// diff --git a/src/Server/Services/TimerServcie/JobCleanupService.cs b/src/Server/Services/TimerServcie/JobCleanupService.cs new file mode 100644 index 0000000..5d492b6 --- /dev/null +++ b/src/Server/Services/TimerServcie/JobCleanupService.cs @@ -0,0 +1,29 @@ +using BoredCareers.Entities; + +namespace BoredCareers.Services.TimerService { + public class JobCleanupService : BackgroundService { + + private readonly DatabaseService.DatabaseService _db; + + public JobCleanupService(DatabaseService.DatabaseService db) { + _db = db; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + while (!stoppingToken.IsCancellationRequested) { + try { + JobListing[] deletedJobListings = await _db.GetJobListingsPastExipre(); + await _db.DeleteJobListingsPastExipre(); + foreach (JobListing listing in deletedJobListings) { + // Get applicants for each closed listing + // Notify applicants of the listing closing + // Mark company for auto-closed job -> ghost listing + } + } catch (Exception e) { + Console.WriteLine($"Error: {e.Message}"); + } + await Task.Delay(TimeSpan.FromHours(24), stoppingToken); + } + } + } +} From 35018d921bb9d6ed799c5127d88eae24bc1b90e1 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 16:27:41 -0700 Subject: [PATCH 08/19] Thread on the server better --- .../Services/DatabaseService/Company.cs | 8 +- .../Services/DatabaseService/Employee.cs | 13 ++- .../Services/DatabaseService/JobListing.cs | 84 +++++++++++++++++-- .../DatabaseService/JobListingSkill.cs | 5 +- 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/Server/Services/DatabaseService/Company.cs b/src/Server/Services/DatabaseService/Company.cs index 504e481..f8efbda 100644 --- a/src/Server/Services/DatabaseService/Company.cs +++ b/src/Server/Services/DatabaseService/Company.cs @@ -10,7 +10,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task GetCompany( int CompanyID ) { Company? company = null; using( MySqlConnection connection = GetConnection() ) { - connection.Open(); + await connection.OpenAsync(); string command = @" SELECT * FROM Company @@ -22,7 +22,6 @@ namespace BoredCareers.Services.DatabaseService { using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) { while( await reader.ReadAsync() ) { - if( reader == null ) { break; } int _id = reader.GetInt32("ID"); string _name = reader.GetString("Name"); string _email = reader.GetString("Email"); @@ -58,8 +57,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task SetCompany( Company company ) { using (MySqlConnection connection = GetConnection()) { - connection.Open(); - + await connection.OpenAsync(); string command = @" INSERT INTO Company (ID,Name,Email,EmailVerified,WebsiteURL,Logo,Phone,PostalCode,Country,StateOrRegion,City,Description) @@ -104,7 +102,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task DeleteCompany( int CompanyID ) { using( MySqlConnection connection = GetConnection() ) { MySqlCommand cmd; - connection.Open(); + await connection.OpenAsync(); string command = @" DELETE FROM Company WHERE ID = @ID; diff --git a/src/Server/Services/DatabaseService/Employee.cs b/src/Server/Services/DatabaseService/Employee.cs index 143a5b0..b3e926b 100644 --- a/src/Server/Services/DatabaseService/Employee.cs +++ b/src/Server/Services/DatabaseService/Employee.cs @@ -10,7 +10,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task GetEmployee( int EmployeeID ) { Employee? employee = null; using( MySqlConnection connection = GetConnection() ) { - connection.Open(); + await connection.OpenAsync(); string command = @" SELECT * FROM Employee @@ -23,7 +23,6 @@ namespace BoredCareers.Services.DatabaseService { using( DbDataReader reader = await cmd.ExecuteReaderAsync() ) { while( await reader.ReadAsync() ) { - if( reader == null ) { break; } int _id = reader.GetInt32("ID"); int _accountid = reader.GetInt32("AccountID"); int _companyid = reader.GetInt32("CompanyID"); @@ -66,7 +65,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task GetEmployeesFromCompany(int CompanyID) { List employees = new List(); using (MySqlConnection connection = GetConnection()) { - connection.Open(); + await connection.OpenAsync(); string command = @" SELECT * FROM Employee @@ -79,7 +78,6 @@ namespace BoredCareers.Services.DatabaseService { using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { - if (reader == null) { break; } int _id = reader.GetInt32("ID"); int _accountid = reader.GetInt32("AccountID"); int _companyid = reader.GetInt32("CompanyID"); @@ -122,7 +120,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task GetEmployeesFromAccount(int AccountID) { List employees = new List(); using (MySqlConnection connection = GetConnection()) { - connection.Open(); + await connection.OpenAsync(); string command = @" SELECT * FROM Employee @@ -135,7 +133,6 @@ namespace BoredCareers.Services.DatabaseService { using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { - if (reader == null) { break; } int _id = reader.GetInt32("ID"); int _accountid = reader.GetInt32("AccountID"); int _companyid = reader.GetInt32("CompanyID"); @@ -177,7 +174,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task SetEmployee(Employee employee) { using (MySqlConnection connection = GetConnection()) { - connection.Open(); + await connection.OpenAsync(); string command = @" INSERT INTO Employee @@ -201,7 +198,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task DeleteEmployee( int EmployeeID ) { using( MySqlConnection connection = GetConnection() ) { MySqlCommand cmd; - connection.Open(); + await connection.OpenAsync(); string command = @" DELETE FROM Employee WHERE ID = @ID; diff --git a/src/Server/Services/DatabaseService/JobListing.cs b/src/Server/Services/DatabaseService/JobListing.cs index b654062..065088c 100644 --- a/src/Server/Services/DatabaseService/JobListing.cs +++ b/src/Server/Services/DatabaseService/JobListing.cs @@ -9,7 +9,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task GetJobListingPage(int PageNumber, int CountPerPage) { List joblistings = new List(); using (MySqlConnection connection = GetConnection()) { - connection.Open(); + await connection.OpenAsync(); string command = @" SELECT * FROM JobListing @@ -24,7 +24,6 @@ namespace BoredCareers.Services.DatabaseService { using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { - if (reader == null) { break; } int _id = reader.GetInt32("ID"); int _companyid = reader.GetInt32("CompanyID"); string _title = reader.GetString("Title"); @@ -69,7 +68,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task GetJobListingFromCompany(int CompanyID) { List joblistings = new List(); ; using (MySqlConnection connection = GetConnection()) { - connection.Open(); + await connection.OpenAsync(); string command = @" SELECT * FROM JobListing @@ -81,7 +80,6 @@ namespace BoredCareers.Services.DatabaseService { using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { - if (reader == null) { break; } int _id = reader.GetInt32("ID"); int _companyid = reader.GetInt32("CompanyID"); string _title = reader.GetString("Title"); @@ -126,7 +124,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task GetJobListing(int JobListingID) { JobListing? joblisting = null; using (MySqlConnection connection = GetConnection()) { - connection.Open(); + await connection.OpenAsync(); string command = @" SELECT * FROM JobListing @@ -138,7 +136,6 @@ namespace BoredCareers.Services.DatabaseService { using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { - if (reader == null) { break; } int _id = reader.GetInt32("ID"); int _companyid = reader.GetInt32("CompanyID"); string _title = reader.GetString("Title"); @@ -180,9 +177,78 @@ namespace BoredCareers.Services.DatabaseService { return joblisting; } - public async Task SetJobListing( JobListing jobListing ) { + public async Task GetJobListingsPastExipre() { + List joblistings = new List(); using (MySqlConnection connection = GetConnection()) { - connection.Open(); + await connection.OpenAsync(); + string command = @" + SELECT * + FROM JobListing + WHERE IsDeleted = FALSE + AND CreatedTime < NOW() - INTERVAL 1 MONTH; + "; + MySqlCommand cmd = new MySqlCommand(command, connection); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { + while (await reader.ReadAsync()) { + int _id = reader.GetInt32("ID"); + int _companyid = reader.GetInt32("CompanyID"); + string _title = reader.GetString("Title"); + string _postalcode = reader.GetString("PostalCode"); + string _country = reader.GetString("Country"); + string _state = reader.GetString("StateOrRegion"); + string _city = reader.GetString("City"); + int _salarymin = reader.GetInt32("SalaryMin"); + int _salarymax = reader.GetInt32("SalaryMax"); + string _jobtype = reader.GetString("JobType"); + bool _remote = reader.GetBoolean("Remote"); + string _description = reader.GetString("Description"); + JobListingSkill[] _skills = await GetJobListingSkills(_id); + DateTime _createtime = reader.GetDateTime("CreatedTime"); + DateTime _modifiedtime = reader.GetDateTime("ModifiedTime"); + bool _isdeleted = reader.GetBoolean("IsDeleted"); + + joblistings.Add(new JobListing() { + ID = _id, + CompanyID = _companyid, + Title = _title, + PostalCode = _postalcode, + Country = _country, + StateOrRegion = _state, + City = _city, + SalaryMin = _salarymin, + SalaryMax = _salarymax, + JobType = _jobtype, + Remote = _remote, + Description = _description, + Skills = _skills, + CreatedTime = _createtime, + ModifiedTime = _modifiedtime, + IsDeleted = _isdeleted + }); + } + } + } + return joblistings.ToArray(); + } + + public async Task DeleteJobListingsPastExipre() { + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + UPDATE JobListing + SET IsDeleted = TRUE + WHERE IsDeleted = FALSE + AND CreatedTime < NOW() - INTERVAL 1 MONTH; + "; + MySqlCommand cmd = new MySqlCommand(command, connection); + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task SetJobListing(JobListing jobListing) { + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); string command = @" INSERT INTO JobListing @@ -234,7 +300,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task DeleteJobListing( int JobListingID ) { using (MySqlConnection connection = GetConnection()) { MySqlCommand cmd; - connection.Open(); + await connection.OpenAsync(); string command = @" UPDATE JobListing diff --git a/src/Server/Services/DatabaseService/JobListingSkill.cs b/src/Server/Services/DatabaseService/JobListingSkill.cs index 0d022c7..953bc0b 100644 --- a/src/Server/Services/DatabaseService/JobListingSkill.cs +++ b/src/Server/Services/DatabaseService/JobListingSkill.cs @@ -9,7 +9,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task GetJobListingSkills(int JobListingID) { List joblistingskills = new List(); using (MySqlConnection connection = GetConnection()) { - connection.Open(); + await connection.OpenAsync(); string command = @" SELECT * FROM JobListingSkill @@ -41,8 +41,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task SetJobListingSkills( JobListingSkill jobListingSkill ) { using( MySqlConnection connection = GetConnection() ) { - connection.Open(); - + await connection.OpenAsync(); string command = @" INSERT INTO JobListing (ID,JobListingID,Name,Description) From 32c7d2647d1d4fb50444759e4de4b61b3efdf2bb Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 16:27:48 -0700 Subject: [PATCH 09/19] Update ToDo --- ToDo.yaml | 12 +++++++----- src/Server/Services/DatabaseService/Resume.cs | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ToDo.yaml b/ToDo.yaml index 3e63620..254c716 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -8,12 +8,16 @@ Server: Resume: Block API Access as much as possible [ Disallow AI keyword filters ] - Jobs: - Auto unlist jobs after a month of no activity [ Multiple offenders marked ] - Auth: Make sure autorenew works + Services/JobCleanupService: + need to make the applied table in sql before i can finish the background-service + Need to make a ratio on the database + closed successful jobs -> number + not closed successful jobs -> number + For company rating for ghost listings + Client: jobs/new: Job Listing Skills exists but isn't implimented in the UI @@ -37,8 +41,6 @@ Client: CompanyConnect: need to lookup company before making a new one - - Employees: database: diff --git a/src/Server/Services/DatabaseService/Resume.cs b/src/Server/Services/DatabaseService/Resume.cs index 88ff4d2..86eda81 100644 --- a/src/Server/Services/DatabaseService/Resume.cs +++ b/src/Server/Services/DatabaseService/Resume.cs @@ -9,7 +9,7 @@ namespace BoredCareers.Services.DatabaseService { public async Task GetResumes(int AccountID) { List resumes = new List(); using (MySqlConnection connection = GetConnection()) { - connection.Open(); + await connection.OpenAsync(); string command = @" SELECT * FROM Resume From d167405ef1070477970d2f58b8e6db70884a03f1 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 16:44:24 -0700 Subject: [PATCH 10/19] Start work for company rating --- ToDo.yaml | 3 +++ database/mistox.sql | 2 ++ src/Client/src/app/models/Company.ts | 2 ++ src/Server/Entities/Company.cs | 2 ++ src/Server/Services/DatabaseService/Company.cs | 12 ++++++++++-- .../Services/TimerServcie/JobCleanupService.cs | 8 +++++++- 6 files changed, 26 insertions(+), 3 deletions(-) diff --git a/ToDo.yaml b/ToDo.yaml index 254c716..be7ce42 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -18,6 +18,9 @@ Server: not closed successful jobs -> number For company rating for ghost listings + When Job Posting Closes Successful: + Update the company rating + Client: jobs/new: Job Listing Skills exists but isn't implimented in the UI diff --git a/database/mistox.sql b/database/mistox.sql index b7a19a1..bd5dcae 100755 --- a/database/mistox.sql +++ b/database/mistox.sql @@ -131,6 +131,8 @@ CREATE TABLE IF NOT EXISTS `Company` ( `EmailVerified` boolean DEFAULT 0, `WebsiteURL` varchar(255) DEFAULT NULL, `Logo` mediumblob DEFAULT NULL, + `JobsClosedSuccessful` int DEFAULT 0, + `JobsAutoClosed` int DEFAULT 0, `Phone` varchar(20) DEFAULT NULL, `PostalCode` varchar(20) NOT NULL, `Country` char(2) NOT NULL, diff --git a/src/Client/src/app/models/Company.ts b/src/Client/src/app/models/Company.ts index 80a1e65..ae79789 100644 --- a/src/Client/src/app/models/Company.ts +++ b/src/Client/src/app/models/Company.ts @@ -5,6 +5,8 @@ export class Company { public emailVerified: boolean = false; public websiteURL: string = ""; public logo: string = ""; + public jobsClosedSuccessful: number = 0; + public jobsAutoClosed: number = 0; public phone: string = ""; public postalCode: string = ""; public country: string = ""; // 2 Letter Country Code diff --git a/src/Server/Entities/Company.cs b/src/Server/Entities/Company.cs index 1d78a4b..b9d6b06 100644 --- a/src/Server/Entities/Company.cs +++ b/src/Server/Entities/Company.cs @@ -7,6 +7,8 @@ namespace BoredCareers.Entities { public bool EmailVerified { get; set; } = false; public string WebsiteURL { get; set; } = ""; public string Logo { get; set; } = ""; + public int JobsClosedSuccessful { get; set; } = 0; + public int JobsAutoClosed { get; set; } = 0; public string Phone { get; set; } = ""; public string PostalCode { get; set; } = ""; public string Country { get; set; } = ""; // 2 Letter Country Code diff --git a/src/Server/Services/DatabaseService/Company.cs b/src/Server/Services/DatabaseService/Company.cs index f8efbda..5aaad81 100644 --- a/src/Server/Services/DatabaseService/Company.cs +++ b/src/Server/Services/DatabaseService/Company.cs @@ -28,6 +28,8 @@ namespace BoredCareers.Services.DatabaseService { bool _emailVerified = reader.GetBoolean("EmailVerified"); string _websiteurl = reader.GetString("WebsiteURL"); string _logo = Encoding.UTF8.GetString((byte[])reader["Logo"]); + int _jobsclosedsuccessful = reader.GetInt32("JobsClosedSuccessful"); + int _jobsautoclosed = reader.GetInt32("JobsAutoClosed"); string _phone = reader.GetString( "Phone" ); string _postalcode = reader.GetString( "PostalCode" ); string _country = reader.GetString( "Country" ); @@ -42,6 +44,8 @@ namespace BoredCareers.Services.DatabaseService { EmailVerified = _emailVerified, WebsiteURL = _websiteurl, Logo = _logo, + JobsAutoClosed = _jobsautoclosed, + JobsClosedSuccessful = _jobsclosedsuccessful, Phone = _phone, PostalCode = _postalcode, Country = _country, @@ -60,15 +64,17 @@ namespace BoredCareers.Services.DatabaseService { await connection.OpenAsync(); string command = @" INSERT INTO Company - (ID,Name,Email,EmailVerified,WebsiteURL,Logo,Phone,PostalCode,Country,StateOrRegion,City,Description) + (ID,Name,Email,EmailVerified,WebsiteURL,Logo,JobsClosedSuccessful,JobsAutoClosed,Phone,PostalCode,Country,StateOrRegion,City,Description) VALUES - (@ID,@Name,@Email,@EmailVerified,@WebsiteURL,@Logo,@Phone,@PostalCode,@Country,@StateOrRegion,@City,@Description) + (@ID,@Name,@Email,@EmailVerified,@WebsiteURL,@Logo,@JobsClosedSuccessful,@JobsAutoClosed,@Phone,@PostalCode,@Country,@StateOrRegion,@City,@Description) ON DUPLICATE KEY UPDATE Name = @Name, Email = @Email, EmailVerified = @EmailVerified, WebsiteURL = @WebsiteURL, Logo = @Logo, + JobsClosedSuccessful = @JobsClosedSuccessful, + JobsAutoClosed = @JobsAutoClosed, Phone = @Phone, PostalCode = @PostalCode, Country = @Country, @@ -86,6 +92,8 @@ namespace BoredCareers.Services.DatabaseService { cmd.Parameters.AddWithValue("@EmailVerified", company.EmailVerified); cmd.Parameters.AddWithValue("@WebsiteURL", company.WebsiteURL); cmd.Parameters.AddWithValue("@Logo", Encoding.UTF8.GetBytes(company.Logo)); + cmd.Parameters.AddWithValue("@JobsClosedSuccessful", company.JobsClosedSuccessful); + cmd.Parameters.AddWithValue("@JobsAutoClosed", company.JobsAutoClosed); cmd.Parameters.AddWithValue("@Phone", company.Phone); cmd.Parameters.AddWithValue("@PostalCode", company.PostalCode); cmd.Parameters.AddWithValue("@Country", company.Country); diff --git a/src/Server/Services/TimerServcie/JobCleanupService.cs b/src/Server/Services/TimerServcie/JobCleanupService.cs index 5d492b6..01e75a1 100644 --- a/src/Server/Services/TimerServcie/JobCleanupService.cs +++ b/src/Server/Services/TimerServcie/JobCleanupService.cs @@ -17,7 +17,13 @@ namespace BoredCareers.Services.TimerService { foreach (JobListing listing in deletedJobListings) { // Get applicants for each closed listing // Notify applicants of the listing closing - // Mark company for auto-closed job -> ghost listing + + // Mark the company for the auto closed listing + Company? comp = await _db.GetCompany(listing.CompanyID); + if (comp != null) { + comp.JobsAutoClosed++; + await _db.SetCompany(comp); + } } } catch (Exception e) { Console.WriteLine($"Error: {e.Message}"); From 2d627733d7a0294bfda5ef6c60636c72f49f597d Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 16:49:37 -0700 Subject: [PATCH 11/19] Fix time for the job listing record --- .../Services/DatabaseService/JobListing.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Server/Services/DatabaseService/JobListing.cs b/src/Server/Services/DatabaseService/JobListing.cs index 065088c..8ceedb8 100644 --- a/src/Server/Services/DatabaseService/JobListing.cs +++ b/src/Server/Services/DatabaseService/JobListing.cs @@ -232,20 +232,6 @@ namespace BoredCareers.Services.DatabaseService { return joblistings.ToArray(); } - public async Task DeleteJobListingsPastExipre() { - using (MySqlConnection connection = GetConnection()) { - await connection.OpenAsync(); - string command = @" - UPDATE JobListing - SET IsDeleted = TRUE - WHERE IsDeleted = FALSE - AND CreatedTime < NOW() - INTERVAL 1 MONTH; - "; - MySqlCommand cmd = new MySqlCommand(command, connection); - await cmd.ExecuteNonQueryAsync(); - } - } - public async Task SetJobListing(JobListing jobListing) { using (MySqlConnection connection = GetConnection()) { await connection.OpenAsync(); @@ -267,7 +253,6 @@ namespace BoredCareers.Services.DatabaseService { JobType = @JobType, Remote = @Remote, Description = @Description, - CreatedTime = @CreatedTime, ModifiedTime = @ModifiedTime, IsDeleted = @IsDeleted; "; @@ -285,8 +270,8 @@ namespace BoredCareers.Services.DatabaseService { cmd.Parameters.AddWithValue("@JobType", jobListing.JobType); cmd.Parameters.AddWithValue("@Remote", jobListing.Remote); cmd.Parameters.AddWithValue("@Description", jobListing.Description); - cmd.Parameters.AddWithValue("@CreatedTime", jobListing.CreatedTime.ToUniversalTime()); - cmd.Parameters.AddWithValue("@ModifiedTime", jobListing.ModifiedTime.ToUniversalTime()); + cmd.Parameters.AddWithValue("@CreatedTime", DateTime.UtcNow); + cmd.Parameters.AddWithValue("@ModifiedTime", DateTime.UtcNow); cmd.Parameters.AddWithValue("@IsDeleted", jobListing.IsDeleted); await cmd.ExecuteNonQueryAsync(); @@ -296,8 +281,22 @@ namespace BoredCareers.Services.DatabaseService { } } } + + public async Task DeleteJobListingsPastExipre() { + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + UPDATE JobListing + SET IsDeleted = TRUE + WHERE IsDeleted = FALSE + AND CreatedTime < NOW() - INTERVAL 1 MONTH; + "; + MySqlCommand cmd = new MySqlCommand(command, connection); + await cmd.ExecuteNonQueryAsync(); + } + } - public async Task DeleteJobListing( int JobListingID ) { + public async Task DeleteJobListing(int JobListingID) { using (MySqlConnection connection = GetConnection()) { MySqlCommand cmd; await connection.OpenAsync(); From 0a71162162637a0b4100d9fe95ad1227be2b33b6 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 17:27:22 -0700 Subject: [PATCH 12/19] Impliment application --- database/mistox.sql | 21 +- src/Client/src/app/models/Application.ts | 11 ++ .../Controllers/ApplicationController.cs | 55 ++++++ src/Server/Entities/Application.cs | 13 ++ src/Server/Entities/Company.cs | 4 +- .../Services/DatabaseService/Application.cs | 185 ++++++++++++++++++ 6 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 src/Client/src/app/models/Application.ts create mode 100644 src/Server/Controllers/ApplicationController.cs create mode 100644 src/Server/Entities/Application.cs create mode 100644 src/Server/Services/DatabaseService/Application.cs diff --git a/database/mistox.sql b/database/mistox.sql index bd5dcae..e4a488b 100755 --- a/database/mistox.sql +++ b/database/mistox.sql @@ -163,9 +163,9 @@ CREATE TABLE IF NOT EXISTS `JobListing` ( `JobType` varchar(20) NOT NULL, `Remote` boolean DEFAULT 0, `Description` text NOT NULL, - `CreatedTime` datetime Default NULL, + `CreatedTime` datetime DEFAULT NULL, `ModifiedTime` datetime DEFAULT NULL, - `IsDeleted` boolean Default 0, + `IsDeleted` boolean DEFAULT 0, PRIMARY KEY (`ID`), FOREIGN KEY (`CompanyID`) REFERENCES `Company`(`ID`) ON DELETE CASCADE ) AUTO_INCREMENT=1; @@ -177,4 +177,21 @@ CREATE TABLE IF NOT EXISTS `JobListingSkill` ( `Description` text DEFAULT NULL, PRIMARY KEY (`ID`), FOREIGN KEY (`JobListingID`) REFERENCES `JobListing`(`ID`) ON DELETE CASCADE +) AUTO_INCREMENT=1; + +-- Application Section + +CREATE TABLE IF NOT EXISTS `JobApplication` ( + `ID` int NOT NULL AUTO_INCREMENT, + `AccountID` int NOT NULL, + `ResumeID` int NOT NULL, + `JobListingID` int NOT NULL, + `DateApplied` datetime DEFAULT NULL, + `ResponseStatus` varchar(50) NOT NULL DEFAULT 'Pending', + `HasBeenViewed` boolean DEFAULT 0, + `Rating` int DEFAULT NULL, + `Notes` text DEFAULT NULL, + PRIMARY KEY (`ID`), + FOREIGN KEY (`ResumeID`) REFERENCES `Resume`(`ID`) ON DELETE CASCADE, + FOREIGN KEY (`JobListingID`) REFERENCES `JobListing`(`ID`) ON DELETE CASCADE ) AUTO_INCREMENT=1; \ No newline at end of file diff --git a/src/Client/src/app/models/Application.ts b/src/Client/src/app/models/Application.ts new file mode 100644 index 0000000..fb3f1bf --- /dev/null +++ b/src/Client/src/app/models/Application.ts @@ -0,0 +1,11 @@ +export class Application { + public id: number | null = null; + public accountID: number = 0; + public resumeID: number = 0; + public jobListingID: number = 0; + public dateApplied: Date = new Date(); + public responseStatus: string = ""; + public hasBeenViewed: boolean = false; + public rating: number = 0; + public notes: string = ""; +} \ No newline at end of file diff --git a/src/Server/Controllers/ApplicationController.cs b/src/Server/Controllers/ApplicationController.cs new file mode 100644 index 0000000..0b9f1d6 --- /dev/null +++ b/src/Server/Controllers/ApplicationController.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Mvc; +using BoredCareers.Services.DatabaseService; +using BoredCareers.Entities; +using System.Web.Http; + +namespace BoredCareers.Controllers { + [ApiController] + [Route("api/application")] + public class ApplicationController : MistoxControllerBase { + + public ApplicationController(DatabaseService db) : base(db) {} + + [HttpGet] + public async Task GetApplication(int ApplicationID) { + if (isLoggedIn()) { + Application? application = await _databaseService.GetApplication(ApplicationID); + if (application != null) { + return Ok(application); + } + return NotFound("Application doesn't exist"); + } + return NotFound("Not logged in"); + } + + [HttpPost] + public async Task SetApplication([FromBody] Application application) { + if (isLoggedIn()) { + if (application.AccountID == getLoggedInUserID()) { + await _databaseService.SetApplication(application); + return Ok(); + } + return NotFound("Cannot apply for someone else"); + } + return NotFound("Not logged in"); + } + + [HttpDelete] + public async Task DeleteApplication(int ApplicationID) { + if (isLoggedIn()) { + Application? app = await _databaseService.GetApplication(ApplicationID); + if (app != null) { + if (app.AccountID == getLoggedInUserID()) { + await _databaseService.DeleteApplication(ApplicationID); + return Ok(); + } + return NotFound("You cannot delete an app that isnt yours"); + } + return NotFound("Application doesn't exist"); + } + return NotFound("Not logged in"); + } + + } + +} diff --git a/src/Server/Entities/Application.cs b/src/Server/Entities/Application.cs new file mode 100644 index 0000000..11b61d2 --- /dev/null +++ b/src/Server/Entities/Application.cs @@ -0,0 +1,13 @@ +namespace BoredCareers.Entities { + public class Application { + public int? ID { get; set; } // PK + public int AccountID { get; set; } // FK + public int ResumeID { get; set; } // FK + public int JobListingID { get; set; } // FK + public DateTime DateApplied { get; set; } + public string ResponseStatus { get; set; } = ""; + public bool HasBeenViewed { get; set; } = false; + public int Rating { get; set; } + public string Notes { get; set; } = ""; + } +} \ No newline at end of file diff --git a/src/Server/Entities/Company.cs b/src/Server/Entities/Company.cs index b9d6b06..c6fbcb8 100644 --- a/src/Server/Entities/Company.cs +++ b/src/Server/Entities/Company.cs @@ -7,8 +7,8 @@ namespace BoredCareers.Entities { public bool EmailVerified { get; set; } = false; public string WebsiteURL { get; set; } = ""; public string Logo { get; set; } = ""; - public int JobsClosedSuccessful { get; set; } = 0; - public int JobsAutoClosed { get; set; } = 0; + public int JobsClosedSuccessful { get; set; } + public int JobsAutoClosed { get; set; } public string Phone { get; set; } = ""; public string PostalCode { get; set; } = ""; public string Country { get; set; } = ""; // 2 Letter Country Code diff --git a/src/Server/Services/DatabaseService/Application.cs b/src/Server/Services/DatabaseService/Application.cs new file mode 100644 index 0000000..c412944 --- /dev/null +++ b/src/Server/Services/DatabaseService/Application.cs @@ -0,0 +1,185 @@ +using BoredCareers.Entities; +using MySql.Data.MySqlClient; +using System.Data; +using System.Data.Common; + +namespace BoredCareers.Services.DatabaseService { + public partial class DatabaseService { + + public async Task GetApplcationsFromAccount(int AccountID) { + List applications = new List(); + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + SELECT * + FROM JobApplication + WHERE AccountID = @AccountID + "; + + MySqlCommand cmd = new MySqlCommand(command, connection); + cmd.Parameters.AddWithValue("@AccountID", AccountID); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { + while (await reader.ReadAsync()) { + int _id = reader.GetInt32("ID"); + int _accountid = reader.GetInt32("AccountID"); + int _resumeid = reader.GetInt32("ResumeID"); + int _joblistingid = reader.GetInt32("JobListingID"); + DateTime _dateapplied = reader.GetDateTime("DateApplied"); + string _responsestatus = reader.GetString("ResponseStatus"); + bool _hasbeenviewed = reader.GetBoolean("HasBeenViewed"); + int _rating = reader.GetInt32("Rating"); + string _notes = reader.GetString("Notes"); + + applications.Add(new Application() { + ID = _id, + AccountID = _accountid, + ResumeID = _resumeid, + JobListingID = _joblistingid, + DateApplied = _dateapplied, + ResponseStatus = _responsestatus, + HasBeenViewed = _hasbeenviewed, + Rating = _rating, + Notes = _notes + }); + } + } + } + return applications.ToArray(); + } + + public async Task GetApplicationsFromJobListing(int JobListingID) { + List applications = new List(); + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + SELECT * + FROM JobApplication + WHERE JobListingID = @JobListingID + "; + + MySqlCommand cmd = new MySqlCommand(command, connection); + cmd.Parameters.AddWithValue("@JobListingID", JobListingID); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { + while (await reader.ReadAsync()) { + int _id = reader.GetInt32("ID"); + int _accountid = reader.GetInt32("AccountID"); + int _resumeid = reader.GetInt32("ResumeID"); + int _joblistingid = reader.GetInt32("JobListingID"); + DateTime _dateapplied = reader.GetDateTime("DateApplied"); + string _responsestatus = reader.GetString("ResponseStatus"); + bool _hasbeenviewed = reader.GetBoolean("HasBeenViewed"); + int _rating = reader.GetInt32("Rating"); + string _notes = reader.GetString("Notes"); + + applications.Add(new Application() { + ID = _id, + AccountID = _accountid, + ResumeID = _resumeid, + JobListingID = _joblistingid, + DateApplied = _dateapplied, + ResponseStatus = _responsestatus, + HasBeenViewed = _hasbeenviewed, + Rating = _rating, + Notes = _notes + }); + } + } + } + return applications.ToArray(); + } + + public async Task GetApplication(int ApplicationID) { + Application? application = null; + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + SELECT * + FROM JobApplication + WHERE ID = @ApplicationID + "; + + MySqlCommand cmd = new MySqlCommand(command, connection); + cmd.Parameters.AddWithValue("@ApplicationID", ApplicationID); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { + while (await reader.ReadAsync()) { + int _id = reader.GetInt32("ID"); + int _accountid = reader.GetInt32("AccountID"); + int _resumeid = reader.GetInt32("ResumeID"); + int _joblistingid = reader.GetInt32("JobListingID"); + DateTime _dateapplied = reader.GetDateTime("DateApplied"); + string _responsestatus = reader.GetString("ResponseStatus"); + bool _hasbeenviewed = reader.GetBoolean("HasBeenViewed"); + int _rating = reader.GetInt32("Rating"); + string _notes = reader.GetString("Notes"); + + application = new Application() { + ID = _id, + AccountID = _accountid, + ResumeID = _resumeid, + JobListingID = _joblistingid, + DateApplied = _dateapplied, + ResponseStatus = _responsestatus, + HasBeenViewed = _hasbeenviewed, + Rating = _rating, + Notes = _notes + }; + } + } + } + + + + return application; + } + + public async Task SetApplication(Application application) { + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + INSERT INTO JobApplication + (ID,AccountID,ResumeID,JobListingID,DateApplied,ResponseStatus,HasBeenViewed,Rating,Notes) + VALUES + (@ID,@AccountID,@ResumeID,@JobListingID,@DateApplied,@ResponseStatus,@HasBeenViewed,@Rating,@Notes) + ON DUPLICATE KEY UPDATE + AccountID = @AccountID, + ResumeID = @ResumeID, + JobListingID = @JobListingID, + ResponseStatus = @ResponseStatus, + HasBeenViewed = @HasBeenViewed, + Rating = @Rating, + Notes = @Notes; + "; + + MySqlCommand cmd = new MySqlCommand(command, connection); + cmd.Parameters.AddWithValue("@ID", application.ID); + cmd.Parameters.AddWithValue("@AccountID", application.AccountID); + cmd.Parameters.AddWithValue("@ResumeID", application.ResumeID); + cmd.Parameters.AddWithValue("@JobListingID", application.JobListingID); + cmd.Parameters.AddWithValue("@DateApplied", DateTime.UtcNow); + cmd.Parameters.AddWithValue("@ResponseStatus", application.ResponseStatus); + cmd.Parameters.AddWithValue("@HasBeenViewed", application.HasBeenViewed); + cmd.Parameters.AddWithValue("@Rating", application.Rating); + cmd.Parameters.AddWithValue("@Notes", application.Notes); + + await cmd.ExecuteNonQueryAsync(); + } + } + + public async Task DeleteApplication(int ApplicationID) { + using (MySqlConnection connection = GetConnection()) { + MySqlCommand cmd; + await connection.OpenAsync(); + string command = @" + DELETE FROM JobApplication WHERE ID = @ID; + "; + cmd = new MySqlCommand(command, connection); + cmd.Parameters.AddWithValue("@ID", ApplicationID); + await cmd.ExecuteNonQueryAsync(); + } + } + + } +} From ca21bd6c5bf089f111fd9d89e73cc81de57236bd Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 17:39:29 -0700 Subject: [PATCH 13/19] Add response email to application --- database/mistox.sql | 1 + src/Client/src/app/models/Application.ts | 1 + src/Server/Entities/Application.cs | 1 + src/Server/Services/DatabaseService/Application.cs | 12 ++++++++++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/database/mistox.sql b/database/mistox.sql index e4a488b..72c979b 100755 --- a/database/mistox.sql +++ b/database/mistox.sql @@ -186,6 +186,7 @@ CREATE TABLE IF NOT EXISTS `JobApplication` ( `AccountID` int NOT NULL, `ResumeID` int NOT NULL, `JobListingID` int NOT NULL, + `ResponseEmail` varchar(255) DEFAULT NULL, `DateApplied` datetime DEFAULT NULL, `ResponseStatus` varchar(50) NOT NULL DEFAULT 'Pending', `HasBeenViewed` boolean DEFAULT 0, diff --git a/src/Client/src/app/models/Application.ts b/src/Client/src/app/models/Application.ts index fb3f1bf..576fefd 100644 --- a/src/Client/src/app/models/Application.ts +++ b/src/Client/src/app/models/Application.ts @@ -3,6 +3,7 @@ export class Application { public accountID: number = 0; public resumeID: number = 0; public jobListingID: number = 0; + public responseEmail: string = ""; public dateApplied: Date = new Date(); public responseStatus: string = ""; public hasBeenViewed: boolean = false; diff --git a/src/Server/Entities/Application.cs b/src/Server/Entities/Application.cs index 11b61d2..0683cb7 100644 --- a/src/Server/Entities/Application.cs +++ b/src/Server/Entities/Application.cs @@ -4,6 +4,7 @@ namespace BoredCareers.Entities { public int AccountID { get; set; } // FK public int ResumeID { get; set; } // FK public int JobListingID { get; set; } // FK + public string ResponseEmail { get; set; } = ""; public DateTime DateApplied { get; set; } public string ResponseStatus { get; set; } = ""; public bool HasBeenViewed { get; set; } = false; diff --git a/src/Server/Services/DatabaseService/Application.cs b/src/Server/Services/DatabaseService/Application.cs index c412944..83c163f 100644 --- a/src/Server/Services/DatabaseService/Application.cs +++ b/src/Server/Services/DatabaseService/Application.cs @@ -25,6 +25,7 @@ namespace BoredCareers.Services.DatabaseService { int _accountid = reader.GetInt32("AccountID"); int _resumeid = reader.GetInt32("ResumeID"); int _joblistingid = reader.GetInt32("JobListingID"); + string _responseemail = reader.GetString("ResponseEmail"); DateTime _dateapplied = reader.GetDateTime("DateApplied"); string _responsestatus = reader.GetString("ResponseStatus"); bool _hasbeenviewed = reader.GetBoolean("HasBeenViewed"); @@ -36,6 +37,7 @@ namespace BoredCareers.Services.DatabaseService { AccountID = _accountid, ResumeID = _resumeid, JobListingID = _joblistingid, + ResponseEmail = _responseemail, DateApplied = _dateapplied, ResponseStatus = _responsestatus, HasBeenViewed = _hasbeenviewed, @@ -67,6 +69,7 @@ namespace BoredCareers.Services.DatabaseService { int _accountid = reader.GetInt32("AccountID"); int _resumeid = reader.GetInt32("ResumeID"); int _joblistingid = reader.GetInt32("JobListingID"); + string _responseemail = reader.GetString("ResponseEmail"); DateTime _dateapplied = reader.GetDateTime("DateApplied"); string _responsestatus = reader.GetString("ResponseStatus"); bool _hasbeenviewed = reader.GetBoolean("HasBeenViewed"); @@ -78,6 +81,7 @@ namespace BoredCareers.Services.DatabaseService { AccountID = _accountid, ResumeID = _resumeid, JobListingID = _joblistingid, + ResponseEmail = _responseemail, DateApplied = _dateapplied, ResponseStatus = _responsestatus, HasBeenViewed = _hasbeenviewed, @@ -109,6 +113,7 @@ namespace BoredCareers.Services.DatabaseService { int _accountid = reader.GetInt32("AccountID"); int _resumeid = reader.GetInt32("ResumeID"); int _joblistingid = reader.GetInt32("JobListingID"); + string _responseemail = reader.GetString("ResponseEmail"); DateTime _dateapplied = reader.GetDateTime("DateApplied"); string _responsestatus = reader.GetString("ResponseStatus"); bool _hasbeenviewed = reader.GetBoolean("HasBeenViewed"); @@ -120,6 +125,7 @@ namespace BoredCareers.Services.DatabaseService { AccountID = _accountid, ResumeID = _resumeid, JobListingID = _joblistingid, + ResponseEmail = _responseemail, DateApplied = _dateapplied, ResponseStatus = _responsestatus, HasBeenViewed = _hasbeenviewed, @@ -140,13 +146,14 @@ namespace BoredCareers.Services.DatabaseService { await connection.OpenAsync(); string command = @" INSERT INTO JobApplication - (ID,AccountID,ResumeID,JobListingID,DateApplied,ResponseStatus,HasBeenViewed,Rating,Notes) + (ID,AccountID,ResumeID,JobListingID,ResponseEmail,DateApplied,ResponseStatus,HasBeenViewed,Rating,Notes) VALUES - (@ID,@AccountID,@ResumeID,@JobListingID,@DateApplied,@ResponseStatus,@HasBeenViewed,@Rating,@Notes) + (@ID,@AccountID,@ResumeID,@JobListingID,@ResponseEmail,@DateApplied,@ResponseStatus,@HasBeenViewed,@Rating,@Notes) ON DUPLICATE KEY UPDATE AccountID = @AccountID, ResumeID = @ResumeID, JobListingID = @JobListingID, + ResponseEmail = @ResponseEmail, ResponseStatus = @ResponseStatus, HasBeenViewed = @HasBeenViewed, Rating = @Rating, @@ -158,6 +165,7 @@ namespace BoredCareers.Services.DatabaseService { cmd.Parameters.AddWithValue("@AccountID", application.AccountID); cmd.Parameters.AddWithValue("@ResumeID", application.ResumeID); cmd.Parameters.AddWithValue("@JobListingID", application.JobListingID); + cmd.Parameters.AddWithValue("@ResponseEmail", application.ResponseEmail); cmd.Parameters.AddWithValue("@DateApplied", DateTime.UtcNow); cmd.Parameters.AddWithValue("@ResponseStatus", application.ResponseStatus); cmd.Parameters.AddWithValue("@HasBeenViewed", application.HasBeenViewed); From 8148a245b9355b50d7fdd017411b760c811645a5 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 17:39:43 -0700 Subject: [PATCH 14/19] Complete job cleanup service --- .../EmailService/JobAutoCloseEmail.cs | 52 +++++++++++++++++++ .../TimerServcie/JobCleanupService.cs | 10 ++-- 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100755 src/Server/Services/EmailService/JobAutoCloseEmail.cs diff --git a/src/Server/Services/EmailService/JobAutoCloseEmail.cs b/src/Server/Services/EmailService/JobAutoCloseEmail.cs new file mode 100755 index 0000000..a1055f3 --- /dev/null +++ b/src/Server/Services/EmailService/JobAutoCloseEmail.cs @@ -0,0 +1,52 @@ +namespace BoredCareers.Services { + public partial class EmailService { + +// @UserName +// @VerifyPassword +// https://mistox.com/api/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword + + public static string JobAutoClosedSubject = "Verify Your Email Address"; + public static string JobAutoClosedEmail = @" + + + + + + Verify Your Email + + + + + + +
+ + + + + + + + + + +
+

Verify Email Request

+
+

Hi @UserName,

+

Thank you for making an account with us:

+

In order to start using your account we need to verify your email address by clicking the link below:

+

+ Verify Email +

+

If you didn't create an account please ignore this email.

+

Best regards

+
+

If you have any questions, feel free to contact support.

+
+
+ +"; + + } +} \ No newline at end of file diff --git a/src/Server/Services/TimerServcie/JobCleanupService.cs b/src/Server/Services/TimerServcie/JobCleanupService.cs index 01e75a1..2e69d75 100644 --- a/src/Server/Services/TimerServcie/JobCleanupService.cs +++ b/src/Server/Services/TimerServcie/JobCleanupService.cs @@ -4,9 +4,11 @@ namespace BoredCareers.Services.TimerService { public class JobCleanupService : BackgroundService { private readonly DatabaseService.DatabaseService _db; + private readonly EmailService _em; - public JobCleanupService(DatabaseService.DatabaseService db) { + public JobCleanupService(DatabaseService.DatabaseService db, EmailService em) { _db = db; + _em = em; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { @@ -15,8 +17,10 @@ namespace BoredCareers.Services.TimerService { JobListing[] deletedJobListings = await _db.GetJobListingsPastExipre(); await _db.DeleteJobListingsPastExipre(); foreach (JobListing listing in deletedJobListings) { - // Get applicants for each closed listing - // Notify applicants of the listing closing + Application[] apps = await _db.GetApplicationsFromJobListing(Convert.ToInt32(listing.ID)); + foreach (Application app in apps) { + _em.Send(app.ResponseEmail, EmailService.JobAutoClosedSubject, EmailService.JobAutoClosedEmail); + } // Mark the company for the auto closed listing Company? comp = await _db.GetCompany(listing.CompanyID); From 23830e1e07a5ee03f28c9f3897678aa595dcf794 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 17:40:13 -0700 Subject: [PATCH 15/19] Update Todo --- ToDo.yaml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ToDo.yaml b/ToDo.yaml index be7ce42..3f213f4 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -11,16 +11,12 @@ Server: Auth: Make sure autorenew works - Services/JobCleanupService: - need to make the applied table in sql before i can finish the background-service - Need to make a ratio on the database - closed successful jobs -> number - not closed successful jobs -> number - For company rating for ghost listings - When Job Posting Closes Successful: Update the company rating + JobCleanupService: + Need to update notification email + Client: jobs/new: Job Listing Skills exists but isn't implimented in the UI From f093440b00f308fa80f166a502f095ac05e0925a Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 20:00:52 -0700 Subject: [PATCH 16/19] optimize the background jobs --- .../Services/DatabaseService/Application.cs | 23 ++++++++++ .../Services/DatabaseService/JobListing.cs | 42 ++++--------------- .../TimerServcie/JobCleanupService.cs | 41 +++++++++++++----- 3 files changed, 60 insertions(+), 46 deletions(-) diff --git a/src/Server/Services/DatabaseService/Application.cs b/src/Server/Services/DatabaseService/Application.cs index 83c163f..22b4758 100644 --- a/src/Server/Services/DatabaseService/Application.cs +++ b/src/Server/Services/DatabaseService/Application.cs @@ -94,6 +94,29 @@ namespace BoredCareers.Services.DatabaseService { return applications.ToArray(); } + public async Task GetApplicationResponseEmailFromJobListing(int JobListingID) { + List emailadds = new List(); + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + SELECT ResponseEmail + FROM JobApplication + WHERE JobListingID = @JobListingID + "; + + MySqlCommand cmd = new MySqlCommand(command, connection); + cmd.Parameters.AddWithValue("@JobListingID", JobListingID); + + using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { + while (await reader.ReadAsync()) { + string _responseemail = reader.GetString("ResponseEmail"); + emailadds.Add(_responseemail); + } + } + } + return emailadds.ToArray(); + } + public async Task GetApplication(int ApplicationID) { Application? application = null; using (MySqlConnection connection = GetConnection()) { diff --git a/src/Server/Services/DatabaseService/JobListing.cs b/src/Server/Services/DatabaseService/JobListing.cs index 8ceedb8..18fa04a 100644 --- a/src/Server/Services/DatabaseService/JobListing.cs +++ b/src/Server/Services/DatabaseService/JobListing.cs @@ -1,4 +1,5 @@ using BoredCareers.Entities; +using BoredCareers.Services.TimerService; using MySql.Data.MySqlClient; using System.Data; using System.Data.Common; @@ -177,12 +178,12 @@ namespace BoredCareers.Services.DatabaseService { return joblisting; } - public async Task GetJobListingsPastExipre() { - List joblistings = new List(); + public async Task GetJobListingsPastExipre() { + List joblistings = new List(); using (MySqlConnection connection = GetConnection()) { await connection.OpenAsync(); string command = @" - SELECT * + SELECT ID, CompanyID FROM JobListing WHERE IsDeleted = FALSE AND CreatedTime < NOW() - INTERVAL 1 MONTH; @@ -193,38 +194,9 @@ namespace BoredCareers.Services.DatabaseService { while (await reader.ReadAsync()) { int _id = reader.GetInt32("ID"); int _companyid = reader.GetInt32("CompanyID"); - string _title = reader.GetString("Title"); - string _postalcode = reader.GetString("PostalCode"); - string _country = reader.GetString("Country"); - string _state = reader.GetString("StateOrRegion"); - string _city = reader.GetString("City"); - int _salarymin = reader.GetInt32("SalaryMin"); - int _salarymax = reader.GetInt32("SalaryMax"); - string _jobtype = reader.GetString("JobType"); - bool _remote = reader.GetBoolean("Remote"); - string _description = reader.GetString("Description"); - JobListingSkill[] _skills = await GetJobListingSkills(_id); - DateTime _createtime = reader.GetDateTime("CreatedTime"); - DateTime _modifiedtime = reader.GetDateTime("ModifiedTime"); - bool _isdeleted = reader.GetBoolean("IsDeleted"); - - joblistings.Add(new JobListing() { - ID = _id, - CompanyID = _companyid, - Title = _title, - PostalCode = _postalcode, - Country = _country, - StateOrRegion = _state, - City = _city, - SalaryMin = _salarymin, - SalaryMax = _salarymax, - JobType = _jobtype, - Remote = _remote, - Description = _description, - Skills = _skills, - CreatedTime = _createtime, - ModifiedTime = _modifiedtime, - IsDeleted = _isdeleted + joblistings.Add(new JobListingDTO() { + JobListingID = _id, + CompanyID = _companyid }); } } diff --git a/src/Server/Services/TimerServcie/JobCleanupService.cs b/src/Server/Services/TimerServcie/JobCleanupService.cs index 2e69d75..fbd30fb 100644 --- a/src/Server/Services/TimerServcie/JobCleanupService.cs +++ b/src/Server/Services/TimerServcie/JobCleanupService.cs @@ -14,26 +14,45 @@ namespace BoredCareers.Services.TimerService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { - JobListing[] deletedJobListings = await _db.GetJobListingsPastExipre(); - await _db.DeleteJobListingsPastExipre(); - foreach (JobListing listing in deletedJobListings) { - Application[] apps = await _db.GetApplicationsFromJobListing(Convert.ToInt32(listing.ID)); - foreach (Application app in apps) { - _em.Send(app.ResponseEmail, EmailService.JobAutoClosedSubject, EmailService.JobAutoClosedEmail); - } + // Get listing's past expire + JobListingDTO[] deletedJobListings = await _db.GetJobListingsPastExipre(); - // Mark the company for the auto closed listing - Company? comp = await _db.GetCompany(listing.CompanyID); + // Group them by CompanyID, ListingCount + Dictionary listingsByCompany = deletedJobListings + .GroupBy(l => l.CompanyID) + .ToDictionary(g => g.Key, g => g.Count()); + + // Update each company's rating + foreach (KeyValuePair kvp in listingsByCompany) { + Company? comp = await _db.GetCompany(kvp.Key); if (comp != null) { - comp.JobsAutoClosed++; + comp.JobsAutoClosed += kvp.Value; await _db.SetCompany(comp); } } + + // Get each listing + foreach (JobListingDTO listing in deletedJobListings) { + // Get each Person + string[] emails = await _db.GetApplicationResponseEmailFromJobListing(listing.JobListingID); + foreach (string email in emails) { + // Send Notify Email + _em.Send(email, EmailService.JobAutoClosedSubject, EmailService.JobAutoClosedEmail); + } + } + + // Delete Listing's past expire + await _db.DeleteJobListingsPastExipre(); } catch (Exception e) { Console.WriteLine($"Error: {e.Message}"); } - await Task.Delay(TimeSpan.FromHours(24), stoppingToken); + await Task.Delay(TimeSpan.FromHours(2), stoppingToken); } } } + + public class JobListingDTO { + public int JobListingID { get; set; } + public int CompanyID { get; set; } + } } From 2636360cf9ae74cef4413a548a1c028ac8470ed3 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 20:02:05 -0700 Subject: [PATCH 17/19] Cleanup --- .../EmailService/ResetPasswordEmail.cs | 51 ------------------ .../Services/EmailService/VerifyEmail.cs | 52 ------------------- 2 files changed, 103 deletions(-) delete mode 100755 src/Server/Services/EmailService/ResetPasswordEmail.cs delete mode 100755 src/Server/Services/EmailService/VerifyEmail.cs diff --git a/src/Server/Services/EmailService/ResetPasswordEmail.cs b/src/Server/Services/EmailService/ResetPasswordEmail.cs deleted file mode 100755 index 7eff79c..0000000 --- a/src/Server/Services/EmailService/ResetPasswordEmail.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace BoredCareers.Services { - public partial class EmailService { - -// @UserName -// @ResetPassWord -// https://mistox.com/account/resetpassword?UserName=@UserName&ResetPwd=@ResetPassWord - - public static string ResetPasswordSubject = "Password Reset Request"; - public static string ResetPasswordEmail = @" - - - - - - Password Reset - - - - - - -
- - - - - - - - - - -
-

Password Reset Request

-
-

Hi @UserName,

-

We received a request to reset your password. You can reset your password by clicking the button below:

-

- Reset Password -

-

If you didn't request a password reset, you can safely ignore this email.

-

Best regards

-
-

If you have any questions, feel free to contact support.

-
-
- -"; - - } -} \ No newline at end of file diff --git a/src/Server/Services/EmailService/VerifyEmail.cs b/src/Server/Services/EmailService/VerifyEmail.cs deleted file mode 100755 index 9207038..0000000 --- a/src/Server/Services/EmailService/VerifyEmail.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace BoredCareers.Services { - public partial class EmailService { - -// @UserName -// @VerifyPassword -// https://mistox.com/api/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword - - public static string VerifyEmailSubject = "Verify Your Email Address"; - public static string VerifyEmailEmail = @" - - - - - - Verify Your Email - - - - - - -
- - - - - - - - - - -
-

Verify Email Request

-
-

Hi @UserName,

-

Thank you for making an account with us:

-

In order to start using your account we need to verify your email address by clicking the link below:

-

- Verify Email -

-

If you didn't create an account please ignore this email.

-

Best regards

-
-

If you have any questions, feel free to contact support.

-
-
- -"; - - } -} \ No newline at end of file From caeb30189d854a596188d417ada493ebeac848cc Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 20:05:58 -0700 Subject: [PATCH 18/19] Fix ordering --- src/Server/Program.cs | 16 ++++++++++------ .../JobCleanupService.cs | 0 2 files changed, 10 insertions(+), 6 deletions(-) rename src/Server/Services/{TimerServcie => BackgroundServices}/JobCleanupService.cs (100%) diff --git a/src/Server/Program.cs b/src/Server/Program.cs index c6d917f..c6dd26f 100755 --- a/src/Server/Program.cs +++ b/src/Server/Program.cs @@ -40,12 +40,6 @@ builder.Services.AddSingleton(sp => new DatabaseService("server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;") ); -//////////////////////////////// -/////// Timer Service /////// -//////////////////////////////// - -builder.Services.AddHostedService(); - //////////////////////////////// ///////// Email Service //////// //////////////////////////////// @@ -176,6 +170,16 @@ builder.Services.AddRateLimiter(options => { }); }); +//////////////////////////////// +///// Background Services ///// +//////////////////////////////// + +builder.Services.AddHostedService(); + +//////////////////////////////// +///// ASPNET Core Function ///// +//////////////////////////////// + builder.Services.AddControllers(); var app = builder.Build(); diff --git a/src/Server/Services/TimerServcie/JobCleanupService.cs b/src/Server/Services/BackgroundServices/JobCleanupService.cs similarity index 100% rename from src/Server/Services/TimerServcie/JobCleanupService.cs rename to src/Server/Services/BackgroundServices/JobCleanupService.cs From 641b8ad5252d3c99ced13ad7950f1ea10d8a0f30 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 4 Aug 2025 21:15:28 -0700 Subject: [PATCH 19/19] Work on the Homepage --- .../app/pages/main/home/home.component.css | 21 +++++++++++-- .../app/pages/main/home/home.component.html | 31 +++++++++++++------ src/Client/src/styles.css | 2 +- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/Client/src/app/pages/main/home/home.component.css b/src/Client/src/app/pages/main/home/home.component.css index 1b670c7..013fb16 100644 --- a/src/Client/src/app/pages/main/home/home.component.css +++ b/src/Client/src/app/pages/main/home/home.component.css @@ -3,6 +3,19 @@ width: calc(100% - 400px); } +.center-text { + text-align: center; +} + +.center-text h1 { + font-size: 100px; + margin: 20px 0; +} + +.center-text h2 { + margin-bottom: 200px; +} + .content-frame { display: flex; flex-direction: row; @@ -16,6 +29,7 @@ .floating-frame { width: 450px; + height: 200px; background-color: var(--Mistox-Frame); padding: 20px; margin: 20px 0; @@ -41,8 +55,11 @@ hr { } .solution-frame { - height: 200px; - background-color: blueviolet; width: calc(100% - 515px); margin: 20px 0; + text-align: center; +} + +.border { + border-bottom: solid 1px #000; } \ No newline at end of file diff --git a/src/Client/src/app/pages/main/home/home.component.html b/src/Client/src/app/pages/main/home/home.component.html index d36ac97..fa7c930 100644 --- a/src/Client/src/app/pages/main/home/home.component.html +++ b/src/Client/src/app/pages/main/home/home.component.html @@ -1,5 +1,11 @@
-
+ +
+

Bored Careers

+

The Anti-AI Job Board

+
+ +

death by a thousand applicants

@@ -11,11 +17,14 @@
- +
+

Rate Limiting - We use strong rate limiting to prevent bot's from flooding applications

+

Strong Authentication - All API's require authentication and will ban on suspision of bots

+
-
+

keyword frenzie

@@ -27,11 +36,12 @@
- +

No External Access - All companies and clients are required to interface with the application. Minimizing the surface for AI resume filters.

+

Skill Based Resumes - Resume's are skill based not work history based. This allows companies to know what your good at without infering it.

-
+

response black-hole

@@ -43,11 +53,12 @@
- +

Automated Email Notifications - No longer will you be left in the dark. No matter how the job listing is closed you will be notified

+

More Analytics - Visibility into if the company has looked at your resume, view your resume score

-
+

zombie postings

@@ -59,7 +70,8 @@
- +

Job Postings Auto Close - Job postings auto close 30 days after originally posted. If the company creates a repost habit they will be flagged.

+

Collective Flagging - Companies hold a reputation on the patform and its visible on all their job postings

@@ -75,7 +87,8 @@
- +

Resume Builder - No staring at a blank sheet trying to build a resume. Use our curated resume builder

+

Companies Search Tools - Companies can find you with ease with the strong search tools

\ No newline at end of file diff --git a/src/Client/src/styles.css b/src/Client/src/styles.css index 99bb268..15e206c 100644 --- a/src/Client/src/styles.css +++ b/src/Client/src/styles.css @@ -5,7 +5,7 @@ --Mistox-Medium: #890620; --Mistox-Light: #B6465F; --Mistox-Bright: #FC440F; - --Mistox-Frame: #FF5A00CC; + --Mistox-Frame: #FFE0A5; --Mistox-Button: #ff9999; --Mistox-Button-Hover: #ff999977; --Mistox-White: #FFF;