Skip to main content

Auth Service

The authentication service handles user registration, login, and session management.

Endpoints

MethodPathDescription
POST/api/signupRegister new user
POST/api/loginAuthenticate user
POST/api/logoutInvalidate session
GET/api/meGet current user
GET/api/validateValidate token (internal)

Implementation

apps/backend-auth/src/index.ts
import { createDocsRouter, createServer, init, router, ok, err, TaggedError, type HonoBaseContext } from "@alt-stack/server-hono";
import { z } from "zod";

const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
});

const SessionSchema = z.object({
token: z.string(),
userId: z.string(),
expiresAt: z.string().datetime(),
});

// Error classes
class EmailExistsError extends TaggedError {
readonly _tag = "EmailExistsError" as const;
constructor(public readonly message: string = "Email already registered") {
super(message);
}
}

const EmailExistsErrorSchema = z.object({
_tag: z.literal("EmailExistsError"),
message: z.string(),
});

class InvalidCredentialsError extends TaggedError {
readonly _tag = "InvalidCredentialsError" as const;
constructor(public readonly message: string = "Invalid email or password") {
super(message);
}
}

const InvalidCredentialsErrorSchema = z.object({
_tag: z.literal("InvalidCredentialsError"),
message: z.string(),
});

const factory = init<HonoBaseContext>();
const publicProc = factory.procedure;

const authRouter = router<HonoBaseContext>({
"/signup": publicProc
.input({
body: z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(1),
}),
})
.output(z.object({ user: UserSchema, session: SessionSchema }))
.errors({
409: EmailExistsErrorSchema,
})
.post(({ input }) => {
const existing = users.find(u => u.email === input.body.email);
if (existing) {
return err(new EmailExistsError("Email already registered"));
}
// Create user and session...
return ok({ user: { id, email, name }, session: { token, userId, expiresAt } });
}),

"/login": publicProc
.input({
body: z.object({
email: z.string().email(),
password: z.string(),
}),
})
.output(z.object({ user: UserSchema, session: SessionSchema }))
.errors({
401: InvalidCredentialsErrorSchema,
})
.post(({ input }) => {
const user = users.find(u => u.email === input.body.email);
if (!user || !verifyPassword(input.body.password, user.passwordHash)) {
return err(new InvalidCredentialsError("Invalid email or password"));
}
// Create session...
return ok({ user: { id, email, name }, session: { token, userId, expiresAt } });
}),

// Internal endpoint for other services to validate tokens
"/validate": publicProc
.output(z.object({ valid: z.boolean(), userId: z.string().optional() }))
.get(({ ctx }) => {
const auth = ctx.hono.req.header("Authorization") ?? "";
const token = auth.replace("Bearer ", "");
const session = sessions.get(token);
if (!session || session.expiresAt < new Date()) {
return ok({ valid: false });
}
return ok({ valid: true, userId: session.userId });
}),
});

Token Validation Endpoint

The /validate endpoint is designed for service-to-service communication. Other services can call it to verify tokens without implementing token parsing themselves.

// In backend-logic
async function validateToken(token: string): Promise<string | null> {
const res = await ky.get(`${AUTH_SERVICE_URL}/api/validate`, {
headers: { authorization: token },
}).json<{ valid: boolean; userId?: string }>();

return res.valid ? (res.userId ?? null) : null;
}

Generating the OpenAPI Spec

apps/backend-auth/src/generate-spec.ts
import { writeFileSync } from "fs";
import { generateOpenAPISpec } from "@alt-stack/server-hono";
import { authRouter } from "./index.js";

const spec = generateOpenAPISpec({ api: authRouter }, {
title: "Auth API",
version: "1.0.0",
});

writeFileSync("openapi.json", JSON.stringify(spec, null, 2));
console.log("Generated openapi.json");

Run with:

pnpm --filter @real-life/backend-auth generate