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:
| Layer | Contents | Responsibility |
|---|---|---|
| Atoms | Button, InputField, Label | Basic reusable controls |
| Molecules | LoginForm | Collects login credentials |
| Organisms | LoginSystem | Selects signed-in or signed-out UI |
| Pages | Login, Logout | Route-level page components |
| Hooks | useProfile | Exposes the current reactive session |
| Library | login, logout | Application-facing session commands |
| Services | securityService | Owns persistence and change events |
The runtime flow is:
LoginFormcollects an email address and password.LoginSystempasses the credentials tologin.logindelegates tosecurityService.login.- The service creates a profile, stores it under
security-profile, and dispatchessecurity-session-change. useProfilereceives the event and refreshes subscribed components.logoutremoves 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.tsxStorybook story. - A
*.test.tsxcomponent test. - A
*.mock.tsdefault 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.loginwith a backend authentication request. - Never store passwords or sensitive credentials in browser storage.
- Prefer secure,
HttpOnly,SameSitecookies 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.