@snokam/auth 5.0.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -2
- package/package.json +6 -6
- package/src/app-router/index.tsx +0 -1
- package/src/components/Error/index.tsx +3 -4
- package/src/handoff/business.ts +6 -1
- package/src/handoff/seal.ts +3 -1
- package/src/impersonation/context.tsx +1 -0
- package/src/impersonation/handlers.ts +2 -0
- package/src/impersonation/picker.ts +92 -0
- package/src/impersonation/required.ts +0 -2
- package/src/impersonation/routes.ts +8 -3
- package/src/impersonation/personas.ts +0 -64
package/CHANGELOG.md
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# [5.1.0](https://github.com/snokam/components/compare/@snokam/auth@5.0.0...@snokam/auth@5.1.0) (2026-10-14)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- company-first Vis som picker (test identity P3) ([#2917](https://github.com/snokam/components/issues/2917)) ([6098d44](https://github.com/snokam/components/commit/6098d446d91f630a360633615e6c4c0e81c9985f))
|
|
9
11
|
|
|
10
12
|
## [5.0.1](https://github.com/snokam/components/compare/@snokam/auth@5.0.0...@snokam/auth@5.0.1) (2026-10-14)
|
|
11
13
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snokam/auth",
|
|
3
3
|
"description": "Snøkam auth components",
|
|
4
|
-
"version": "5.0
|
|
4
|
+
"version": "5.1.0",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"files": [
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"@snokam/employees-client": "^1.3.0",
|
|
27
27
|
"next-auth": "4.24.15",
|
|
28
28
|
"path-to-regexp": "^8.4.2",
|
|
29
|
-
"@snokam/sanity-page-content": "6.2.0",
|
|
30
|
-
"@snokam/tenant": "4.0.0",
|
|
31
|
-
"@snokam/ui": "6.0.1",
|
|
32
|
-
"@snokam/platform-client": "5.0.0",
|
|
33
29
|
"@snokam/identity": "1.1.0",
|
|
34
|
-
"@snokam/react-query": "1.1.0"
|
|
30
|
+
"@snokam/react-query": "1.1.0",
|
|
31
|
+
"@snokam/platform-client": "5.1.0",
|
|
32
|
+
"@snokam/sanity-page-content": "6.2.0",
|
|
33
|
+
"@snokam/tenant": "4.1.0",
|
|
34
|
+
"@snokam/ui": "6.0.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"next": "16.3.8",
|
package/src/app-router/index.tsx
CHANGED
|
@@ -33,7 +33,6 @@ export {
|
|
|
33
33
|
useImpersonation,
|
|
34
34
|
} from "../impersonation/context";
|
|
35
35
|
export type { ImpersonationActive } from "../impersonation/cookies";
|
|
36
|
-
export { FOREIGN_BUSINESS_ERROR } from "../impersonation/required";
|
|
37
36
|
export { isAllowedRedirect } from "../impersonation/redirects";
|
|
38
37
|
export { signIn, SessionProvider } from "next-auth/react";
|
|
39
38
|
export {
|
|
@@ -6,7 +6,7 @@ import { Alert, Avatar } from "@snokam/ui";
|
|
|
6
6
|
import { SystemPage } from "@snokam/ui/system-page";
|
|
7
7
|
import { ThemeScope } from "@snokam/ui/theme";
|
|
8
8
|
import { useAuth } from "../../hooks";
|
|
9
|
-
import {
|
|
9
|
+
import { useTenantEntitlements } from "@snokam/tenant";
|
|
10
10
|
|
|
11
11
|
export interface ErrorPageProps {
|
|
12
12
|
message?: ReactNode;
|
|
@@ -23,8 +23,7 @@ export const Error = ({
|
|
|
23
23
|
}: ErrorPageProps) => {
|
|
24
24
|
const { session, signOut } = useAuth();
|
|
25
25
|
|
|
26
|
-
const
|
|
27
|
-
const isProd = config?.NEXT_PUBLIC_ENVIRONMENT === "p";
|
|
26
|
+
const tenant = useTenantEntitlements();
|
|
28
27
|
|
|
29
28
|
return (
|
|
30
29
|
<SystemPage>
|
|
@@ -50,7 +49,7 @@ export const Error = ({
|
|
|
50
49
|
<br />({session.user.email})
|
|
51
50
|
</Styled.User>
|
|
52
51
|
)}
|
|
53
|
-
{
|
|
52
|
+
{tenant?.personaMode && (
|
|
54
53
|
<Alert severity="warning">
|
|
55
54
|
Du er i test! Husk at det kun er mulig å logge inn <br />
|
|
56
55
|
med en av våre testbrukere som du finner i 1Password.
|
package/src/handoff/business.ts
CHANGED
|
@@ -22,5 +22,10 @@ export async function hostOwner(url: string): Promise<HostOwner> {
|
|
|
22
22
|
platformApiBase: process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL ?? "",
|
|
23
23
|
authToken: await getManagedBearer(),
|
|
24
24
|
});
|
|
25
|
-
return tenant?.businessId
|
|
25
|
+
return tenant?.businessId
|
|
26
|
+
? {
|
|
27
|
+
businessId: tenant.businessId,
|
|
28
|
+
displayName: tenant.displayName ?? undefined,
|
|
29
|
+
}
|
|
30
|
+
: UNKNOWN_HOST;
|
|
26
31
|
}
|
package/src/handoff/seal.ts
CHANGED
|
@@ -2,7 +2,9 @@ export const PLATFORM_HOST = "platform";
|
|
|
2
2
|
export const UNKNOWN_HOST = "unknown";
|
|
3
3
|
|
|
4
4
|
export type HostOwner =
|
|
5
|
-
|
|
5
|
+
| typeof PLATFORM_HOST
|
|
6
|
+
| typeof UNKNOWN_HOST
|
|
7
|
+
| { businessId: string; displayName?: string };
|
|
6
8
|
|
|
7
9
|
export function maySealFor(
|
|
8
10
|
owner: HostOwner,
|
|
@@ -20,6 +20,7 @@ interface StartBody {
|
|
|
20
20
|
targetName?: string;
|
|
21
21
|
targetImage?: string;
|
|
22
22
|
reason?: string;
|
|
23
|
+
businessId?: string;
|
|
23
24
|
redirectTo?: string;
|
|
24
25
|
returnTo?: string;
|
|
25
26
|
landingUrl?: string;
|
|
@@ -85,6 +86,7 @@ export function createImpersonationStartHandler(authConfig: Auth.Config) {
|
|
|
85
86
|
targetEmail: body.targetEmail,
|
|
86
87
|
targetName: body.targetName,
|
|
87
88
|
reason: body.reason,
|
|
89
|
+
businessId: body.businessId,
|
|
88
90
|
},
|
|
89
91
|
})
|
|
90
92
|
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ProtectedEmployee } from "@snokam/platform-client";
|
|
2
|
+
import { Auth } from "../types";
|
|
3
|
+
import { resolveAccessToken } from "../route-handler/token";
|
|
4
|
+
import { platformCall, platformClient, platformNotConfigured } from "./request";
|
|
5
|
+
import { hostOwner } from "../handoff/business";
|
|
6
|
+
|
|
7
|
+
function persona(employee: ProtectedEmployee) {
|
|
8
|
+
return {
|
|
9
|
+
name: employee.name,
|
|
10
|
+
email: employee.email,
|
|
11
|
+
position: employee.position,
|
|
12
|
+
about: employee.about,
|
|
13
|
+
active: employee.active,
|
|
14
|
+
isBot: employee.isBot,
|
|
15
|
+
image: { asset: { url: employee.image?.asset?.url } },
|
|
16
|
+
scopeGrants: employee.scopeGrants,
|
|
17
|
+
groups: (employee.groups ?? []).map((group) => ({
|
|
18
|
+
name: group.name,
|
|
19
|
+
scopeGrants: group.scopeGrants,
|
|
20
|
+
})),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ReturnBusiness {
|
|
25
|
+
id: string;
|
|
26
|
+
displayName: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function returnBusiness(
|
|
30
|
+
request: Request
|
|
31
|
+
): Promise<ReturnBusiness | null> {
|
|
32
|
+
const returnTo = new URL(request.url).searchParams.get("returnTo");
|
|
33
|
+
if (!returnTo || !URL.canParse(returnTo)) return null;
|
|
34
|
+
try {
|
|
35
|
+
const owner = await hostOwner(returnTo);
|
|
36
|
+
return typeof owner === "object"
|
|
37
|
+
? { id: owner.businessId, displayName: owner.displayName ?? null }
|
|
38
|
+
: null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function callerClient(authConfig: Auth.Config, request: Request) {
|
|
45
|
+
const caller = await resolveAccessToken(authConfig, request);
|
|
46
|
+
if ("error" in caller) return { error: caller.error };
|
|
47
|
+
const client = platformClient(caller.accessToken);
|
|
48
|
+
return client ? { client } : { error: platformNotConfigured() };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createPickerBusinessesHandler(authConfig: Auth.Config) {
|
|
52
|
+
return async function GET(request: Request) {
|
|
53
|
+
const caller = await callerClient(authConfig, request);
|
|
54
|
+
if ("error" in caller) return caller.error;
|
|
55
|
+
|
|
56
|
+
const [businesses, apps] = await Promise.all([
|
|
57
|
+
platformCall(caller.client.listImpersonationBusinesses()),
|
|
58
|
+
platformCall(caller.client.getMeApps()),
|
|
59
|
+
]);
|
|
60
|
+
if ("error" in businesses) return businesses.error;
|
|
61
|
+
if ("error" in apps) return apps.error;
|
|
62
|
+
if (businesses.value.length === 0) {
|
|
63
|
+
return new Response("Not found", { status: 404 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return Response.json({
|
|
67
|
+
businesses: businesses.value,
|
|
68
|
+
apps: apps.value,
|
|
69
|
+
returnBusiness: await returnBusiness(request),
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createPickerPeopleHandler(authConfig: Auth.Config) {
|
|
75
|
+
return async function GET(request: Request) {
|
|
76
|
+
const businessId = new URL(request.url).searchParams.get("businessId");
|
|
77
|
+
if (!businessId) return new Response("Missing businessId", { status: 400 });
|
|
78
|
+
|
|
79
|
+
const caller = await callerClient(authConfig, request);
|
|
80
|
+
if ("error" in caller) return caller.error;
|
|
81
|
+
|
|
82
|
+
const people = await platformCall(
|
|
83
|
+
caller.client.listImpersonationPeople({ businessId })
|
|
84
|
+
);
|
|
85
|
+
if ("error" in people) return people.error;
|
|
86
|
+
|
|
87
|
+
return Response.json({
|
|
88
|
+
kind: people.value.kind,
|
|
89
|
+
people: (people.value.people ?? []).map(persona),
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -2,8 +2,6 @@ import type { TokenClaims } from "./claims";
|
|
|
2
2
|
|
|
3
3
|
export const IMPERSONATION_REQUIRED_ERROR = "impersonation_required";
|
|
4
4
|
|
|
5
|
-
export const FOREIGN_BUSINESS_ERROR = "foreign_business";
|
|
6
|
-
|
|
7
5
|
export const IMPERSONATION_PICKER_PATH = "/velg-bruker";
|
|
8
6
|
|
|
9
7
|
const RENEW_MARGIN_SECONDS = 120;
|
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
createImpersonationAdoptHandler,
|
|
6
6
|
} from "./handlers";
|
|
7
7
|
import { createImpersonationRenewHandler } from "./renew";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createPickerBusinessesHandler,
|
|
10
|
+
createPickerPeopleHandler,
|
|
11
|
+
} from "./picker";
|
|
9
12
|
|
|
10
13
|
type OpContext = { params: Promise<{ op: string }> };
|
|
11
14
|
|
|
@@ -14,13 +17,15 @@ export function createImpersonationRouteHandlers(authConfig: Auth.Config) {
|
|
|
14
17
|
const end = createImpersonationEndHandler(authConfig);
|
|
15
18
|
const adopt = createImpersonationAdoptHandler(authConfig);
|
|
16
19
|
const renew = createImpersonationRenewHandler(authConfig);
|
|
17
|
-
const
|
|
20
|
+
const businesses = createPickerBusinessesHandler(authConfig);
|
|
21
|
+
const people = createPickerPeopleHandler(authConfig);
|
|
18
22
|
|
|
19
23
|
return {
|
|
20
24
|
async GET(request: Request, ctx: OpContext) {
|
|
21
25
|
const { op } = await ctx.params;
|
|
22
26
|
if (op === "adopt") return adopt(request);
|
|
23
|
-
if (op === "
|
|
27
|
+
if (op === "businesses") return businesses(request);
|
|
28
|
+
if (op === "people") return people(request);
|
|
24
29
|
return new Response("Not found", { status: 404 });
|
|
25
30
|
},
|
|
26
31
|
async POST(request: Request, ctx: OpContext) {
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { ProtectedEmployee } from "@snokam/platform-client";
|
|
2
|
-
import { Auth } from "../types";
|
|
3
|
-
import { resolveAccessToken } from "../route-handler/token";
|
|
4
|
-
import { tokenClaims } from "./claims";
|
|
5
|
-
import { platformCall, platformClient, platformNotConfigured } from "./request";
|
|
6
|
-
import { FOREIGN_BUSINESS_ERROR, personaRequired } from "./required";
|
|
7
|
-
import { hostOwner } from "../handoff/business";
|
|
8
|
-
import { maySealFor } from "../handoff/seal";
|
|
9
|
-
|
|
10
|
-
function persona(employee: ProtectedEmployee) {
|
|
11
|
-
return {
|
|
12
|
-
name: employee.name,
|
|
13
|
-
email: employee.email,
|
|
14
|
-
position: employee.position,
|
|
15
|
-
about: employee.about,
|
|
16
|
-
active: employee.active,
|
|
17
|
-
isBot: employee.isBot,
|
|
18
|
-
image: { asset: { url: employee.image?.asset?.url } },
|
|
19
|
-
scopeGrants: employee.scopeGrants,
|
|
20
|
-
groups: (employee.groups ?? []).map((group) => ({
|
|
21
|
-
name: group.name,
|
|
22
|
-
scopeGrants: group.scopeGrants,
|
|
23
|
-
})),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function isForeignReturn(
|
|
28
|
-
request: Request,
|
|
29
|
-
callerBusiness: string | undefined
|
|
30
|
-
): Promise<boolean> {
|
|
31
|
-
const returnTo = new URL(request.url).searchParams.get("returnTo");
|
|
32
|
-
if (!returnTo || !URL.canParse(returnTo)) return false;
|
|
33
|
-
return !maySealFor(await hostOwner(returnTo), callerBusiness);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function createImpersonationPersonasHandler(authConfig: Auth.Config) {
|
|
37
|
-
return async function GET(request: Request) {
|
|
38
|
-
const caller = await resolveAccessToken(authConfig, request);
|
|
39
|
-
if ("error" in caller) return caller.error;
|
|
40
|
-
|
|
41
|
-
const claims = tokenClaims(caller.accessToken);
|
|
42
|
-
if (await isForeignReturn(request, claims.biz)) {
|
|
43
|
-
return Response.json({ error: FOREIGN_BUSINESS_ERROR }, { status: 403 });
|
|
44
|
-
}
|
|
45
|
-
if (!personaRequired(claims)) {
|
|
46
|
-
return new Response("Not found", { status: 404 });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const client = platformClient(caller.accessToken);
|
|
50
|
-
if (!client) return platformNotConfigured();
|
|
51
|
-
|
|
52
|
-
const [personas, apps] = await Promise.all([
|
|
53
|
-
platformCall(client.listTestPersonas()),
|
|
54
|
-
platformCall(client.getMeApps()),
|
|
55
|
-
]);
|
|
56
|
-
if ("error" in personas) return personas.error;
|
|
57
|
-
if ("error" in apps) return apps.error;
|
|
58
|
-
|
|
59
|
-
return Response.json({
|
|
60
|
-
personas: personas.value.map(persona),
|
|
61
|
-
apps: apps.value,
|
|
62
|
-
});
|
|
63
|
-
};
|
|
64
|
-
}
|