Skip to main content

Security Module

The Security module provides a small client-side authentication example built with React. It contains reusable form controls, login and logout pages, session helpers, a reactive profile hook, and a local storage-backed service.

Important: This is a demonstration module, not a production authentication system. It does not verify passwords or communicate with a backend. Any email and password creates a local profile, and the profile is stored in localStorage.

Public API

The module entry point re-exports its components, hooks, services, and helper functions:

import {
Button,
InputField,
Label,
Login,
LoginForm,
LoginSystem,
Logout,
login,
logout,
securityService,
useProfile,
} from "./modules/Security";

The shared data types are:

export interface SecurityProfile {
email: string;
name: string;
}

export interface LoginCredentials {
email: string;
password: string;
}

Architecture

The components follow an atomic hierarchy:

LayerContentsResponsibility
AtomsButton, InputField, LabelBasic reusable controls
MoleculesLoginFormCollects login credentials
OrganismsLoginSystemSelects signed-in or signed-out UI
PagesLogin, LogoutRoute-level page components
HooksuseProfileExposes the current reactive session
Librarylogin, logoutApplication-facing session commands
ServicessecurityServiceOwns persistence and change events

The runtime flow is:

  1. LoginForm collects an email address and password.
  2. LoginSystem passes the credentials to login.
  3. login delegates to securityService.login.
  4. The service creates a profile, stores it under security-profile, and dispatches security-session-change.
  5. useProfile receives the event and refreshes subscribed components.
  6. logout removes the profile and emits the same change event.

The service also listens for the browser storage event, so session changes in another tab update mounted consumers.

Usage

Render the complete login experience:

import { Login } from "./modules/Security";

export const SecurityPage = () => <Login />;

Read the active profile:

import { useProfile } from "./modules/Security";

export const AccountStatus = () => {
const { isAuthenticated, profile } = useProfile();

return isAuthenticated ? <span>{profile?.email}</span> : <span>Guest</span>;
};

Call the session commands directly:

import { login, logout } from "./modules/Security";

login({
email: "person@example.com",
password: "demo-only",
});

logout();

Runtime Source

The following listings contain the module's runtime implementation. Storybook stories, mocks, tests, MDX pages, empty template placeholders, and repetitive barrel exports remain in their colocated source files.

Entry Point

index.ts

export * from "./components";
export * from "./hooks";
export * from "./services";
export * from "./lib";

Types

components/_types_/index.ts

export interface SecurityProfile {
email: string;
name: string;
}

export interface LoginCredentials {
email: string;
password: string;
}

Security Service

services/securityService/securityService.ts

import type {
LoginCredentials,
SecurityProfile,
} from "../../components/_types_";

const storageKey = "security-profile";
const sessionEvent = "security-session-change";

const getStorage = () =>
typeof window === "undefined" ? undefined : window.localStorage;

const emitChange = () => {
if (typeof window !== "undefined") {
window.dispatchEvent(new Event(sessionEvent));
}
};

export const securityService = {
getProfile(): SecurityProfile | null {
const value = getStorage()?.getItem(storageKey);

if (!value) return null;

try {
return JSON.parse(value) as SecurityProfile;
} catch {
getStorage()?.removeItem(storageKey);
return null;
}
},

login({ email }: LoginCredentials): SecurityProfile {
const profile = {
email,
name: email.split("@")[0] || "User",
};

getStorage()?.setItem(storageKey, JSON.stringify(profile));
emitChange();
return profile;
},

logout() {
getStorage()?.removeItem(storageKey);
emitChange();
},

subscribe(listener: () => void) {
if (typeof window === "undefined") return () => {};

window.addEventListener("storage", listener);
window.addEventListener(sessionEvent, listener);

return () => {
window.removeEventListener("storage", listener);
window.removeEventListener(sessionEvent, listener);
};
},
};

export default securityService;

Login and Logout Commands

lib/login/login.ts

import type { LoginCredentials } from "../../components/_types_";
import { securityService } from "../../services/securityService/securityService";

export const login = (credentials: LoginCredentials) =>
securityService.login(credentials);

export default login;

lib/logout/logout.ts

import { securityService } from "../../services/securityService/securityService";

export const logout = () => securityService.logout();

export default logout;

Profile Hook

hooks/useProfile/useProfile.ts

import { useEffect, useState } from "react";
import { securityService } from "../../services/securityService/securityService";

export const useProfile = () => {
const [profile, setProfile] = useState(() => securityService.getProfile());

useEffect(
() =>
securityService.subscribe(() => {
setProfile(securityService.getProfile());
}),
[],
);

return {
isAuthenticated: Boolean(profile),
profile,
};
};

export default useProfile;

Button

components/atoms/Button/Button.interface.tsx

import type { ButtonHTMLAttributes, ReactNode } from "react";

export interface ButtonInterface extends ButtonHTMLAttributes<HTMLButtonElement> {
children?: ReactNode;
testID?: string;
}

components/atoms/Button/Button.tsx

import type { ButtonInterface } from "./Button.interface";

const Button = ({
children = "Continue",
className = "",
testID,
type = "button",
...buttonProps
}: ButtonInterface) => (
<button
{...buttonProps}
className={`Button ${className}`.trim()}
data-testid={testID}
type={type}
>
{children}
</button>
);

export default Button;

Input Field

components/atoms/InputField/InputField.interface.tsx

import type { InputHTMLAttributes } from "react";

export interface InputFieldInterface extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
testID?: string;
}

