npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tenminuteschool/auth-admin-react

v0.8.0

Published

OAuth 2.0 + PKCE browser client for Login with 10MinuteSchool (admin)

Readme

@tenminuteschool/auth-admin-react

Add "Login with 10MS Admin" to any React app in minutes.


AI Prompt

Copy and paste this into your AI assistant to get a full working implementation:

Implement "Login with 10MS Admin" in this project using @tenminuteschool/auth-admin-react.

Steps:
1. Install: npm install @tenminuteschool/auth-admin-react

2. Add to .env.local:
   NEXT_PUBLIC_TENMS_CLIENT_ID=YOUR_CLIENT_ID

3. Create lib/auth.ts:
   import { TenMSAuth } from '@tenminuteschool/auth-admin-react';
   export const CLIENT_ID = process.env.NEXT_PUBLIC_TENMS_CLIENT_ID!;
   export const auth = new TenMSAuth({ clientId: CLIENT_ID, storage: 'localStorage' });

4. Create app/providers.tsx (a "use client" component):
   'use client';
   import { TenMSAuthProvider } from '@tenminuteschool/auth-admin-react';
   import { auth } from '@/lib/auth';
   export function Providers({ children }) {
     return <TenMSAuthProvider auth={auth}>{children}</TenMSAuthProvider>;
   }
   Then wrap {children} with <Providers> in app/layout.tsx (which stays a
   Server Component — don't import TenMSAuthProvider there directly).

5. Create a login page that:
   - Imports LoginButton and CLIENT_ID, and calls useTenMSAuth() to get
     { user, loading, refresh }
   - If loading, render nothing yet. If user is already set, redirect to
     /dashboard
   - Shows the button with clientId={CLIENT_ID} and onSuccess handler
   - In onSuccess: await auth.handleLoginSuccess(response), then call
     refresh(), then redirect to /dashboard

6. On the dashboard page:
   - Call useTenMSAuth() to get { user, loading, auth, refresh }
   - If loading, render nothing yet. If no user, redirect back to the login
     page
   - Render user.name / user.email / user.picture
   - Add a logout button that calls auth.logout(), then refresh(), then
     redirects back to the login page

Do not hardcode the client ID anywhere. Do not change the storage value.
Do not import TenMSAuthProvider directly into app/layout.tsx or any other
Server Component — always go through the small "use client" wrapper from
step 4. Do not call auth.isLoggedIn() / auth.getUser() directly in components
to read session state — use useTenMSAuth() instead. Do not call
auth.handleTokenHandoff() yourself anywhere — TenMSAuthProvider already calls
it once, globally; calling it again races with that and can drop a handoff
token before the provider sees it. Do not call auth.handleLoginSuccess() or
auth.logout() without immediately calling refresh() right after — the
provider does not detect those on its own.

Installation

npm install @tenminuteschool/auth-admin-react
# or
pnpm add @tenminuteschool/auth-admin-react

Setup

Add your client ID to .env.local:

NEXT_PUBLIC_TENMS_CLIENT_ID=your_client_id_here

Then create this file once at the root of your project:

// lib/auth.ts
import { TenMSAuth } from '@tenminuteschool/auth-admin-react';

export const CLIENT_ID = process.env.NEXT_PUBLIC_TENMS_CLIENT_ID!;

export const auth = new TenMSAuth({
  clientId: CLIENT_ID,
  storage: 'localStorage',
});

Session provider

Wrap your app once with TenMSAuthProvider to get session state via a hook, and to support signing a user in via a tenms_token URL param handed off from another app (that app can redirect to any page in yours — the provider handles it globally, wherever it lands, instead of you having to check for it on every page yourself).

TenMSAuthProvider uses hooks, so it needs "use client" — but your root layout doesn't have to become a client component to use it. Wrap it in your own small client component instead:

// app/providers.tsx
'use client';
import { TenMSAuthProvider } from '@tenminuteschool/auth-admin-react';
import { auth } from '@/lib/auth';

export function Providers({ children }: { children: React.ReactNode }) {
  return <TenMSAuthProvider auth={auth}>{children}</TenMSAuthProvider>;
}
// app/layout.tsx — stays a Server Component
import { Providers } from './providers';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Then anywhere in the tree:

import { useTenMSAuth } from '@tenminuteschool/auth-admin-react';

const { auth, user, loading, error, refresh } = useTenMSAuth();

| Field | Description | |---|---| | auth | The same TenMSAuth instance you passed to the provider | | user | The current session's user, or null | | loading | true until the initial session check resolves — gate session-dependent UI on this | | error | Set if a tenms_token handoff was present but failed (invalid/expired/non-admin token) | | refresh() | Re-reads the session into context state — call this right after auth.handleLoginSuccess() or auth.logout(), since those write to storage without the provider knowing |

Common mistakes:

  • Importing TenMSAuthProvider straight into app/layout.tsx instead of through the wrapper above — this breaks the build, since a Server Component can't render something that needs "use client" directly.
  • Reading session state with auth.isLoggedIn() / auth.getUser() inside a component instead of useTenMSAuth() — this bypasses the provider's handling of an incoming tenms_token handoff and its loading state, and can flash the wrong UI.
  • Calling auth.handleTokenHandoff() yourself anywhere — the provider already does this once, globally, on mount. A second call races it and can consume the tenms_token before the provider processes it.
  • Calling auth.handleLoginSuccess() / auth.logout() without calling refresh() right after — the provider has no other way to know storage changed.

Protecting a page

useRequireAuth() collapses the "redirect away if there's no session" guard into one line, instead of hand-writing a useEffect on every protected page:

import { useRequireAuth } from '@tenminuteschool/auth-admin-react';

const { user, loading } = useRequireAuth();
if (loading || !user) return null;

It waits for the initial session check (including any in-flight cross-app handoff) to settle before redirecting, so it won't bounce someone who's about to be signed in. Defaults to a full window.location.href = '/' redirect, which works with zero framework dependency — pass your own router for a client-side transition instead of a full page reload:

import { useRouter } from 'next/navigation';
import { useRequireAuth } from '@tenminuteschool/auth-admin-react';

const router = useRouter();
const { user, loading } = useRequireAuth({ redirectTo: '/', redirect: router.replace });

Note this only protects the one page it's called on — there's no project-wide route list. Add it to whichever pages need a session; leave it off any page that should stay public.


Login Button

import { LoginButton, useTenMSAuth } from '@tenminuteschool/auth-admin-react';

const { auth, refresh } = useTenMSAuth();

<LoginButton
  clientId={CLIENT_ID}
  onSuccess={async (response) => {
    const session = await auth.handleLoginSuccess(response);
    // session.user → { sub, name, email, picture, ... }
    refresh();
    window.location.href = '/dashboard';
  }}
  onError={(err) => console.error(err)}
/>

Control which login methods are shown

methods takes an array, same style as scopes. Defaults to ['google']:

// Google sign-in only (default — no prop needed)
<LoginButton clientId="..." onSuccess={...} />
<LoginButton clientId="..." onSuccess={...} methods={['google']} />

// Email/password only
<LoginButton clientId="..." onSuccess={...} methods={['email']} />

// Both
<LoginButton clientId="..." onSuccess={...} methods={['google', 'email']} />

Button appearance

<LoginButton
  clientId="..."
  onSuccess={...}
  theme="dark"       // 'dark' (default) | 'light'
  variant="fill"     // 'fill' (default) | 'outline'
  size="large"       // 'large' (default) | 'medium' | 'small'
  text="Sign in"     // custom label
/>

Reading the session

Prefer useTenMSAuth() (see above) inside components under TenMSAuthProvider. The underlying TenMSAuth methods it wraps are also available directly — useful outside React, or in the provider's own auth instance:

import { auth } from '@/lib/auth';

auth.isLoggedIn()           // true/false — no network call
auth.getUser()              // { sub, name, email, picture, ... } or null
await auth.getAccessToken() // valid token string, auto-refreshes if expired
await auth.getPermissions() // ['os.batch.view', ...] — reads storage, no request
await auth.getMyProfile()   // your own full HR record
await auth.logout()         // revokes token + clears session

My profile

getMyProfile() returns the signed-in admin's own full HR record — designation, teams, branch, education, employment and personal details:

const profile = await auth.getMyProfile();

profile.name;         // 'Test Admin'
profile.designation;  // 'Engineer'
profile.teams;        // [{ id: 't1', name: 'Platform' }]
profile.joining_date; // '2024-03-01'

It takes no arguments and there's no way to ask for someone else's: the id comes from the session. The underlying endpoint does accept any admin's id, but this SDK is about the app's own signed-in user — reading other people's HR records belongs in an admin console, against the endpoint directly.

This is a different thing from getAdminMe(), which returns only the identity claims (sub, name, email, picture) and is what the SDK itself uses to validate a session.

Unset fields come back from the API as the literal string '(missing in get)' rather than being omitted. The SDK strips those, so an unset field reads as undefined and the sentinel never reaches your UI.

The declared fields cover the record as it stands, and anything the backend adds later still arrives on the object — typed as unknown until it's declared in the SDK:

const value = profile.some_new_field as string | undefined;

Permissions

Login fetches the admin's permission list and stores it — handleLoginSuccess() and handleTokenHandoff() both do this, so the API is called once, at login. The names come back on the session:

const session = await auth.handleLoginSuccess(response);
session.permissions;
// ['os.batch.view', 'os.batch.update', 'erp.promo.create', ...]

Read them again anywhere with getPermissions(), which serves the stored list without a request:

const permissions = await auth.getPermissions();

if (permissions.includes('os.batch.view')) {
  // ...
}

It re-fetches only when nothing is stored — i.e. the request during login failed. Pass force: true to refetch and re-store immediately after changing someone's access, or accessToken to read the permissions of some other token (a plain read-through that neither consults nor overwrites the stored list).

Revoked permissions are dropped rather than stored: the API returns them as INACTIVE instead of omitting them, and keeping them would make a plain includes() grant access that was explicitly taken away. So every name in the list is genuinely granted, and membership alone is a valid check.

Storage

The names are stored in localStorage under _tenms_admin_permissions, as a plain array:

["os.batch.view", "erp.promo.create"]

Unlike the session keys, this ignores the storage option and always uses localStorage.

Every login path rewrites it and logout() clears it, so it always belongs to the current session. It is not otherwise re-validated: a token refresh leaves it as-is, so a permission change lands at the next login (or a force: true call), not before.

A failed permissions request never fails the login — it's logged, and the session is still established with an empty list. So if you gate access on having permissions, check the list rather than relying on login throwing:

<LoginButton
  clientId={CLIENT_ID}
  onSuccess={async (response) => {
    const session = await auth.handleLoginSuccess(response);

    if (session.permissions.length === 0) {
      await auth.logout();
      refresh();
      alert('No permissions assigned to your account.');
      return;
    }

    refresh();
    window.location.href = '/dashboard';
  }}
/>

TenMSAuth options

| Option | Type | Default | Description | |--------|------|---------|-------------| | clientId | string | required | Your registered client ID | | storage | 'cookie' \| 'localStorage' | 'cookie' | Use localStorage for SPAs, cookie for SSR | | redirectUri | string | window.location.origin | Override only if needed | | scopes | string[] | ['profile', 'email', 'phone'] | OAuth scopes | | local | boolean | false | Point at the local environment instead of production | | debug | boolean | false | Logs every OAuth step to the console |

Local environment

Pass local: true to point the SDK at the local environment instead of production. Every request follows automatically — nothing else to configure.

export const auth = new TenMSAuth({
  clientId: CLIENT_ID,
  storage: 'localStorage',
  local: true,
});

Client IDs are not shared across environments — use the locally registered one when local is on, and make sure your redirectUri is registered there too. Pair it with debug: true and the init log line will print the resolved URLs.