Storiyan EHMRS
Archived
Product · Web

Storiyan 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.

Built with
PHPPHP
MySQLMySQL
HTMLHTML
CSSCSS
JavaScriptJavaScript
Project Details
RESOURCES

STATUS
Archived
YEAR

2022

TYPE

Product · Web

TAGS
Authentication
Onboarding

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. .htaccess rewrites any single-segment path to index.php?type=$1, and index.php dispatches on it, stitching a shared template.html shell together with the page partial through Apache's virtual(). The same file also turns on server-parsed handling, which is what makes virtual() available at all — without mod_include the dispatcher is a call to an undefined function.
  • Referrer-gated routes. The verify, verify-success and verify-failure screens each check that HTTP_REFERER matches the exact expected origin plus path. Deep-linking any of them returns a real 403 Forbidden status 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 key cookie maps to a row in logged_in holding 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.in lookup 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 dashboarddashboard_index.php checks login before rendering anything, includes the dashboard header and applicant navigation, and loads panel content into #data_html client-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
RESOURCES

STATUS
Archived
YEAR

2022

TYPE

Product · Web

TAGS
Authentication
Onboarding