working #40

Merged
derek merged 8 commits from working into main 2025-09-04 20:15:04 -07:00
10 changed files with 230 additions and 83 deletions
Showing only changes of commit 73c1e1ce98 - Show all commits
+13
View File
@@ -15,6 +15,19 @@ Server:
Emails: Emails:
Make emails follow theme of website better Make emails follow theme of website better
JobListingController:
Dont refresh on every filter edit
Line 63 is terrible and need to be fixed in the JobListing DB JobListingController
bools and numbers are getting strigified which breaks the mysql parameters
Validation:
Alot of the validation is only taking place client side.
Need to validate all inputs before processing
Phone number
Email Address
City, CountryCode, PostalCode
When applying to a job, The server doesnt make sure that the company only fields are not modified
Client: Client:
jobs/editor: jobs/editor:
Want to add completed job listing preview at end of carosel Want to add completed job listing preview at end of carosel
+2 -2
View File
@@ -193,8 +193,8 @@ CREATE TABLE IF NOT EXISTS `PostalCodes` (
`StateCode` varchar(20), `StateCode` varchar(20),
`County` varchar(100), `County` varchar(100),
`CountyCode` varchar(20), `CountyCode` varchar(20),
`Admin` varchar(100), `Community` varchar(100),
`AdminCode` varchar(20), `CommunityCode` varchar(20),
`Latitude` float, `Latitude` float,
`Longitude` float, `Longitude` float,
`Accuracy` varchar(2) `Accuracy` varchar(2)
+11
View File
@@ -0,0 +1,11 @@
export class JobFilter {
public JobsPerPage: number = 20;
public CurrentPage: number = 1;
public CountryCode: string | null = null;
public PostalCode: string | null = null;
public Distance: number | null = null;
public JobType: string | null = null;
public Remote: boolean | null = null;
public SalaryMin: number | null = null;
public SalaryMax: number | null = null;
}
@@ -1,3 +1,35 @@
<!-- Filter Bar -->
<div>
<div>
<h1>Country</h1>
<input name="CountryCode" [(ngModel)]="currentFilter.CountryCode" (ngModelChange)="reloadFilters()" type="text" />
</div>
<div>
<h1>Postal Code</h1>
<input name="PostalCode" [(ngModel)]="currentFilter.PostalCode" (ngModelChange)="reloadFilters()" type="text" />
</div>
<div>
<h1>Distance</h1>
<input name="Distance" [(ngModel)]="currentFilter.Distance" (ngModelChange)="reloadFilters()" type="number" />
</div>
<div>
<h1>Job Type</h1>
<input name="CountryCode" [(ngModel)]="currentFilter.JobType" (ngModelChange)="reloadFilters()" type="text" />
</div>
<div>
<h1>Remote</h1>
<input name="Remote" [(ngModel)]="currentFilter.Remote" (ngModelChange)="reloadFilters()" type="checkbox" />
</div>
<div>
<h1>Minimum Salary</h1>
<input name="SalaryMin" [(ngModel)]="currentFilter.SalaryMin" (ngModelChange)="reloadFilters()" type="number" />
</div>
<div>
<h1>Maximum Salary</h1>
<input name="SalaryMax" [(ngModel)]="currentFilter.SalaryMax" (ngModelChange)="reloadFilters()" type="number" />
</div>
</div>
<!-- Avaliable Jobs --> <!-- Avaliable Jobs -->
<div class="tile-frame"> <div class="tile-frame">
@for (cur of JobListingPage; track cur.id){ @for (cur of JobListingPage; track cur.id){
@@ -21,4 +53,8 @@
</div> </div>
</div> </div>
} }
<div>
<h1>Jobs Per Page</h1>
<input name="JobsPerPage" [(ngModel)]="currentFilter.JobsPerPage" (ngModelChange)="reloadFilters()" type="number" />
</div>
</div> </div>
@@ -6,6 +6,7 @@ import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { JobListing } from 'app/models/JobListing'; import { JobListing } from 'app/models/JobListing';
import { Authentication } from 'app/services/Authentication'; import { Authentication } from 'app/services/Authentication';
import { JobFilter } from 'app/models/JobFilter';
@Component({ @Component({
selector: 'main-jobs', selector: 'main-jobs',
@@ -15,7 +16,7 @@ import { Authentication } from 'app/services/Authentication';
}) })
export class JobsComponent { export class JobsComponent {
public MyJobListings: JobListing[] = []; public currentFilter: JobFilter;
public JobListingPage: JobListing[] = []; public JobListingPage: JobListing[] = [];
public ErrorMsg: string = ""; public ErrorMsg: string = "";
@@ -23,10 +24,46 @@ export class JobsComponent {
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 | BoredCareers"); this.title.setTitle("Jobs | BoredCareers");
this.currentFilter = new JobFilter();
}; };
ngOnInit(){ ngOnInit(){
this.http.get<JobListing[]>("api/joblisting?PageQuantity=" + 10 + "&Page=" + 1).subscribe({ this.http.get<JobListing[]>("api/joblisting?PageQuantity=" + this.currentFilter.JobsPerPage + "&Page=" + this.currentFilter.CurrentPage).subscribe({
next: data => {
this.JobListingPage = data;
},
error: err => {
this.ErrorMsg = err.error;
}
});
}
reloadFilters(){
var queryBuilder = "api/joblisting?PageQuantity=" + this.currentFilter.JobsPerPage + "&Page=" + this.currentFilter.CurrentPage;
if (this.currentFilter.PostalCode != null){
queryBuilder += "&PC=" + this.currentFilter.PostalCode;
}
if (this.currentFilter.CountryCode != null){
queryBuilder += "&CC=" + this.currentFilter.CountryCode;
}
if (this.currentFilter.Distance != null){
queryBuilder += "&D=" + this.currentFilter.Distance;
}
if (this.currentFilter.JobType != null){
queryBuilder += "&JT=" + this.currentFilter.JobType;
}
if (this.currentFilter.Remote != null){
queryBuilder += "&R=" + this.currentFilter.Remote;
}
if (this.currentFilter.SalaryMin != null){
queryBuilder += "&SMI=" + this.currentFilter.SalaryMin;
}
if (this.currentFilter.SalaryMax != null){
queryBuilder += "&SMA=" + this.currentFilter.SalaryMax;
}
this.http.get<JobListing[]>(queryBuilder).subscribe({
next: data => { next: data => {
this.JobListingPage = data; this.JobListingPage = data;
}, },
+25 -10
View File
@@ -74,16 +74,31 @@ export class Validation {
///////// HELPER FUNCTIONS ///////// ///////// HELPER FUNCTIONS /////////
isPrivateIPv6(ip: string): boolean { isPrivateIPv6(ip: string): boolean {
try { try {
const normalized = ip.replace(/^\[|\]$/g, '').toLowerCase(); const normalized = ip.replace(/^\[|\]$/g, '').toLowerCase();
if (normalized === '::1') return true; if (normalized === '::1') return true;
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true; if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
const first4 = normalized.slice(0, 4); const first4 = normalized.slice(0, 4);
if (first4 >= 'fe80' && first4 <= 'febf') return true; if (first4 >= 'fe80' && first4 <= 'febf') return true;
return false; return false;
} catch { } catch {
return true; return true;
}
}
///////// GETTERS /////////
get ValidCountries(): string[] {
return ['AD', 'AE', 'AI', 'AL', 'AR', 'AS', 'AT', 'AU', 'AX', 'AZ', 'BD', 'BE', 'BG',
'BM', 'BR', 'BY', 'CA', 'CC', 'CH', 'CL', 'CN', 'CO', 'CR', 'CX', 'CY', 'CZ',
'DE', 'DK', 'DO', 'DZ', 'EC', 'EE', 'ES', 'FI', 'FK', 'FM', 'FO', 'FR', 'GB',
'GF', 'GG', 'GI', 'GL', 'GP', 'GS', 'GT', 'GU', 'HK', 'HM', 'HN', 'HR', 'HT',
'HU', 'ID', 'IE', 'IM', 'IN', 'IO', 'IS', 'IT', 'JE', 'JP', 'KE', 'KR', 'LI',
'LK', 'LT', 'LU', 'LV', 'MA', 'MC', 'MD', 'MH', 'MK', 'MO', 'MP', 'MQ', 'MT',
'MW', 'MX', 'MY', 'NC', 'NF', 'NL', 'NO', 'NR', 'NU', 'NZ', 'PA', 'PE', 'PF',
'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PT', 'PW', 'RE', 'RO', 'RS', 'RU', 'SE',
'SG', 'SI', 'SJ', 'SK', 'SM', 'TC', 'TH', 'TR', 'UA', 'US', 'UY', 'VA', 'VI',
'WF', 'WS', 'YT', 'ZA'];
} }
}
} }
+20 -2
View File
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services.DatabaseService; using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities; using BoredCareers.Entities;
using Org.BouncyCastle.Tls;
namespace BoredCareers.Controllers { namespace BoredCareers.Controllers {
[ApiController] [ApiController]
@@ -31,8 +32,25 @@ namespace BoredCareers.Controllers {
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GetJobListings(int Page = 1, int PageQuantity = 25) { public async Task<IActionResult> GetJobListings(string? CC, string? PC, float? D, string? JT, bool? R, int? SMI, int? SMA, int Page = 1, int PageQuantity = 25 ) {
JobListing[] jobListings = await _databaseService.GetJobListingPage(Page, PageQuantity);
// Get all relevant postal codes
List<string> PostalCodes = new List<string>();
if (PC != null && CC != null && D != null) {
Location[] nearby = await _databaseService.GetNearbyLocations(PC, CC, D.Value);
foreach (Location cur in nearby) {
PostalCodes.Add(cur.PostalCode);
}
} else if (PC != null) {
PostalCodes.Add(PC);
}
string[]? pc = null;
if (PostalCodes.Count > 0) {
pc = PostalCodes.ToArray();
}
JobListing[] jobListings = await _databaseService.GetJobListingPage(Page, PageQuantity, pc, CC, JT, R, SMI, SMA );
return Ok(jobListings); return Ok(jobListings);
} }
+3 -9
View File
@@ -1,25 +1,19 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services.DatabaseService; using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities; using BoredCareers.Entities;
using BoredCareers.Services;
namespace BoredCareers.Controllers { namespace BoredCareers.Controllers {
[ApiController] [ApiController]
[Route("api/location")] [Route("api/location")]
public class LocationController : MistoxControllerBase { public class LocationController : MistoxControllerBase {
public LocationController(DatabaseService db, EmailService emailContext) : base(db) {} public LocationController(DatabaseService db) : base(db) {}
[HttpGet] [HttpGet]
public async Task<IActionResult> GetCompany(string PostalCode, string CountryCode, float MaxDistanceKm) { public async Task<IActionResult> GetCompany(string PostalCode, string CountryCode, float MaxDistanceKm) {
if (isLoggedIn()) { if (isLoggedIn()) {
Location[] places = await _databaseService.GetNearbyLocations(PostalCode, CountryCode, MaxDistanceKm);
Location? MyZIP = await _databaseService.GetLocation(PostalCode, CountryCode); return Ok(places.ToArray());
if (MyZIP != null) {
Location[] places = await _databaseService.GetNearbyLocations(MyZIP.Latitude, MyZIP.Longitude, MyZIP.CountryCode, MaxDistanceKm);
return Ok(places.ToArray());
}
return NotFound("Postal + CountryCode not found");
} }
return NotFound("Not logged in"); return NotFound("Not logged in");
} }
@@ -7,21 +7,73 @@ using System.Data.Common;
namespace BoredCareers.Services.DatabaseService { namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService { public partial class DatabaseService {
public async Task<JobListing[]> GetJobListingPage(int PageNumber, int CountPerPage) { public async Task<JobListing[]> GetJobListingPage(int PageNumber, int CountPerPage, string[]? PostalCodes = null, string? CountryCode = null, string? JobType = null, bool? Remote = null, int? SalaryMin = null, int? SalaryMax = null ) {
List<JobListing> joblistings = new List<JobListing>(); List<JobListing> joblistings = new List<JobListing>();
using (MySqlConnection connection = GetConnection()) { using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync(); await connection.OpenAsync();
string command = @"
SELECT *
FROM JobListing
WHERE IsDeleted = FALSE
ORDER BY CreatedTime DESC
LIMIT @PageSize OFFSET @PageNumber;
";
MySqlCommand cmd = new MySqlCommand(command, connection); string select = "SELECT * FROM JobListing";
string order = " ORDER BY CreatedTime DESC";
string limit = " LIMIT @PageSize OFFSET @PageNumber;";
List<string> Filters = new List<string>();
List<string> Parameters = new List<string>();
List<string> ParameterName = new List<string>();
if (PostalCodes != null) {
for (int i = 0; i < PostalCodes.Length; i++) {
Filters.Add("PostalCode");
Parameters.Add(PostalCodes[i]);
ParameterName.Add("@PostalCode" + i);
}
}
if (CountryCode != null) {
Filters.Add("Country");
Parameters.Add(CountryCode);
ParameterName.Add("@CountryCode");
}
if (JobType != null) {
Filters.Add("JobType");
Parameters.Add(JobType);
ParameterName.Add("@JobType");
}
if (Remote != null) {
Filters.Add("Remote");
Parameters.Add(Remote.Value.ToString());
ParameterName.Add("@Remote");
}
if (SalaryMin != null) {
Filters.Add("SalaryMin");
Parameters.Add(SalaryMin.Value.ToString());
ParameterName.Add("@SalaryMin");
}
if (SalaryMax != null) {
Filters.Add("SalaryMax");
Parameters.Add(SalaryMax.Value.ToString());
ParameterName.Add("@SalaryMax");
}
string filter = " WHERE IsDeleted = False";
for (int i = 0; i < Filters.Count; i++) {
if (Filters[i] == "PostalCode" && i != 0) {
filter += " OR ";
} else {
filter += " AND ";
}
filter += Filters[i] + " = " + ParameterName[i];
}
MySqlCommand cmd = new MySqlCommand(select + filter + order + limit, connection);
cmd.Parameters.AddWithValue("@PageSize", CountPerPage); cmd.Parameters.AddWithValue("@PageSize", CountPerPage);
cmd.Parameters.AddWithValue("@PageNumber", (PageNumber - 1) * CountPerPage); cmd.Parameters.AddWithValue("@PageNumber", (PageNumber - 1) * CountPerPage);
for (int i = 0; i < Filters.Count; i++) {
cmd.Parameters.AddWithValue( ParameterName[i], Parameters[i] );
}
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) { while (await reader.ReadAsync()) {
+20 -49
View File
@@ -7,64 +7,35 @@ using System.Text;
namespace BoredCareers.Services.DatabaseService { namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService { public partial class DatabaseService {
public async Task<Location?> GetLocation(string PostalCode, string CountryCode) { public async Task<Location[]> GetNearbyLocations(string PostalCode, string CountryCode, float MaxDistanceKm) {
using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync();
string command = @"
SELECT PostalCode, CountryCode, Latitude, Longitude, City
FROM PostalCodes
WHERE PostalCode = @PostalCode
AND CountryCode = @CountryCode;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@PostalCode", PostalCode);
cmd.Parameters.AddWithValue("@CountryCode", CountryCode);
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
string _city = reader.GetString("City");
string _postalCode = reader.GetString("PostalCode");
string _countryCode = reader.GetString("CountryCode");
float _latitude = reader.GetFloat("Latitude");
float _longitude = reader.GetFloat("Longitude");
return new Location() {
City = _city,
PostalCode = _postalCode,
CountryCode = _countryCode,
Latitude = _latitude,
Longitude = _longitude
};
}
}
}
return null;
}
public async Task<Location[]> GetNearbyLocations(float Latitude, float Longitude, string CountryCode, float MaxDistanceKm) {
List<Location> closePostalCodes = new List<Location>(); List<Location> closePostalCodes = new List<Location>();
using (MySqlConnection connection = GetConnection()) { using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync(); await connection.OpenAsync();
string command = @" string command = @"
SELECT PostalCode, CountryCode, Latitude, Longitude, City, ( SELECT
6371 * acos( pc2.PostalCode,
cos(radians(@Latitude)) * pc2.CountryCode,
cos(radians(Latitude)) * pc2.Latitude,
cos(radians(Longitude) - radians(@Longitude)) + pc2.Longitude,
sin(radians(@Latitude)) * pc2.City,
sin(radians(Latitude)) (
) 6371 * acos(
) AS distance_km cos(radians(pc1.Latitude)) *
FROM PostalCodes cos(radians(pc2.Latitude)) *
WHERE countrycode = @CountryCode cos(radians(pc2.Longitude) - radians(pc1.Longitude)) +
sin(radians(pc1.Latitude)) *
sin(radians(pc2.Latitude))
)
) AS distance_km
FROM PostalCodes pc1
JOIN PostalCodes pc2 ON pc2.CountryCode = 'us'
WHERE pc1.PostalCode = @PostalCode AND pc1.CountryCode = @CountryCode
HAVING distance_km <= @MaxDistanceKm HAVING distance_km <= @MaxDistanceKm
ORDER BY distance_km; ORDER BY distance_km;
"; ";
MySqlCommand cmd = new MySqlCommand(command, connection); MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@Latitude", Latitude); cmd.Parameters.AddWithValue("@PostalCode", PostalCode);
cmd.Parameters.AddWithValue("@Longitude", Longitude);
cmd.Parameters.AddWithValue("@CountryCode", CountryCode); cmd.Parameters.AddWithValue("@CountryCode", CountryCode);
cmd.Parameters.AddWithValue("@MaxDistanceKm", MaxDistanceKm); cmd.Parameters.AddWithValue("@MaxDistanceKm", MaxDistanceKm);