Add initial library

This commit is contained in:
Jonathan Cooper 2026-02-06 18:23:33 -08:00
parent 7cecc1d042
commit e8a82796e0
6 changed files with 180 additions and 0 deletions

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "sf-auth-nextjs-middleware",
"version": "0.1.0",
"description": "Nextjs middleware and callback route for sf-auth",
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"files": [
"src"
],
"keywords": [
"nextjs",
"middleware",
"authentication",
"sf-auth"
],
"license": "MIT",
"peerDependencies": {
"next": ">=13.4.0"
}
}

87
src/callback.ts Normal file
View file

@ -0,0 +1,87 @@
import { NextResponse, type NextRequest } from "next/server";
import { AUTH_VALIDATE_URL, DEFAULT_COOKIE_NAMES } from "./constants";
import type { SfAuthCookieNames } from "./middleware";
export type SfAuthCallbackOptions = {
redirectTo: string;
cookieNames?: SfAuthCookieNames;
validateEndpoint?: string;
};
type ValidateResponse = {
valid: boolean;
user_id: string;
};
const resolveCookieNames = (cookieNames?: SfAuthCookieNames) => ({
userId: cookieNames?.userId ?? DEFAULT_COOKIE_NAMES.userId,
username: cookieNames?.username ?? DEFAULT_COOKIE_NAMES.username
});
const errorResponse = (message: string, status = 400) =>
new NextResponse(message, {
status,
headers: {
"content-type": "text/plain; charset=utf-8"
}
});
export const createSfAuthCallbackRoute = (
options: SfAuthCallbackOptions
) => {
const cookieNames = resolveCookieNames(options.cookieNames);
const validateUrl = options.validateEndpoint ?? AUTH_VALIDATE_URL;
return async (request: NextRequest) => {
const url = new URL(request.url);
const userId = url.searchParams.get("user_id");
const username = url.searchParams.get("username");
const key = url.searchParams.get("key");
if (!userId || !username || !key) {
return errorResponse("Missing required query parameters.");
}
let validateResponse: ValidateResponse;
try {
const response = await fetch(validateUrl, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({ user_id: userId, key })
});
if (!response.ok) {
return errorResponse("Failed to validate credentials.");
}
validateResponse = (await response.json()) as ValidateResponse;
} catch (error) {
return errorResponse("Unable to validate credentials.", 500);
}
if (!validateResponse.valid || validateResponse.user_id !== userId) {
return errorResponse("Invalid credentials.");
}
const redirectTarget = new URL(options.redirectTo, request.url);
const response = NextResponse.redirect(redirectTarget);
response.cookies.set(cookieNames.userId, userId, {
httpOnly: true,
sameSite: "lax",
path: "/"
});
response.cookies.set(cookieNames.username, username, {
httpOnly: true,
sameSite: "lax",
path: "/"
});
return response;
};
};

10
src/constants.ts Normal file
View file

@ -0,0 +1,10 @@
export const DEFAULT_COOKIE_NAMES = {
userId: "sf_user_id",
username: "sf_username"
} as const;
export const AUTH_REDIRECT_BASE_URL =
"https://snazzyfellas.com/api/redirect/authenticate";
export const AUTH_VALIDATE_URL =
"https://snazzyfellas.com/api/redirect/validate";

5
src/index.ts Normal file
View file

@ -0,0 +1,5 @@
export { createSfAuthMiddleware } from "./middleware";
export type { SfAuthMiddlewareOptions, SfAuthCookieNames } from "./middleware";
export { createSfAuthCallbackRoute } from "./callback";
export type { SfAuthCallbackOptions } from "./callback";

38
src/middleware.ts Normal file
View file

@ -0,0 +1,38 @@
import { NextResponse, type NextRequest } from "next/server";
import { AUTH_REDIRECT_BASE_URL, DEFAULT_COOKIE_NAMES } from "./constants";
export type SfAuthCookieNames = {
userId?: string;
username?: string;
};
export type SfAuthMiddlewareOptions = {
cookieNames?: SfAuthCookieNames;
};
const resolveCookieNames = (cookieNames?: SfAuthCookieNames) => ({
userId: cookieNames?.userId ?? DEFAULT_COOKIE_NAMES.userId,
username: cookieNames?.username ?? DEFAULT_COOKIE_NAMES.username
});
export const createSfAuthMiddleware = (
redirectUri: string,
options: SfAuthMiddlewareOptions = {}
) => {
const cookieNames = resolveCookieNames(options.cookieNames);
const redirectUrl = `${AUTH_REDIRECT_BASE_URL}?redirect_uri=${encodeURIComponent(
redirectUri
)}`;
return (request: NextRequest) => {
const userId = request.cookies.get(cookieNames.userId)?.value;
const username = request.cookies.get(cookieNames.username)?.value;
if (!userId || !username) {
return NextResponse.redirect(redirectUrl);
}
return NextResponse.next();
};
};

15
tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"declaration": true,
"declarationMap": true,
"noEmit": true,
"isolatedModules": true,
"skipLibCheck": true
},
"include": ["src"]
}