components/atoms/InputField/InputField.tsx

import type { InputFieldInterface } from "./InputField.interface";

const InputField = ({
className = "",
id,
label,
name,
testID,
...inputProps
}: InputFieldInterface) => {
const inputId = id || name;

return (
<label className={`InputField ${className}`.trim()} htmlFor={inputId}>
{label && <span>{label}</span>}
<input {...inputProps} data-testid={testID} id={inputId} name={name} />
</label>
);
};

export default InputField;

Label

components/atoms/Label/Label.interface.tsx

import type { HTMLAttributes, ReactNode } from "react";

export interface LabelInterface extends HTMLAttributes<HTMLSpanElement> {
children?: ReactNode;
testID?: string;
}

components/atoms/Label/Label.tsx

import type { LabelInterface } from "./Label.interface";

const Label = ({
children,
className = "",
testID,
...labelProps
}: LabelInterface) => (
<span
{...labelProps}
className={`Label ${className}`.trim()}
data-testid={testID}
>
{children}
</span>
);

export default Label;

Login Form

components/molecules/LoginForm/LoginForm.interface.tsx

import type { LoginCredentials } from "../../_types_";

export interface LoginFormInterface {
onSubmit?: (credentials: LoginCredentials) => void;
testID?: string;
}

components/molecules/LoginForm/LoginForm.tsx

import { useState, type ChangeEvent, type FormEvent } from "react";
import Button from "../../atoms/Button";
import InputField from "../../atoms/InputField";
import type { LoginFormInterface } from "./LoginForm.interface";

const LoginForm = ({ onSubmit, testID }: LoginFormInterface) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit?.({ email, password });
};

return (
<form className="LoginForm" data-testid={testID} onSubmit={submit}>
<InputField
autoComplete="email"
label="Email"
name="email"
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setEmail(event.target.value)
}
placeholder="you@example.com"
required
type="email"
value={email}
/>
<InputField
autoComplete="current-password"
label="Password"
name="password"
onChange={(event: ChangeEvent<HTMLInputElement>) =>
setPassword(event.target.value)
}
required
type="password"
value={password}
/>
<Button type="submit">Sign in</Button>
</form>
);
};

export default LoginForm;

Login System

components/organisms/LoginSystem/LoginSystem.interface.tsx

export interface LoginSystemInterface {
testID?: string;
}

components/organisms/LoginSystem/LoginSystem.tsx

import Button from "../../atoms/Button";
import Label from "../../atoms/Label";
import LoginForm from "../../molecules/LoginForm";
import { useProfile } from "../../../hooks/useProfile/useProfile";
import { login } from "../../../lib/login/login";
import { logout } from "../../../lib/logout/logout";
import type { LoginSystemInterface } from "./LoginSystem.interface";

const LoginSystem = ({ testID }: LoginSystemInterface) => {
const { profile } = useProfile();

return (
<section className="LoginSystem" data-testid={testID}>
<h1>Security example</h1>
{profile ? (
<div className="LoginSystem__profile">
<Label>Signed in as {profile.name}</Label>
<small>{profile.email}</small>
<Button onClick={logout}>Sign out</Button>
</div>
) : (
<>
<p>Use any email and password to create a local demo session.</p>
<LoginForm onSubmit={login} />
</>
)}
</section>
);
};

export default LoginSystem;

Login Page

components/pages/Login/Login.interface.tsx

export interface LoginInterface {
testID?: string;
}

components/pages/Login/Login.tsx

import LoginSystem from "../../organisms/LoginSystem";
import type { LoginInterface } from "./Login.interface";

const Login = ({ testID }: LoginInterface) => (
<main className="Login" data-testid={testID}>
<LoginSystem />
</main>
);

export default Login;

Logout Page

components/pages/Logout/Logout.interface.tsx

export interface LogoutInterface {
testID?: string;
}

components/pages/Logout/Logout.tsx

import Button from "../../atoms/Button";
import { useProfile } from "../../../hooks/useProfile/useProfile";
import { logout } from "../../../lib/logout/logout";
import type { LogoutInterface } from "./Logout.interface";

const Logout = ({ testID }: LogoutInterface) => {
const { profile } = useProfile();

return (
<main className="Logout" data-testid={testID}>
<h1>Sign out</h1>
<p>
{profile
? `End the session for ${profile.email}?`
: "There is no active session."}
</p>
{profile && <Button onClick={logout}>Sign out</Button>}
</main>
);
};

export default Logout;

Styling

Each component has a colocated SCSS file and class name matching the component, such as .Button, .LoginForm, and .LoginSystem. The current styles contain responsive breakpoint placeholders but no visual declarations. Aggregate _index.scss files expose the component styles to the application's main stylesheet.

Tests and Stories

Every implemented component has:

  • A *.stories.tsx Storybook story.
  • A *.test.tsx component test.
  • A *.mock.ts default props fixture.
  • A colocated interface and SCSS entry point.

The hook and library functions also have MDX documentation beside their source. When behavior changes, update the implementation, its tests, its Storybook story where applicable, and this document together.

Production Requirements

Before using this module for real authentication:

  • Replace securityService.login with a backend authentication request.
  • Never store passwords or sensitive credentials in browser storage.
  • Prefer secure, HttpOnly, SameSite cookies for server-issued sessions.
  • Validate and expire sessions on the server.
  • Add loading, error, and invalid-credentials states.
  • Add authorization checks separately from authentication.
  • Add service and hook tests for persistence, malformed storage, events, and cross-tab updates.