mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
The IAM callback at /auth/callback reads IAM's userinfo answer and refuses an address IAM has not verified. An account is found by its linked IAM identity alone (provider hanzo, the IAM subject); an existing account that only shares the address is refused, never merged. A first sign-in makes an account only for an admin of the org the application serves or for someone an organisation invited at that verified address. Every refusal returns to /signin with a sentence saying what to do. Accepting an organisation invitation joins the signed-in person, and only when their verified address is the invited one; opening the link no longer adds whoever holds the address. The local email/password sign-up, sign-in, reset, verify-email and update routes, pages, jobs and mail are gone, as are passkey sign-in, the Google, Microsoft and generic OIDC providers, the per-organisation SSO portal and its account-link flow. Passkeys and two-factor stay for confirming a signature. The e2e suite starts sessions the way a finished IAM sign-in leaves them. Co-authored-by: Hanzo Dev <[email protected]>
189 lines
4.8 KiB
TypeScript
189 lines
4.8 KiB
TypeScript
import type { ClientResponse } from 'hono/client';
|
|
import { hc } from 'hono/client';
|
|
import superjson from 'superjson';
|
|
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@hanzo/esign-lib/constants/app';
|
|
import { AppError } from '@hanzo/esign-lib/errors/app-error';
|
|
|
|
import type { AuthAppType } from '../server';
|
|
import type { SessionValidationResult } from '../server/lib/session/session';
|
|
import type { PartialAccount } from '../server/lib/utils/get-accounts';
|
|
import type { ActiveSession } from '../server/lib/utils/get-session';
|
|
import type {
|
|
TDisableTwoFactorRequestSchema,
|
|
TEnableTwoFactorRequestSchema,
|
|
TViewTwoFactorRecoveryCodesRequestSchema,
|
|
} from '../server/routes/two-factor.types';
|
|
|
|
type AuthClientType = ReturnType<typeof hc<AuthAppType>>;
|
|
|
|
export class AuthClient {
|
|
public client: AuthClientType;
|
|
|
|
private signOutredirectPath: string = '/signin';
|
|
|
|
constructor(options: { baseUrl: string }) {
|
|
this.client = hc<AuthAppType>(options.baseUrl);
|
|
}
|
|
|
|
public async signOut({ redirectPath }: { redirectPath?: string } = {}) {
|
|
await this.client.signout.$post();
|
|
|
|
window.location.href = redirectPath ?? this.signOutredirectPath;
|
|
}
|
|
|
|
public async signOutAllSessions() {
|
|
await this.client['signout-all'].$post();
|
|
}
|
|
|
|
public async signOutSession({
|
|
sessionId,
|
|
redirectPath,
|
|
}: {
|
|
sessionId: string;
|
|
redirectPath?: string;
|
|
}) {
|
|
await this.client['signout-session'].$post({
|
|
json: { sessionId },
|
|
});
|
|
|
|
if (redirectPath) {
|
|
window.location.href = redirectPath;
|
|
}
|
|
}
|
|
|
|
public async getSession() {
|
|
const response = await this.client['session-json'].$get();
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
return superjson.deserialize<SessionValidationResult>(result);
|
|
}
|
|
|
|
public async getSessions() {
|
|
const response = await this.client['sessions'].$get();
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
return superjson.deserialize<{ sessions: ActiveSession[] }>(result);
|
|
}
|
|
|
|
// !: Unused for now since it isn't providing the type narrowing
|
|
// !: we need.
|
|
private async handleError<T>(response: ClientResponse<T>): Promise<void> {
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
}
|
|
|
|
public account = {
|
|
getMany: async () => {
|
|
const response = await this.client['accounts'].$get();
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
return superjson.deserialize<{ accounts: PartialAccount[] }>(result);
|
|
},
|
|
delete: async (accountId: string) => {
|
|
const response = await this.client['account'][':accountId'].$delete({
|
|
param: { accountId },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
},
|
|
};
|
|
|
|
public twoFactor = {
|
|
setup: async () => {
|
|
const response = await this.client['two-factor'].setup.$post();
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
|
|
return response.json();
|
|
},
|
|
enable: async (data: TEnableTwoFactorRequestSchema) => {
|
|
const response = await this.client['two-factor'].enable.$post({ json: data });
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
|
|
return response.json();
|
|
},
|
|
disable: async (data: TDisableTwoFactorRequestSchema) => {
|
|
const response = await this.client['two-factor'].disable.$post({ json: data });
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
},
|
|
viewRecoveryCodes: async (data: TViewTwoFactorRecoveryCodesRequestSchema) => {
|
|
const response = await this.client['two-factor']['view-recovery-codes'].$post({ json: data });
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
|
|
return response.json();
|
|
},
|
|
};
|
|
|
|
public hanzo = {
|
|
signIn: async ({ redirectPath }: { redirectPath?: string } = {}) => {
|
|
const response = await this.client['oauth'].authorize.hanzo.$post({
|
|
json: { redirectPath },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
|
|
throw AppError.parseError(error);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.redirectUrl) {
|
|
window.location.href = data.redirectUrl;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
export const authClient = new AuthClient({
|
|
baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/v1/auth`,
|
|
});
|