Release 0.12 (#143)

* changed tb imports

* cleanup: replace console.log/warn with $console, remove commented-out code

Removed various commented-out code and replaced direct `console.log` and `console.warn` usage across the codebase with `$console` from "core" for standardized logging. Also adjusted linting rules in biome.json to enable warnings for `console.log` usage.

* ts: enable incremental

* fix imports in test files

reorganize imports to use "@sinclair/typebox" directly, replacing local utility references, and add missing "override" keywords in test classes.

* added media permissions (#142)

* added permissions support for media module

introduced `MediaPermissions` for fine-grained access control in the media module, updated routes to enforce these permissions, and adjusted permission registration logic.

* fix: handle token absence in getUploadHeaders and add tests for transport modes

ensure getUploadHeaders does not set Authorization header when token is missing. Add unit tests to validate behavior for different token_transport options.

* remove console.log on DropzoneContainer.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add bcrypt and refactored auth resolve (#147)

* reworked auth architecture with improved password handling and claims

Refactored password strategy to prepare supporting bcrypt, improving hashing/encryption flexibility. Updated authentication flow with enhanced user resolution mechanisms, safe JWT generation, and consistent profile handling. Adjusted dependencies to include bcryptjs and updated lock files accordingly.

* fix strategy forms handling, add register route and hidden fields

Refactored strategy forms to include hidden fields for type and name. Added a registration route with necessary adjustments to the admin controller and routes. Corrected field handling within relevant forms and components.

* refactored auth handling to support bcrypt, extracted user pool

* update email regex to allow '+' and '_' characters

* update test stub password for AppAuth spec

* update data exceptions to use HttpStatus constants, adjust logging level in AppUserPool

* rework strategies to extend a base class instead of interface

* added simple bcrypt test

* add validation logs and improve data validation handling (#157)

Added warning logs for invalid data during mutator validation, refined field validation logic to handle undefined values, and adjusted event validation comments for clarity. Minor improvements include exporting events from core and handling optional chaining in entity field validation.

* modify MediaApi to support custom fetch implementation, defaults to native fetch (#158)

* modify MediaApi to support custom fetch implementation, defaults to native fetch

added an optional `fetcher` parameter to allow usage of a custom fetch function in both `upload` and `fetcher` methods. Defaults to the standard `fetch` if none is provided.

* fix tests and improve api fetcher types

* update admin basepath handling and window context integration (#155)

Refactored `useBkndWindowContext` to include `admin_basepath` and updated its usage in routing. Improved type consistency with `AdminBkndWindowContext` and ensured default values are applied for window context.

* trigger `repository-find-[one|many]-[before|after]` based on `limit` (#160)

* refactor error handling in authenticator and password strategy (#161)

made `respondWithError` method public, updated login and register routes in `PasswordStrategy` to handle errors using `respondWithError` for consistency.

* add disableSubmitOnError prop to NativeForm and export getFlashMessage (#162)

Introduced a `disableSubmitOnError` prop to NativeForm to control submit button behavior when errors are present. Also exported `getFlashMessage` from the core for external usage.

* update dependencies in package.json (#156)

moved several dependencies between devDependencies and dependencies for better categorization and removed redundant entries.

* update imports to adjust nodeTestRunner path and remove unused export (#163)

updated imports in test files to reflect the correct path for nodeTestRunner. removed redundant export of nodeTestRunner from index file to clean up module structure. In some environments this could cause issues requiring to exclude `node:test`, just removing it for now.

* fix sync events not awaited (#164)

* refactor(dropzone): extract DropzoneInner and unify state management with zustand (#165)

Simplified Dropzone implementation by extracting inner logic to a new component, `DropzoneInner`. Replaced local dropzone state logic with centralized state management using zustand. Adjusted API exports and props accordingly for consistency and maintainability.

* replace LiquidJs rendering with simplified renderer (#167)

* replace LiquidJs rendering with simplified renderer

Removed dependency on LiquidJS and replaced it with a custom templating solution using lodash `get`. Updated corresponding components, editors, and tests to align with the new rendering approach. Removed unused filters and tags.

* remove liquid js from package json

* feat/cli-generate-types (#166)

* init types generation

* update type generation for entities and fields

Refactored `EntityTypescript` to support improved field types and relations. Added `toType` method overrides for various fields to define accurate TypeScript types. Enhanced CLI `types` command with new options for output style and file handling. Removed redundant test files.

* update type generation code and CLI option description

removed unused imports definition, adjusted formatting in EntityTypescript, and clarified the CLI style option description.

* fix json schema field type generation

* reworked system entities to prevent recursive types

* reworked system entities to prevent recursive types

* remove unused object function

* types: use number instead of Generated

* update data hooks and api types

* update data hooks and api types

* update data hooks and api types

* update data hooks and api types

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
dswbx
2025-05-01 10:12:18 +02:00
committed by GitHub
parent d6f94a2ce1
commit 372f94d22a
186 changed files with 2617 additions and 1997 deletions

View File

@@ -1,29 +1,23 @@
import {
type AuthAction,
AuthPermissions,
Authenticator,
type ProfileExchange,
Role,
type Strategy,
} from "auth";
import { Authenticator, AuthPermissions, Role, type Strategy } from "auth";
import type { PasswordStrategy } from "auth/authenticate/strategies";
import { $console, type DB, Exception, type PrimaryFieldType } from "core";
import { type Static, secureRandomString, transformObject } from "core/utils";
import { $console, type DB } from "core";
import { secureRandomString, transformObject } from "core/utils";
import type { Entity, EntityManager } from "data";
import { type FieldSchema, em, entity, enumm, text } from "data/prototype";
import { pick } from "lodash-es";
import { em, entity, enumm, type FieldSchema, text } from "data/prototype";
import { Module } from "modules/Module";
import { AuthController } from "./api/AuthController";
import { type AppAuthSchema, STRATEGIES, authConfigSchema } from "./auth-schema";
import { type AppAuthSchema, authConfigSchema, STRATEGIES } from "./auth-schema";
import { AppUserPool } from "auth/AppUserPool";
import type { AppEntity } from "core/config";
export type UserFieldSchema = FieldSchema<typeof AppAuth.usersFields>;
declare module "core" {
interface Users extends AppEntity, UserFieldSchema {}
interface DB {
users: { id: PrimaryFieldType } & UserFieldSchema;
users: Users;
}
}
type AuthSchema = Static<typeof authConfigSchema>;
export type CreateUserPayload = { email: string; password: string; [key: string]: any };
export class AppAuth extends Module<typeof authConfigSchema> {
@@ -31,12 +25,12 @@ export class AppAuth extends Module<typeof authConfigSchema> {
cache: Record<string, any> = {};
_controller!: AuthController;
override async onBeforeUpdate(from: AuthSchema, to: AuthSchema) {
override async onBeforeUpdate(from: AppAuthSchema, to: AppAuthSchema) {
const defaultSecret = authConfigSchema.properties.jwt.properties.secret.default;
if (!from.enabled && to.enabled) {
if (to.jwt.secret === defaultSecret) {
console.warn("No JWT secret provided, generating a random one");
$console.warn("No JWT secret provided, generating a random one");
to.jwt.secret = secureRandomString(64);
}
}
@@ -80,7 +74,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
}
});
this._authenticator = new Authenticator(strategies, this.resolveUser.bind(this), {
this._authenticator = new Authenticator(strategies, new AppUserPool(this), {
jwt: this.config.jwt,
cookie: this.config.cookie,
});
@@ -90,7 +84,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
this._controller = new AuthController(this);
this.ctx.server.route(this.config.basepath, this._controller.getController());
this.ctx.guard.registerPermissions(Object.values(AuthPermissions));
this.ctx.guard.registerPermissions(AuthPermissions);
}
isStrategyEnabled(strategy: Strategy | string) {
@@ -122,120 +116,6 @@ export class AppAuth extends Module<typeof authConfigSchema> {
return this.ctx.em as any;
}
private async resolveUser(
action: AuthAction,
strategy: Strategy,
identifier: string,
profile: ProfileExchange,
): Promise<any> {
if (!this.config.allow_register && action === "register") {
throw new Exception("Registration is not allowed", 403);
}
const fields = this.getUsersEntity()
.getFillableFields("create")
.map((f) => f.name);
const filteredProfile = Object.fromEntries(
Object.entries(profile).filter(([key]) => fields.includes(key)),
);
switch (action) {
case "login":
return this.login(strategy, identifier, filteredProfile);
case "register":
return this.register(strategy, identifier, filteredProfile);
}
}
private filterUserData(user: any) {
return pick(user, this.config.jwt.fields);
}
private async login(strategy: Strategy, identifier: string, profile: ProfileExchange) {
if (!("email" in profile)) {
throw new Exception("Profile must have email");
}
if (typeof identifier !== "string" || identifier.length === 0) {
throw new Exception("Identifier must be a string");
}
const users = this.getUsersEntity();
this.toggleStrategyValueVisibility(true);
const result = await this.em
.repo(users as unknown as "users")
.findOne({ email: profile.email! });
this.toggleStrategyValueVisibility(false);
if (!result.data) {
throw new Exception("User not found", 404);
}
// compare strategy and identifier
if (result.data.strategy !== strategy.getName()) {
//console.log("!!! User registered with different strategy");
throw new Exception("User registered with different strategy");
}
if (result.data.strategy_value !== identifier) {
throw new Exception("Invalid credentials");
}
return this.filterUserData(result.data);
}
private async register(strategy: Strategy, identifier: string, profile: ProfileExchange) {
if (!("email" in profile)) {
throw new Exception("Profile must have an email");
}
if (typeof identifier !== "string" || identifier.length === 0) {
throw new Exception("Identifier must be a string");
}
const users = this.getUsersEntity();
const { data } = await this.em.repo(users).findOne({ email: profile.email! });
if (data) {
throw new Exception("User already exists");
}
const payload: any = {
...profile,
strategy: strategy.getName(),
strategy_value: identifier,
};
const mutator = this.em.mutator(users);
mutator.__unstable_toggleSystemEntityCreation(false);
this.toggleStrategyValueVisibility(true);
const createResult = await mutator.insertOne(payload);
mutator.__unstable_toggleSystemEntityCreation(true);
this.toggleStrategyValueVisibility(false);
if (!createResult.data) {
throw new Error("Could not create user");
}
return this.filterUserData(createResult.data);
}
private toggleStrategyValueVisibility(visible: boolean) {
const toggle = (name: string, visible: boolean) => {
const field = this.getUsersEntity().field(name)!;
if (visible) {
field.config.hidden = false;
field.config.fillable = true;
} else {
// reset to normal
const template = AppAuth.usersFields.strategy_value.config;
field.config.hidden = template.hidden;
field.config.fillable = template.fillable;
}
};
toggle("strategy_value", visible);
toggle("strategy", visible);
// @todo: think about a PasswordField that automatically hashes on save?
}
getUsersEntity(forceCreate?: boolean): Entity<"users", typeof AppAuth.usersFields> {
const entity_name = this.config.entity_name;
if (forceCreate || !this.em.hasEntity(entity_name)) {
@@ -288,7 +168,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
throw new Error("Cannot create user, auth not enabled");
}
const strategy = "password";
const strategy = "password" as const;
const pw = this.authenticator.strategy(strategy) as PasswordStrategy;
const strategy_value = await pw.hash(password);
const mutator = this.em.mutator(this.config.entity_name as "users");
@@ -315,8 +195,7 @@ export class AppAuth extends Module<typeof authConfigSchema> {
...this.authenticator.toJSON(secrets),
strategies: transformObject(strategies, (strategy) => ({
enabled: this.isStrategyEnabled(strategy),
type: strategy.getType(),
config: strategy.toJSON(secrets),
...strategy.toJSON(secrets),
})),
};
}

View File

@@ -0,0 +1,83 @@
import { AppAuth } from "auth/AppAuth";
import type { CreateUser, SafeUser, User, UserPool } from "auth/authenticate/Authenticator";
import { $console } from "core";
import { pick } from "lodash-es";
import {
InvalidConditionsException,
UnableToCreateUserException,
UserNotFoundException,
} from "auth/errors";
export class AppUserPool implements UserPool {
constructor(private appAuth: AppAuth) {}
get em() {
return this.appAuth.em;
}
get users() {
return this.appAuth.getUsersEntity();
}
async findBy(strategy: string, prop: keyof SafeUser, value: any) {
$console.debug("[AppUserPool:findBy]", { strategy, prop, value });
this.toggleStrategyValueVisibility(true);
const result = await this.em.repo(this.users).findOne({ [prop]: value, strategy });
this.toggleStrategyValueVisibility(false);
if (!result.data) {
$console.debug("[AppUserPool]: User not found");
throw new UserNotFoundException();
}
return result.data;
}
async create(strategy: string, payload: CreateUser & Partial<Omit<User, "id">>) {
$console.debug("[AppUserPool:create]", { strategy, payload });
if (!("strategy_value" in payload)) {
throw new InvalidConditionsException("Profile must have a strategy_value value");
}
const fields = this.users.getSelect(undefined, "create");
const safeProfile = pick(payload, fields) as any;
const createPayload: Omit<User, "id"> = {
...safeProfile,
strategy,
};
const mutator = this.em.mutator(this.users);
mutator.__unstable_toggleSystemEntityCreation(false);
this.toggleStrategyValueVisibility(true);
const createResult = await mutator.insertOne(createPayload);
mutator.__unstable_toggleSystemEntityCreation(true);
this.toggleStrategyValueVisibility(false);
if (!createResult.data) {
throw new UnableToCreateUserException();
}
$console.debug("[AppUserPool]: User created", createResult.data);
return createResult.data;
}
private toggleStrategyValueVisibility(visible: boolean) {
const toggle = (name: string, visible: boolean) => {
const field = this.users.field(name)!;
if (visible) {
field.config.hidden = false;
field.config.fillable = true;
} else {
// reset to normal
const template = AppAuth.usersFields.strategy_value.config;
field.config.hidden = template.hidden;
field.config.fillable = template.fillable;
}
};
toggle("strategy_value", visible);
toggle("strategy", visible);
// @todo: think about a PasswordField that automatically hashes on save?
}
}

View File

@@ -1,9 +1,11 @@
import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth";
import { tbValidator as tb } from "core";
import { Type, TypeInvalidError, parse, transformObject } from "core/utils";
import { TypeInvalidError, parse, transformObject } from "core/utils";
import { DataPermissions } from "data";
import type { Hono } from "hono";
import { Controller, type ServerEnv } from "modules/Controller";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
export type AuthActionResponse = {
success: boolean;

View File

@@ -1,6 +1,8 @@
import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator";
import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies";
import { type Static, StringRecord, Type, objectTransform } from "core/utils";
import { type Static, StringRecord, objectTransform } from "core/utils";
import * as tbbox from "@sinclair/typebox";
const { Type } = tbbox;
export const Strategies = {
password: {

View File

@@ -1,13 +1,12 @@
import { type DB, Exception, type PrimaryFieldType } from "core";
import { $console, type DB, Exception } from "core";
import { addFlashMessage } from "core/server/flash";
import {
type Static,
StringEnum,
type TObject,
Type,
parse,
runtimeSupports,
transformObject,
truncate,
} from "core/utils";
import type { Context, Hono } from "hono";
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
@@ -15,6 +14,9 @@ import { sign, verify } from "hono/jwt";
import type { CookieOptions } from "hono/utils/cookie";
import type { ServerEnv } from "modules/Controller";
import { pick } from "lodash-es";
import * as tbbox from "@sinclair/typebox";
import { InvalidConditionsException } from "auth/errors";
const { Type } = tbbox;
type Input = any; // workaround
export type JWTPayload = Parameters<typeof sign>[0];
@@ -23,11 +25,12 @@ export const strategyActions = ["create", "change"] as const;
export type StrategyActionName = (typeof strategyActions)[number];
export type StrategyAction<S extends TObject = TObject> = {
schema: S;
preprocess: (input: unknown) => Promise<Omit<DB["users"], "id" | "strategy">>;
preprocess: (input: Static<S>) => Promise<Omit<DB["users"], "id" | "strategy">>;
};
export type StrategyActions = Partial<Record<StrategyActionName, StrategyAction>>;
// @todo: add schema to interface to ensure proper inference
// @todo: add tests (e.g. invalid strategy_value)
export interface Strategy {
getController: (auth: Authenticator) => Hono<any>;
getType: () => string;
@@ -37,28 +40,22 @@ export interface Strategy {
getActions?: () => StrategyActions;
}
export type User = {
id: PrimaryFieldType;
email: string;
password: string;
role?: string | null;
};
export type User = DB["users"];
export type ProfileExchange = {
email?: string;
username?: string;
sub?: string;
password?: string;
strategy?: string;
strategy_value?: string;
[key: string]: any;
};
export type SafeUser = Omit<User, "password">;
export type SafeUser = Omit<User, "strategy_value">;
export type CreateUser = Pick<User, "email"> & { [key: string]: any };
export type AuthResponse = { user: SafeUser; token: string };
export interface UserPool<Fields = "id" | "email" | "username"> {
findBy: (prop: Fields, value: string | number) => Promise<User | undefined>;
create: (user: CreateUser) => Promise<User | undefined>;
export interface UserPool {
findBy: (strategy: string, prop: keyof SafeUser, value: string | number) => Promise<User>;
create: (strategy: string, user: CreateUser) => Promise<User>;
}
const defaultCookieExpires = 60 * 60 * 24 * 7; // 1 week in seconds
@@ -100,12 +97,17 @@ export const authenticatorConfig = Type.Object({
type AuthConfig = Static<typeof authenticatorConfig>;
export type AuthAction = "login" | "register";
export type AuthResolveOptions = {
identifier?: "email" | string;
redirect?: string;
forceJsonResponse?: boolean;
};
export type AuthUserResolver = (
action: AuthAction,
strategy: Strategy,
identifier: string,
profile: ProfileExchange,
) => Promise<SafeUser | undefined>;
opts?: AuthResolveOptions,
) => Promise<ProfileExchange | undefined>;
type AuthClaims = SafeUser & {
iat: number;
iss?: string;
@@ -113,33 +115,117 @@ type AuthClaims = SafeUser & {
};
export class Authenticator<Strategies extends Record<string, Strategy> = Record<string, Strategy>> {
private readonly strategies: Strategies;
private readonly config: AuthConfig;
private readonly userResolver: AuthUserResolver;
constructor(strategies: Strategies, userResolver?: AuthUserResolver, config?: AuthConfig) {
this.userResolver = userResolver ?? (async (a, s, i, p) => p as any);
this.strategies = strategies as Strategies;
constructor(
private readonly strategies: Strategies,
private readonly userPool: UserPool,
config?: AuthConfig,
) {
this.config = parse(authenticatorConfig, config ?? {});
}
async resolve(
action: AuthAction,
async resolveLogin(
c: Context,
strategy: Strategy,
identifier: string,
profile: ProfileExchange,
): Promise<AuthResponse> {
//console.log("resolve", { action, strategy: strategy.getName(), profile });
const user = await this.userResolver(action, strategy, identifier, profile);
profile: Partial<SafeUser>,
verify: (user: User) => Promise<void>,
opts?: AuthResolveOptions,
) {
try {
// @todo: centralize identifier and checks
// @todo: check identifier value (if allowed)
const identifier = opts?.identifier || "email";
if (typeof identifier !== "string" || identifier.length === 0) {
throw new InvalidConditionsException("Identifier must be a string");
}
if (!(identifier in profile)) {
throw new InvalidConditionsException(`Profile must have identifier "${identifier}"`);
}
if (user) {
return {
user,
token: await this.jwt(user),
};
const user = await this.userPool.findBy(
strategy.getName(),
identifier as any,
profile[identifier],
);
if (!user.strategy_value) {
throw new InvalidConditionsException("User must have a strategy value");
} else if (user.strategy !== strategy.getName()) {
throw new InvalidConditionsException("User signed up with a different strategy");
}
await verify(user);
const data = await this.safeAuthResponse(user);
return this.respondWithUser(c, data, opts);
} catch (e) {
return this.respondWithError(c, e as Error, opts);
}
}
async resolveRegister(
c: Context,
strategy: Strategy,
profile: CreateUser,
verify: (user: User) => Promise<void>,
opts?: AuthResolveOptions,
) {
try {
const identifier = opts?.identifier || "email";
if (typeof identifier !== "string" || identifier.length === 0) {
throw new InvalidConditionsException("Identifier must be a string");
}
if (!(identifier in profile)) {
throw new InvalidConditionsException(`Profile must have identifier "${identifier}"`);
}
if (!("strategy_value" in profile)) {
throw new InvalidConditionsException("Profile must have a strategy value");
}
const user = await this.userPool.create(strategy.getName(), {
...profile,
strategy_value: profile.strategy_value,
});
await verify(user);
const data = await this.safeAuthResponse(user);
return this.respondWithUser(c, data, opts);
} catch (e) {
return this.respondWithError(c, e as Error, opts);
}
}
private async respondWithUser(c: Context, data: AuthResponse, opts?: AuthResolveOptions) {
const successUrl = this.getSafeUrl(
c,
opts?.redirect ?? this.config.cookie.pathSuccess ?? "/",
);
if ("token" in data) {
await this.setAuthCookie(c, data.token);
if (this.isJsonRequest(c) || opts?.forceJsonResponse) {
return c.json(data);
}
// can't navigate to "/" doesn't work on nextjs
return c.redirect(successUrl);
}
throw new Error("User could not be resolved");
throw new Exception("Invalid response");
}
async respondWithError(c: Context, error: Error, opts?: AuthResolveOptions) {
$console.error("respondWithError", error);
if (this.isJsonRequest(c) || opts?.forceJsonResponse) {
// let the server handle it
throw error;
}
await addFlashMessage(c, String(error), "error");
const referer = this.getSafeUrl(c, opts?.redirect ?? c.req.header("Referer") ?? "/");
return c.redirect(referer);
}
getStrategies(): Strategies {
@@ -158,7 +244,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
}
// @todo: add jwt tests
async jwt(_user: Omit<User, "password">): Promise<string> {
async jwt(_user: SafeUser | ProfileExchange): Promise<string> {
const user = pick(_user, this.config.jwt.fields);
const payload: JWTPayload = {
@@ -184,6 +270,14 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
return sign(payload, secret, this.config.jwt?.alg ?? "HS256");
}
async safeAuthResponse(_user: User): Promise<AuthResponse> {
const user = pick(_user, this.config.jwt.fields) as SafeUser;
return {
user,
token: await this.jwt(user),
};
}
async verify(jwt: string): Promise<AuthClaims | undefined> {
try {
const payload = await verify(
@@ -225,7 +319,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
return token;
} catch (e: any) {
if (e instanceof Error) {
console.error("[Error:getAuthCookie]", e.message);
$console.error("[getAuthCookie]", e.message);
}
return undefined;
@@ -242,11 +336,13 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
}
private async setAuthCookie(c: Context<ServerEnv>, token: string) {
$console.debug("setting auth cookie", truncate(token));
const secret = this.config.jwt.secret;
await setSignedCookie(c, "auth", token, secret, this.cookieOptions);
}
private async deleteAuthCookie(c: Context) {
$console.debug("deleting auth cookie");
await deleteCookie(c, "auth", this.cookieOptions);
}
@@ -262,7 +358,6 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
// @todo: move this to a server helper
isJsonRequest(c: Context): boolean {
//return c.req.header("Content-Type") === "application/x-www-form-urlencoded";
return c.req.header("Content-Type") === "application/json";
}
@@ -286,37 +381,6 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
return p;
}
async respond(c: Context, data: AuthResponse | Error | any, redirect?: string) {
const successUrl = this.getSafeUrl(c, redirect ?? this.config.cookie.pathSuccess ?? "/");
const referer = redirect ?? c.req.header("Referer") ?? successUrl;
//console.log("auth respond", { redirect, successUrl, successPath });
if ("token" in data) {
await this.setAuthCookie(c, data.token);
if (this.isJsonRequest(c)) {
return c.json(data);
}
// can't navigate to "/" doesn't work on nextjs
//console.log("auth success, redirecting to", successUrl);
return c.redirect(successUrl);
}
if (this.isJsonRequest(c)) {
return c.json(data, 400);
}
let message = "An error occured";
if (data instanceof Exception) {
message = data.message;
}
await addFlashMessage(c, message, "error");
//console.log("auth failed, redirecting to", referer);
return c.redirect(referer);
}
// @todo: don't extract user from token, but from the database or cache
async resolveAuthFromRequest(c: Context): Promise<SafeUser | undefined> {
let token: string | undefined;
@@ -341,13 +405,3 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
};
}
}
export function createStrategyAction<S extends TObject>(
schema: S,
preprocess: (input: Static<S>) => Promise<Partial<DB["users"]>>,
) {
return {
schema,
preprocess,
} as StrategyAction<S>;
}

View File

@@ -1,152 +1,135 @@
import type { Authenticator, Strategy } from "auth";
import { isDebug, tbValidator as tb } from "core";
import { type Static, StringEnum, Type, parse } from "core/utils";
import { hash } from "core/utils";
import { type Context, Hono } from "hono";
import { type StrategyAction, type StrategyActions, createStrategyAction } from "../Authenticator";
import { type Authenticator, InvalidCredentialsException, type User } from "auth";
import { $console, tbValidator as tb } from "core";
import { hash, parse, type Static, StrictObject, StringEnum } from "core/utils";
import { Hono } from "hono";
import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs";
import * as tbbox from "@sinclair/typebox";
import { Strategy } from "./Strategy";
type LoginSchema = { username: string; password: string } | { email: string; password: string };
type RegisterSchema = { email: string; password: string; [key: string]: any };
const { Type } = tbbox;
const schema = Type.Object({
hashing: StringEnum(["plain", "sha256" /*, "bcrypt"*/] as const, { default: "sha256" }),
const schema = StrictObject({
hashing: StringEnum(["plain", "sha256", "bcrypt"], { default: "sha256" }),
rounds: Type.Optional(Type.Number({ minimum: 1, maximum: 10 })),
});
export type PasswordStrategyOptions = Static<typeof schema>;
/*export type PasswordStrategyOptions2 = {
hashing?: "plain" | "bcrypt" | "sha256";
};*/
export class PasswordStrategy implements Strategy {
private options: PasswordStrategyOptions;
export class PasswordStrategy extends Strategy<typeof schema> {
constructor(config: Partial<PasswordStrategyOptions> = {}) {
super(config as any, "password", "password", "form");
constructor(options: Partial<PasswordStrategyOptions> = {}) {
this.options = parse(schema, options);
}
async hash(password: string) {
switch (this.options.hashing) {
case "sha256":
return hash.sha256(password);
default:
return password;
}
}
async login(input: LoginSchema) {
if (!("email" in input) || !("password" in input)) {
throw new Error("Invalid input: Email and password must be provided");
}
const hashedPassword = await this.hash(input.password);
return { ...input, password: hashedPassword };
}
async register(input: RegisterSchema) {
if (!input.email || !input.password) {
throw new Error("Invalid input: Email and password must be provided");
}
return {
...input,
password: await this.hash(input.password),
};
}
getController(authenticator: Authenticator): Hono<any> {
const hono = new Hono();
return hono
.post(
"/login",
tb(
"query",
Type.Object({
redirect: Type.Optional(Type.String()),
}),
),
async (c) => {
const body = await authenticator.getBody(c);
const { redirect } = c.req.valid("query");
try {
const payload = await this.login(body);
const data = await authenticator.resolve(
"login",
this,
payload.password,
payload,
);
return await authenticator.respond(c, data, redirect);
} catch (e) {
return await authenticator.respond(c, e);
}
},
)
.post(
"/register",
tb(
"query",
Type.Object({
redirect: Type.Optional(Type.String()),
}),
),
async (c) => {
const body = await authenticator.getBody(c);
const { redirect } = c.req.valid("query");
const payload = await this.register(body);
const data = await authenticator.resolve(
"register",
this,
payload.password,
payload,
);
return await authenticator.respond(c, data, redirect);
},
);
}
getActions(): StrategyActions {
return {
create: createStrategyAction(
Type.Object({
email: Type.String({
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
}),
password: Type.String({
minLength: 8, // @todo: this should be configurable
}),
}),
async ({ password, ...input }) => {
return {
...input,
strategy_value: await this.hash(password),
};
},
),
};
this.registerAction("create", this.getPayloadSchema(), async ({ password, ...input }) => {
return {
...input,
strategy_value: await this.hash(password),
};
});
}
getSchema() {
return schema;
}
getType() {
return "password";
private getPayloadSchema() {
return Type.Object({
email: Type.String({
pattern: "^[\\w-\\.\\+_]+@([\\w-]+\\.)+[\\w-]{2,4}$",
}),
password: Type.String({
minLength: 8, // @todo: this should be configurable
}),
});
}
getMode() {
return "form" as const;
async hash(password: string) {
switch (this.config.hashing) {
case "sha256":
return hash.sha256(password);
case "bcrypt": {
const salt = await bcryptGenSalt(this.config.rounds ?? 4);
return bcryptHash(password, salt);
}
default:
return password;
}
}
getName() {
return "password" as const;
async compare(actual: string, compare: string): Promise<boolean> {
switch (this.config.hashing) {
case "sha256": {
const compareHashed = await this.hash(compare);
return actual === compareHashed;
}
case "bcrypt":
return await bcryptCompare(compare, actual);
}
return false;
}
toJSON(secrets?: boolean) {
return secrets ? this.options : undefined;
verify(password: string) {
return async (user: User) => {
const compare = await this.compare(user?.strategy_value!, password);
if (compare !== true) {
throw new InvalidCredentialsException();
}
};
}
getController(authenticator: Authenticator): Hono<any> {
const hono = new Hono();
const redirectQuerySchema = Type.Object({
redirect: Type.Optional(Type.String()),
});
const payloadSchema = this.getPayloadSchema();
hono.post("/login", tb("query", redirectQuerySchema), async (c) => {
try {
const body = parse(payloadSchema, await authenticator.getBody(c), {
onError: (errors) => {
$console.error("Invalid login payload", [...errors]);
throw new InvalidCredentialsException();
},
});
const { redirect } = c.req.valid("query");
return await authenticator.resolveLogin(c, this, body, this.verify(body.password), {
redirect,
});
} catch (e) {
return authenticator.respondWithError(c, e as any);
}
});
hono.post("/register", tb("query", redirectQuerySchema), async (c) => {
try {
const { redirect } = c.req.valid("query");
const { password, email, ...body } = parse(
payloadSchema,
await authenticator.getBody(c),
{
onError: (errors) => {
$console.error("Invalid register payload", [...errors]);
new InvalidCredentialsException();
},
},
);
const profile = {
...body,
email,
strategy_value: await this.hash(password),
};
return await authenticator.resolveRegister(c, this, profile, async () => void 0, {
redirect,
});
} catch (e) {
return authenticator.respondWithError(c, e as any);
}
});
return hono;
}
}

View File

@@ -0,0 +1,63 @@
import type {
Authenticator,
StrategyAction,
StrategyActionName,
StrategyActions,
} from "../Authenticator";
import type { Hono } from "hono";
import type { Static, TSchema } from "@sinclair/typebox";
import { parse, type TObject } from "core/utils";
export type StrategyMode = "form" | "external";
export abstract class Strategy<Schema extends TSchema = TSchema> {
protected actions: StrategyActions = {};
constructor(
protected config: Static<Schema>,
public type: string,
public name: string,
public mode: StrategyMode,
) {
// don't worry about typing, it'll throw if invalid
this.config = parse(this.getSchema(), (config ?? {}) as any) as Static<Schema>;
}
protected registerAction<S extends TObject = TObject>(
name: StrategyActionName,
schema: S,
preprocess: StrategyAction<S>["preprocess"],
): void {
this.actions[name] = {
schema,
preprocess,
} as const;
}
protected abstract getSchema(): Schema;
abstract getController(auth: Authenticator): Hono;
getType(): string {
return this.type;
}
getMode() {
return this.mode;
}
getName(): string {
return this.name;
}
toJSON(secrets?: boolean): { type: string; config: Static<Schema> | {} | undefined } {
return {
type: this.getType(),
config: secrets ? this.config : undefined,
};
}
getActions(): StrategyActions {
return this.actions;
}
}

View File

@@ -5,8 +5,8 @@ import { OAuthCallbackException, OAuthStrategy } from "./oauth/OAuthStrategy";
export * as issuers from "./oauth/issuers";
export {
PasswordStrategy,
type PasswordStrategyOptions,
PasswordStrategy,
OAuthStrategy,
OAuthCallbackException,
CustomOAuthStrategy,

View File

@@ -1,43 +1,35 @@
import { type Static, StringEnum, Type } from "core/utils";
import { type Static, StrictObject, StringEnum } from "core/utils";
import * as tbbox from "@sinclair/typebox";
import type * as oauth from "oauth4webapi";
import { OAuthStrategy } from "./OAuthStrategy";
const { Type } = tbbox;
type SupportedTypes = "oauth2" | "oidc";
type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;
const UrlString = Type.String({ pattern: "^(https?|wss?)://[^\\s/$.?#].[^\\s]*$" });
const oauthSchemaCustom = Type.Object(
const oauthSchemaCustom = StrictObject(
{
type: StringEnum(["oidc", "oauth2"] as const, { default: "oidc" }),
name: Type.String(),
client: Type.Object(
{
client_id: Type.String(),
client_secret: Type.String(),
token_endpoint_auth_method: StringEnum(["client_secret_basic"]),
},
{
additionalProperties: false,
},
),
as: Type.Object(
{
issuer: Type.String(),
code_challenge_methods_supported: Type.Optional(StringEnum(["S256"])),
scopes_supported: Type.Optional(Type.Array(Type.String())),
scope_separator: Type.Optional(Type.String({ default: " " })),
authorization_endpoint: Type.Optional(UrlString),
token_endpoint: Type.Optional(UrlString),
userinfo_endpoint: Type.Optional(UrlString),
},
{
additionalProperties: false,
},
),
client: StrictObject({
client_id: Type.String(),
client_secret: Type.String(),
token_endpoint_auth_method: StringEnum(["client_secret_basic"]),
}),
as: StrictObject({
issuer: Type.String(),
code_challenge_methods_supported: Type.Optional(StringEnum(["S256"])),
scopes_supported: Type.Optional(Type.Array(Type.String())),
scope_separator: Type.Optional(Type.String({ default: " " })),
authorization_endpoint: Type.Optional(UrlString),
token_endpoint: Type.Optional(UrlString),
userinfo_endpoint: Type.Optional(UrlString),
}),
// @todo: profile mapping
},
{ title: "Custom OAuth", additionalProperties: false },
{ title: "Custom OAuth" },
);
type OAuthConfigCustom = Static<typeof oauthSchemaCustom>;
@@ -62,6 +54,11 @@ export type IssuerConfig<UserInfo = any> = {
};
export class CustomOAuthStrategy extends OAuthStrategy {
constructor(config: OAuthConfigCustom) {
super(config as any);
this.type = "custom_oauth";
}
override getIssuerConfig(): IssuerConfig {
return { ...this.config, profile: async (info) => info } as any;
}
@@ -70,8 +67,4 @@ export class CustomOAuthStrategy extends OAuthStrategy {
override getSchema() {
return oauthSchemaCustom;
}
override getType() {
return "custom_oauth";
}
}

View File

@@ -1,10 +1,13 @@
import type { AuthAction, Authenticator, Strategy } from "auth";
import type { AuthAction, Authenticator } from "auth";
import { Exception, isDebug } from "core";
import { type Static, StringEnum, type TSchema, Type, filterKeys, parse } from "core/utils";
import { type Static, StringEnum, filterKeys, StrictObject } from "core/utils";
import { type Context, Hono } from "hono";
import { getSignedCookie, setSignedCookie } from "hono/cookie";
import * as oauth from "oauth4webapi";
import * as issuers from "./issuers";
import * as tbbox from "@sinclair/typebox";
import { Strategy } from "auth/authenticate/strategies/Strategy";
const { Type } = tbbox;
type ConfiguredIssuers = keyof typeof issuers;
type SupportedTypes = "oauth2" | "oidc";
@@ -13,17 +16,12 @@ type RequireKeys<T extends object, K extends keyof T> = Required<Pick<T, K>> & O
const schemaProvided = Type.Object(
{
//type: StringEnum(["oidc", "oauth2"] as const, { default: "oidc" }),
name: StringEnum(Object.keys(issuers) as ConfiguredIssuers[]),
client: Type.Object(
{
client_id: Type.String(),
client_secret: Type.String(),
},
{
additionalProperties: false,
},
),
type: StringEnum(["oidc", "oauth2"] as const, { default: "oauth2" }),
client: StrictObject({
client_id: Type.String(),
client_secret: Type.String(),
}),
},
{ title: "OAuth" },
);
@@ -71,11 +69,13 @@ export class OAuthCallbackException extends Exception {
}
}
export class OAuthStrategy implements Strategy {
constructor(private _config: OAuthConfig) {}
export class OAuthStrategy extends Strategy<typeof schemaProvided> {
constructor(config: ProvidedOAuthConfig) {
super(config, "oauth", config.name, "external");
}
get config() {
return this._config;
getSchema() {
return schemaProvided;
}
getIssuerConfig(): IssuerConfig {
@@ -103,7 +103,7 @@ export class OAuthStrategy implements Strategy {
type: info.type,
client: {
...info.client,
...this._config.client,
...this.config.client,
},
};
}
@@ -172,8 +172,7 @@ export class OAuthStrategy implements Strategy {
) {
const config = await this.getConfig();
const { client, as, type } = config;
//console.log("config", config);
console.log("callbackParams", callbackParams, options);
const parameters = oauth.validateAuthResponse(
as,
client, // no client_secret required
@@ -181,13 +180,9 @@ export class OAuthStrategy implements Strategy {
oauth.expectNoState,
);
if (oauth.isOAuth2Error(parameters)) {
//console.log("callback.error", parameters);
throw new OAuthCallbackException(parameters, "validateAuthResponse");
}
/*console.log(
"callback.parameters",
JSON.stringify(Object.fromEntries(parameters.entries()), null, 2),
);*/
const response = await oauth.authorizationCodeGrantRequest(
as,
client,
@@ -195,13 +190,9 @@ export class OAuthStrategy implements Strategy {
options.redirect_uri,
options.state,
);
//console.log("callback.response", response);
const challenges = oauth.parseWwwAuthenticateChallenges(response);
if (challenges) {
for (const challenge of challenges) {
//console.log("callback.challenge", challenge);
}
// @todo: Handle www-authenticate challenges as needed
throw new OAuthCallbackException(challenges, "www-authenticate");
}
@@ -216,20 +207,13 @@ export class OAuthStrategy implements Strategy {
expectedNonce,
);
if (oauth.isOAuth2Error(result)) {
console.log("callback.error", result);
// @todo: Handle OAuth 2.0 response body error
throw new OAuthCallbackException(result, "processAuthorizationCodeOpenIDResponse");
}
//console.log("callback.result", result);
const claims = oauth.getValidatedIdTokenClaims(result);
//console.log("callback.IDTokenClaims", claims);
const infoRequest = await oauth.userInfoRequest(as, client, result.access_token!);
const resultUser = await oauth.processUserInfoResponse(as, client, claims.sub, infoRequest);
//console.log("callback.resultUser", resultUser);
return await config.profile(resultUser, config, claims); // @todo: check claims
}
@@ -240,8 +224,7 @@ export class OAuthStrategy implements Strategy {
) {
const config = await this.getConfig();
const { client, type, as, profile } = config;
console.log("config", { client, as, type });
console.log("callbackParams", callbackParams, options);
const parameters = oauth.validateAuthResponse(
as,
client, // no client_secret required
@@ -249,13 +232,9 @@ export class OAuthStrategy implements Strategy {
oauth.expectNoState,
);
if (oauth.isOAuth2Error(parameters)) {
console.log("callback.error", parameters);
throw new OAuthCallbackException(parameters, "validateAuthResponse");
}
console.log(
"callback.parameters",
JSON.stringify(Object.fromEntries(parameters.entries()), null, 2),
);
const response = await oauth.authorizationCodeGrantRequest(
as,
client,
@@ -266,9 +245,6 @@ export class OAuthStrategy implements Strategy {
const challenges = oauth.parseWwwAuthenticateChallenges(response);
if (challenges) {
for (const challenge of challenges) {
//console.log("callback.challenge", challenge);
}
// @todo: Handle www-authenticate challenges as needed
throw new OAuthCallbackException(challenges, "www-authenticate");
}
@@ -279,19 +255,15 @@ export class OAuthStrategy implements Strategy {
try {
result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response);
if (oauth.isOAuth2Error(result)) {
console.log("error", result);
throw new Error(); // Handle OAuth 2.0 response body error
}
} catch (e) {
result = (await copy.json()) as any;
console.log("failed", result);
}
const res2 = await oauth.userInfoRequest(as, client, result.access_token!);
const user = await res2.json();
console.log("res2", res2, user);
console.log("result", result);
return await config.profile(user, config, result);
}
@@ -301,7 +273,6 @@ export class OAuthStrategy implements Strategy {
): Promise<UserProfile> {
const type = this.getIssuerConfig().type;
console.log("type", type);
switch (type) {
case "oidc":
return await this.oidc(callbackParams, options);
@@ -325,7 +296,6 @@ export class OAuthStrategy implements Strategy {
};
const setState = async (c: Context, config: TState): Promise<void> => {
console.log("--- setting state", config);
await setSignedCookie(c, cookie_name, JSON.stringify(config), secret, {
secure: true,
httpOnly: true,
@@ -356,7 +326,6 @@ export class OAuthStrategy implements Strategy {
const params = new URLSearchParams(url.search);
const state = await getState(c);
console.log("state", state);
// @todo: add config option to determine if state.action is allowed
const redirect_uri =
@@ -369,21 +338,28 @@ export class OAuthStrategy implements Strategy {
state: state.state,
});
try {
const data = await auth.resolve(state.action, this, profile.sub, profile);
console.log("******** RESOLVED ********", data);
const safeProfile = {
email: profile.email,
strategy_value: profile.sub,
} as const;
if (state.mode === "cookie") {
return await auth.respond(c, data, state.redirect);
const verify = async (user) => {
if (user.strategy_value !== profile.sub) {
throw new Exception("Invalid credentials");
}
};
const opts = {
redirect: state.redirect,
forceJsonResponse: state.mode !== "cookie",
} as const;
return c.json(data);
} catch (e) {
if (state.mode === "cookie") {
return await auth.respond(c, e, state.redirect);
}
throw e;
switch (state.action) {
case "login":
return auth.resolveLogin(c, this, safeProfile, verify, opts);
case "register":
return auth.resolveRegister(c, this, safeProfile, verify, opts);
default:
throw new Error("Invalid action");
}
});
@@ -412,10 +388,8 @@ export class OAuthStrategy implements Strategy {
redirect_uri,
state,
});
//console.log("_state", state);
await setState(c, { state, action, redirect: referer.toString(), mode: "cookie" });
console.log("--redirecting to", response.url);
return c.redirect(response.url);
});
@@ -456,28 +430,15 @@ export class OAuthStrategy implements Strategy {
return hono;
}
getType() {
return "oauth";
}
getMode() {
return "external" as const;
}
getName() {
return this.config.name;
}
getSchema() {
return schemaProvided;
}
toJSON(secrets?: boolean) {
override toJSON(secrets?: boolean) {
const config = secrets ? this.config : filterKeys(this.config, ["secret", "client_id"]);
return {
type: this.getIssuerConfig().type,
...config,
...super.toJSON(secrets),
config: {
...config,
type: this.getIssuerConfig().type,
},
};
}
}

View File

@@ -34,8 +34,6 @@ export const github: IssuerConfig<GithubUserInfo> = {
config: Omit<IssuerConfig, "profile">,
tokenResponse: any,
) => {
console.log("github info", info, config, tokenResponse);
try {
const res = await fetch("https://api.github.com/user/emails", {
headers: {
@@ -45,7 +43,6 @@ export const github: IssuerConfig<GithubUserInfo> = {
},
});
const data = (await res.json()) as GithubUserEmailResponse;
console.log("data", data);
const email = data.find((e: any) => e.primary)?.email;
if (!email) {
throw new Error("No primary email found");

View File

@@ -1,4 +1,4 @@
import { Exception, Permission } from "core";
import { $console, Exception, Permission } from "core";
import { objectTransform } from "core/utils";
import type { Context } from "hono";
import type { ServerEnv } from "modules/Controller";
@@ -14,8 +14,6 @@ export type GuardConfig = {
};
export type GuardContext = Context<ServerEnv> | GuardUserContext;
const debug = false;
export class Guard {
permissions: Permission[];
roles?: Role[];
@@ -83,8 +81,12 @@ export class Guard {
return this;
}
registerPermissions(permissions: Permission[]) {
for (const permission of permissions) {
registerPermissions(permissions: Record<string, Permission>);
registerPermissions(permissions: Permission[]);
registerPermissions(permissions: Permission[] | Record<string, Permission>) {
const p = Array.isArray(permissions) ? permissions : Object.values(permissions);
for (const permission of p) {
this.registerPermission(permission);
}
@@ -95,16 +97,14 @@ export class Guard {
if (user && typeof user.role === "string") {
const role = this.roles?.find((role) => role.name === user?.role);
if (role) {
debug && console.log("guard: role found", [user.role]);
$console.debug(`guard: role "${user.role}" found`);
return role;
}
}
debug &&
console.log("guard: role not found", {
user: user,
role: user?.role,
});
$console.debug("guard: role not found", {
user,
});
return this.getDefaultRole();
}
@@ -120,11 +120,14 @@ export class Guard {
hasPermission(name: string, user?: GuardUserContext): boolean;
hasPermission(permissionOrName: Permission | string, user?: GuardUserContext): boolean {
if (!this.isEnabled()) {
//console.log("guard not enabled, allowing");
return true;
}
const name = typeof permissionOrName === "string" ? permissionOrName : permissionOrName.name;
$console.debug("guard: checking permission", {
name,
user: { id: user?.id, role: user?.role },
});
const exists = this.permissionExists(name);
if (!exists) {
throw new Error(`Permission ${name} does not exist`);
@@ -133,10 +136,10 @@ export class Guard {
const role = this.getUserRole(user);
if (!role) {
debug && console.log("guard: role not found, denying");
$console.debug("guard: user has no role, denying");
return false;
} else if (role.implicit_allow === true) {
debug && console.log("guard: role implicit allow, allowing");
$console.debug(`guard: role "${role.name}" has implicit allow, allowing`);
return true;
}
@@ -144,12 +147,11 @@ export class Guard {
(rolePermission) => rolePermission.permission.name === name,
);
debug &&
console.log("guard: rolePermission, allowing?", {
permission: name,
role: role.name,
allowing: !!rolePermission,
});
$console.debug("guard: rolePermission, allowing?", {
permission: name,
role: role.name,
allowing: !!rolePermission,
});
return !!rolePermission;
}

View File

@@ -1,28 +1,66 @@
import { Exception } from "core";
import { Exception, isDebug } from "core";
import { HttpStatus } from "core/utils";
export class UserExistsException extends Exception {
export class AuthException extends Exception {
getSafeErrorAndCode() {
return {
error: "Invalid credentials",
code: HttpStatus.UNAUTHORIZED,
};
}
override toJSON(): any {
if (isDebug()) {
return super.toJSON();
}
return {
error: this.getSafeErrorAndCode().error,
type: "AuthException",
};
}
}
export class UserExistsException extends AuthException {
override name = "UserExistsException";
override code = 422;
override code = HttpStatus.UNPROCESSABLE_ENTITY;
constructor() {
super("User already exists");
}
}
export class UserNotFoundException extends Exception {
export class UserNotFoundException extends AuthException {
override name = "UserNotFoundException";
override code = 404;
override code = HttpStatus.NOT_FOUND;
constructor() {
super("User not found");
}
}
export class InvalidCredentialsException extends Exception {
export class InvalidCredentialsException extends AuthException {
override name = "InvalidCredentialsException";
override code = 401;
override code = HttpStatus.UNAUTHORIZED;
constructor() {
super("Invalid credentials");
}
}
export class UnableToCreateUserException extends AuthException {
override name = "UnableToCreateUserException";
override code = HttpStatus.INTERNAL_SERVER_ERROR;
constructor() {
super("Unable to create user");
}
}
export class InvalidConditionsException extends AuthException {
override code = HttpStatus.UNPROCESSABLE_ENTITY;
constructor(message: string) {
super(message ?? "Invalid conditions");
}
}

View File

@@ -1,5 +1,4 @@
export { UserExistsException, UserNotFoundException, InvalidCredentialsException } from "./errors";
export { sha256 } from "./utils/hash";
export {
type ProfileExchange,
type Strategy,

View File

@@ -1,13 +0,0 @@
// @deprecated: moved to @bknd/core
export async function sha256(password: string, salt?: string) {
// 1. Convert password to Uint8Array
const encoder = new TextEncoder();
const data = encoder.encode((salt ?? "") + password);
// 2. Hash the data using SHA-256
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
// 3. Convert hash to hex string for easier display
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
}