More Projects
59 in totalStoriyan EHMRS
2022
PHP front-controller web app: index.php dispatches on a ?type= query parameter and stitches HTML partials with Apache virtual() includes, covering login, a two-step onboarding form, email verification with success and failure states, forgot-password, a dashboard and 403/404 pages, with HTTP-referrer checks guarding the verification routes.
Project Details
2022
Product · Web
The requirement list checked into the repo is six lines long: a login page, a registration page, something to persist the data, a cookie-based login system, a user dashboard, and a second step of the form. What that adds up to is an applicant portal — sign up, prove the email is real, fill a multi-step application, come back later and see how far you got. It was written in plain PHP against Apache with no framework, so routing, sessions and verification all had to be built rather than configured.
That constraint is the whole story. There is no Composer, no autoloader, no template engine and no session library. Everything the framework would have handed over — how a URL becomes a page, how a logged-in visitor is recognised on the next request, how a partial gets stitched into a layout — had to be answered from Apache primitives and PHP's standard library. Most of the interesting code in the repository is one of those answers.
What was built
- A front controller.
.htaccessrewrites any single-segment path toindex.php?type=$1, andindex.phpdispatches on it, stitching a sharedtemplate.htmlshell together with the page partial through Apache'svirtual(). The same file also turns onserver-parsedhandling, which is what makesvirtual()available at all — withoutmod_includethe dispatcher is a call to an undefined function. - Referrer-gated routes. The verify, verify-success and verify-failure screens each check
that
HTTP_REFERERmatches the exact expected origin plus path. Deep-linking any of them returns a real403 Forbiddenstatus with the 403 partial rendered, and an unrecognised path gets the same 403 status with the 404 partial behind it. - Email verification with teeth — a six-digit code stored with its creation timestamp, valid for five minutes, three attempts tracked in the PHP session, and every prior code for that address deleted before a new one is issued.
- IP-bound sessions. A
keycookie maps to a row inlogged_inholding the session key, the email, the client IP and a state flag. Every authorisation check re-matches the key and the requesting IP, so a cookie lifted to another address does not open the account. - Two-step onboarding. Step one takes name, email and password with server-side regex
validation mirroring the client's, returning per-field errors as JSON. Step two takes phone
and pincode, where a valid six-digit pincode fires an
api.postalpincode.inlookup that auto-fills district and state and marks both fields valid. - Applications as first-class rows. Each gets a generated UUID as its reference number, plus type, date, status and a step counter rendered to the applicant as "Step 2 / 5 Completed".
- An applicant dashboard —
dashboard_index.phpchecks login before rendering anything, includes the dashboard header and applicant navigation, and loads panel content into#data_htmlclient-side. - Forgot-password flow and a login screen that redirects an already-authenticated visitor
straight to
/dashboard. - Ten SVG backgrounds picked at random on load, so repeat visits to the auth screens do not look identical.
The schema you have to infer
There is no SQL file in the repository, so the tables exist only as the shape of the queries
against them. Four of them: ehmrs_users, ehmrs_application, logged_in, and
ehmrs_verification_email.
ehmrs_users is the revealing one. Every lookup is written WHERE email='$x' || emp_code='$x',
so a person can sign in with either an address or an employee code — but nothing in this
repository ever writes emp_code. That column is provisioned by the HR system on the other
side of the wall, and its presence in the login query is the seam where the applicant portal
meets it. Registration writes user_state as the literal string applicant, which is the same
column an existing employee would carry a different value in.
logged_in is the hand-built session table: display name, email, session key, IP, and a state
flag where zero means active. It has no expiry column, which is the design decision the whole
auth story hinges on and the one that eventually bites.
Recognising a returning visitor without a session library
PHP's own $_SESSION is used in exactly one place, for the verification attempt counter. The
actual logged-in identity is a separate mechanism built from scratch.
On a successful login the server mints a UUID-shaped token, inserts a row into logged_in with
the requester's IP, and hands the token to the browser. Every protected page then re-queries
that table on the key and the IP together. Both halves have to match, so the token alone is not
enough. That is a genuinely good instinct built on two shaky foundations: the token comes from
mt_rand rather than a cryptographic source, and the client IP is resolved from a ladder of
forwarding headers before falling back to the real socket address, so the second factor is
supplied by the same party as the first.
Verification runs its own small state machine. On entry the generator deletes every outstanding code for that address before issuing a new one, so there is exactly one live code per email at any moment. Freshness is checked at read time by diffing the stored timestamp rather than by a sweeper job, and the checker returns three distinct outcomes — verified, wrong code, expired — so the caller can decrement the attempt counter for one and end the flow for the other. The attempt count lives in the PHP session rather than the database, which means it resets with a new session and does not survive a restart.
Where it stands
Archived. The application model supports five steps and the code writes step 1 and step 2;
steps three through five were never built, and the code that mails the verification code is
still a TODO next to the generator that creates it. There is no mail library anywhere in the
repository, so the six-digit code exists only in the database — which means verification could
never have completed for a real applicant, however carefully the flow around it was designed.
The rest of the unfinished list is a fair picture of where a solo build stops. Both onboarding
handlers end their success path with a debug string and a comment saying to redirect somewhere,
so a completed registration leaves the applicant looking at the words "Success Creating
Databse". The step-two form has one application id hardcoded into its action, because the page
that would have rendered it with a real one was never written. The "Complete Application"
button carries an empty onclick. The dashboard navigation offers seven destinations and the
router behind it serves one, ignoring the requested hash and always fetching the applications
list. Logout clears the cookie without flipping the row it points at, and since logged_in has
no expiry, a copy of that token stays valid.
The parts that were finished — the router, the referrer guards, the IP-bound session check and
the pincode autofill — are the parts that were hard, and they work. What dates the project is
everything underneath them: queries assembled by string interpolation with no prepared
statements, a psw_hash column holding the password as typed, and connection credentials
committed alongside the code. The todo.txt at the root reads like a plan and mostly got done;
its last line is a note to install a TODO-highlighting editor extension, which is why the
unfinished edges are all marked in the source rather than forgotten.
Project Details
2022
Product · Web