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

Reviewed-on: #18
This commit was merged in pull request #18.
This commit is contained in:
2025-08-05 04:16:00 +00:00
25 changed files with 738 additions and 198 deletions
+32 -32
View File
@@ -5,42 +5,42 @@ Server:
When a company is created:
Send email -> verify ownership of the email
Need to timeout email reset tokens:
Resume:
Block API Access as much as possible [ Disallow AI keyword filters ]
Auth:
Make sure autorenew works
When Job Posting Closes Successful:
Update the company rating
JobCleanupService:
Need to update notification email
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
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
database:
Add Applied Jobs Table
Task:
Block API Access as much as possible [ Rate limit | Auth Req | CORS | Disallow AI keyword filters ]
Resume builder minimal user input [ Dont allow AI input ]
Auto unlist jobs after a month of no activity [ Multiple offenders marked ]
Dont allow external applications for users on company sites from the start
Allow company to look up users if their resume is public [ Maybe auto with notify ]
Allow users to look up jobs and apply [ Boost visibility | Completely manual ]
Allow multiple resume's for job specific work
Mark ghost listings to allow users to be informed and put companies on blast
Create advanced filtering tools for company lookup and resume lookup
Create and Auth Database based on the docker compose
Create a server table inside the auth database
Point all requests after auth to the correct regional server. -> Currently only Mistox-West exists
CompanyConnect | need to lookup company before making a new one
Finish Auth setup
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
+31 -2
View File
@@ -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,
@@ -161,9 +163,36 @@ 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;
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;
-- 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,
`ResponseEmail` varchar(255) DEFAULT 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;
+12
View File
@@ -0,0 +1,12 @@
export class Application {
public id: number | null = null;
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;
public rating: number = 0;
public notes: string = "";
}
+2
View File
@@ -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
+8
View File
@@ -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 = "";
}
+50 -50
View File
@@ -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 = "";
}
@@ -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;
}
@@ -4,11 +4,16 @@
</div>
<div class="content-frame">
<div *ngIf="Comp != null">
<h1>{{ Comp.name }}</h1>
<h1>{{ Comp.email }}</h1>
<div class="center-item">
<div><a [href]="'mailto:' + Comp.email" >{{ Comp.email }}</a></div>
<div><h1>{{ Comp.name }}</h1></div>
<div><a [href]="Comp.websiteURL">{{ Comp.websiteURL }}</a></div>
</div>
<div class="center-item">
<img [src]="Comp.logo" />
</div>
<h1>{{ Comp.emailVerified }}</h1>
<h1>{{ Comp.websiteURL }}</h1>
<h1>{{ Comp.logo }}</h1>
<h1>{{ Comp.phone }}</h1>
<h1>{{ Comp.postalCode }}</h1>
<h1>{{ Comp.country }}</h1>
@@ -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;
}
@@ -1,5 +1,11 @@
<div class="center-frame">
<div class="content-frame">
<div class="center-frame center-text border">
<h1>Bored Careers</h1>
<h2>The Anti-AI Job Board</h2>
</div>
<div class="content-frame border">
<div class="floating-frame">
<div class="title-block">
<h1>death by a thousand applicants</h1>
@@ -11,11 +17,14 @@
</div>
</div>
<div class="solution-frame">
<div>
<p><strong>Rate Limiting - </strong>We use strong rate limiting to prevent bot's from flooding applications</p>
<p><strong>Strong Authentication - </strong>All API's require authentication and will ban on suspision of bots</p>
</div>
</div>
</div>
<div class="content-frame">
<div class="content-frame border">
<div class="floating-frame">
<div class="title-block">
<h1>keyword frenzie</h1>
@@ -27,11 +36,12 @@
</div>
</div>
<div class="solution-frame">
<p><strong>No External Access - </strong>All companies and clients are required to interface with the application. Minimizing the surface for AI resume filters.</p>
<p><strong>Skill Based Resumes - </strong>Resume's are skill based not work history based. This allows companies to know what your good at without infering it.</p>
</div>
</div>
<div class="content-frame">
<div class="content-frame border">
<div class="floating-frame">
<div class="title-block">
<h1>response black-hole</h1>
@@ -43,11 +53,12 @@
</div>
</div>
<div class="solution-frame">
<p><strong>Automated Email Notifications - </strong>No longer will you be left in the dark. No matter how the job listing is closed you will be notified</p>
<p><strong>More Analytics - </strong>Visibility into if the company has looked at your resume, view your resume score</p>
</div>
</div>
<div class="content-frame">
<div class="content-frame border">
<div class="floating-frame">
<div class="title-block">
<h1>zombie postings</h1>
@@ -59,7 +70,8 @@
</div>
</div>
<div class="solution-frame">
<p><strong>Job Postings Auto Close - </strong>Job postings auto close 30 days after originally posted. If the company creates a repost habit they will be flagged.</p>
<p><strong>Collective Flagging - </strong>Companies hold a reputation on the patform and its visible on all their job postings</p>
</div>
</div>
@@ -75,7 +87,8 @@
</div>
</div>
<div class="solution-frame">
<p><strong>Resume Builder - </strong>No staring at a blank sheet trying to build a resume. Use our curated resume builder</p>
<p><strong>Companies Search Tools - </strong>Companies can find you with ease with the strong search tools</p>
</div>
</div>
</div>
+1 -1
View File
@@ -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;
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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");
}
}
}
+14
View File
@@ -0,0 +1,14 @@
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 string ResponseEmail { get; set; } = "";
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; } = "";
}
}
+2
View File
@@ -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; }
public int JobsAutoClosed { get; set; }
public string Phone { get; set; } = "";
public string PostalCode { get; set; } = "";
public string Country { get; set; } = ""; // 2 Letter Country Code
+8
View File
@@ -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; } = "";
}
}
+35 -11
View File
@@ -8,11 +8,11 @@ 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);
// Disable null warnings becuse string.IsNullOrEmpty checks for NULL or Empty
#pragma warning disable CS8600
#pragma warning disable CS8604
////////////////////////////////
@@ -36,8 +36,9 @@ 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<DatabaseService>(sp =>
new DatabaseService("server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;")
);
////////////////////////////////
///////// Email Service ////////
@@ -135,15 +136,28 @@ builder.Services.AddAuthentication(options => {
};
});
builder.Services.AddCors(o => o.AddDefaultPolicy(builder => {
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); // No CORS
}));
////////////////////////////////
/// Rate Limiting Service ////
////////////////////////////////
List<string> allowedOrigins = new List<string>{ "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 +170,17 @@ builder.Services.AddRateLimiter(options => {
});
});
// Pages Service
////////////////////////////////
///// Background Services /////
////////////////////////////////
builder.Services.AddHostedService<JobCleanupService>();
////////////////////////////////
///// ASPNET Core Function /////
////////////////////////////////
builder.Services.AddControllers();
builder.Services.AddRazorPages();
var app = builder.Build();
@@ -170,12 +192,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");
@@ -0,0 +1,58 @@
using BoredCareers.Entities;
namespace BoredCareers.Services.TimerService {
public class JobCleanupService : BackgroundService {
private readonly DatabaseService.DatabaseService _db;
private readonly EmailService _em;
public JobCleanupService(DatabaseService.DatabaseService db, EmailService em) {
_db = db;
_em = em;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
while (!stoppingToken.IsCancellationRequested) {
try {
// Get listing's past expire
JobListingDTO[] deletedJobListings = await _db.GetJobListingsPastExipre();
// Group them by CompanyID, ListingCount
Dictionary<int, int> listingsByCompany = deletedJobListings
.GroupBy(l => l.CompanyID)
.ToDictionary(g => g.Key, g => g.Count());
// Update each company's rating
foreach (KeyValuePair<int, int> kvp in listingsByCompany) {
Company? comp = await _db.GetCompany(kvp.Key);
if (comp != null) {
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(2), stoppingToken);
}
}
}
public class JobListingDTO {
public int JobListingID { get; set; }
public int CompanyID { get; set; }
}
}
@@ -0,0 +1,216 @@
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<Application[]> GetApplcationsFromAccount(int AccountID) {
List<Application> applications = new List<Application>();
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");
string _responseemail = reader.GetString("ResponseEmail");
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,
ResponseEmail = _responseemail,
DateApplied = _dateapplied,
ResponseStatus = _responsestatus,
HasBeenViewed = _hasbeenviewed,
Rating = _rating,
Notes = _notes
});
}
}
}
return applications.ToArray();
}
public async Task<Application[]> GetApplicationsFromJobListing(int JobListingID) {
List<Application> applications = new List<Application>();
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");
string _responseemail = reader.GetString("ResponseEmail");
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,
ResponseEmail = _responseemail,
DateApplied = _dateapplied,
ResponseStatus = _responsestatus,
HasBeenViewed = _hasbeenviewed,
Rating = _rating,
Notes = _notes
});
}
}
}
return applications.ToArray();
}
public async Task<string[]> GetApplicationResponseEmailFromJobListing(int JobListingID) {
List<string> emailadds = new List<string>();
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<Application?> 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");
string _responseemail = reader.GetString("ResponseEmail");
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,
ResponseEmail = _responseemail,
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,ResponseEmail,DateApplied,ResponseStatus,HasBeenViewed,Rating,Notes)
VALUES
(@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,
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("@ResponseEmail", application.ResponseEmail);
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();
}
}
}
}
+13 -7
View File
@@ -10,7 +10,7 @@ namespace BoredCareers.Services.DatabaseService {
public async Task<Company?> GetCompany( int CompanyID ) {
Company? company = null;
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
await connection.OpenAsync();
string command = @"
SELECT *
FROM Company
@@ -22,13 +22,14 @@ 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");
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" );
@@ -43,6 +44,8 @@ namespace BoredCareers.Services.DatabaseService {
EmailVerified = _emailVerified,
WebsiteURL = _websiteurl,
Logo = _logo,
JobsAutoClosed = _jobsautoclosed,
JobsClosedSuccessful = _jobsclosedsuccessful,
Phone = _phone,
PostalCode = _postalcode,
Country = _country,
@@ -58,19 +61,20 @@ namespace BoredCareers.Services.DatabaseService {
public async Task<int> 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)
(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,
@@ -88,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);
@@ -104,7 +110,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;
@@ -10,7 +10,7 @@ namespace BoredCareers.Services.DatabaseService {
public async Task<Employee?> 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<Employee[]> GetEmployeesFromCompany(int CompanyID) {
List<Employee> employees = new List<Employee>();
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<Employee[]> GetEmployeesFromAccount(int AccountID) {
List<Employee> employees = new List<Employee>();
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;
@@ -1,4 +1,5 @@
using BoredCareers.Entities;
using BoredCareers.Services.TimerService;
using MySql.Data.MySqlClient;
using System.Data;
using System.Data.Common;
@@ -9,7 +10,7 @@ namespace BoredCareers.Services.DatabaseService {
public async Task<JobListing[]> GetJobListingPage(int PageNumber, int CountPerPage) {
List<JobListing> joblistings = new List<JobListing>();
using (MySqlConnection connection = GetConnection()) {
connection.Open();
await connection.OpenAsync();
string command = @"
SELECT *
FROM JobListing
@@ -24,7 +25,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");
@@ -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
@@ -67,7 +69,7 @@ namespace BoredCareers.Services.DatabaseService {
public async Task<JobListing[]> GetJobListingFromCompany(int CompanyID) {
List<JobListing> joblistings = new List<JobListing>(); ;
using (MySqlConnection connection = GetConnection()) {
connection.Open();
await connection.OpenAsync();
string command = @"
SELECT *
FROM JobListing
@@ -79,7 +81,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");
@@ -92,6 +93,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 +111,7 @@ namespace BoredCareers.Services.DatabaseService {
JobType = _jobtype,
Remote = _remote,
Description = _description,
Skills = _skills,
CreatedTime = _createtime,
ModifiedTime = _modifiedtime,
IsDeleted = _isdeleted
@@ -122,7 +125,7 @@ namespace BoredCareers.Services.DatabaseService {
public async Task<JobListing?> GetJobListing(int JobListingID) {
JobListing? joblisting = null;
using (MySqlConnection connection = GetConnection()) {
connection.Open();
await connection.OpenAsync();
string command = @"
SELECT *
FROM JobListing
@@ -134,7 +137,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");
@@ -147,6 +149,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 +167,7 @@ namespace BoredCareers.Services.DatabaseService {
JobType = _jobtype,
Remote = _remote,
Description = _description,
Skills = _skills,
CreatedTime = _createtime,
ModifiedTime = _modifiedtime,
IsDeleted = _isdeleted
@@ -174,9 +178,35 @@ namespace BoredCareers.Services.DatabaseService {
return joblisting;
}
public async Task SetJobListing( JobListing jobListing ) {
using( MySqlConnection connection = GetConnection() ) {
connection.Open();
public async Task<JobListingDTO[]> GetJobListingsPastExipre() {
List<JobListingDTO> joblistings = new List<JobListingDTO>();
using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync();
string command = @"
SELECT ID, CompanyID
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");
joblistings.Add(new JobListingDTO() {
JobListingID = _id,
CompanyID = _companyid
});
}
}
}
return joblistings.ToArray();
}
public async Task SetJobListing(JobListing jobListing) {
using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync();
string command = @"
INSERT INTO JobListing
@@ -195,12 +225,11 @@ namespace BoredCareers.Services.DatabaseService {
JobType = @JobType,
Remote = @Remote,
Description = @Description,
CreatedTime = @CreatedTime,
ModifiedTime = @ModifiedTime,
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);
@@ -213,25 +242,43 @@ 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();
foreach (JobListingSkill cur in jobListing.Skills) {
await SetJobListingSkills(cur);
}
}
}
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 ) {
using( MySqlConnection connection = GetConnection() ) {
public async Task DeleteJobListing(int JobListingID) {
using (MySqlConnection connection = GetConnection()) {
MySqlCommand cmd;
connection.Open();
await connection.OpenAsync();
string command = @"
UPDATE JobListing
SET IsDeleted = TRUE
WHERE ID = @ID;
";
cmd = new MySqlCommand( command, connection );
cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@ID", JobListingID);
await cmd.ExecuteNonQueryAsync();
@@ -0,0 +1,67 @@
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<JobListingSkill[]> GetJobListingSkills(int JobListingID) {
List<JobListingSkill> joblistingskills = new List<JobListingSkill>();
using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync();
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() ) {
await connection.OpenAsync();
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();
}
}
}
}
@@ -9,7 +9,7 @@ namespace BoredCareers.Services.DatabaseService {
public async Task<Resume[]> GetResumes(int AccountID) {
List<Resume> resumes = new List<Resume>();
using (MySqlConnection connection = GetConnection()) {
connection.Open();
await connection.OpenAsync();
string command = @"
SELECT *
FROM Resume
@@ -5,8 +5,8 @@ namespace BoredCareers.Services {
// @VerifyPassword
// https://mistox.com/api/account/verifyemail?UserName=@UserName&Guid=@VerifyPassword
public static string VerifyEmailSubject = "Verify Your Email Address";
public static string VerifyEmailEmail = @"
public static string JobAutoClosedSubject = "Verify Your Email Address";
public static string JobAutoClosedEmail = @"
<!DOCTYPE html>
<html lang=""en"">
<head>
@@ -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 = @"
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
<title>Password Reset</title>
</head>
<body style=""font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;"">
<table role=""presentation"" style=""width: 100%; background-color: #f4f4f4; padding: 20px 0;"">
<tr>
<td>
<table role=""presentation"" style=""max-width: 600px; width: 100%; background-color: #ffffff; margin: 0 auto; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);"">
<tr>
<td style=""padding: 20px; text-align: center; background-color: #4CAF50; color: #ffffff; border-top-left-radius: 8px; border-top-right-radius: 8px;"">
<h2>Password Reset Request</h2>
</td>
</tr>
<tr>
<td style=""padding: 20px; text-align: left; font-size: 16px; color: #333333;"">
<p>Hi @UserName,</p>
<p>We received a request to reset your password. You can reset your password by clicking the button below:</p>
<p style=""text-align: center;"">
<a href=""https://mistox.com/account/resetpassword?UserName=@UserName&ResetPwd=@ResetPassWord"" style=""background-color: #4CAF50; color: #ffffff; text-decoration: none; padding: 15px 25px; font-size: 16px; border-radius: 5px; display: inline-block;"">Reset Password</a>
</p>
<p>If you didn't request a password reset, you can safely ignore this email.</p>
<p>Best regards</p>
</td>
</tr>
<tr>
<td style=""padding: 10px; text-align: center; background-color: #f4f4f4; color: #888888; font-size: 12px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;"">
<p>If you have any questions, feel free to <a href=""mailto:webmaster@mistox.com"" style=""color: #4CAF50; text-decoration: none;"">contact support</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
";
}
}