Skip to content

Commit 5d8239f

Browse files
committed
docs(changeset): Change how login and logout redirect are handled
1 parent fc6d986 commit 5d8239f

File tree

8 files changed

+179
-146
lines changed

8 files changed

+179
-146
lines changed

.changeset/brave-suns-kiss.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@storybooker/adapter-azure": patch
3+
"@storybooker/core": patch
4+
---
5+
6+
Change how login and logout redirect are handled

packages/adapter-azure/src/easy-auth.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ export interface AzureEasyAuthUser extends StoryBookerUser {
1717

1818
export type AzureEasyAuthRoleMap = Map<string, Permission[]>;
1919

20-
const DEFAULT_AUTHORISE: AuthServiceAuthorise<AzureEasyAuthUser> = (
21-
{ action },
22-
{ user },
23-
) => {
20+
const DEFAULT_AUTHORISE: AuthServiceAuthorise<AzureEasyAuthUser> = ({
21+
permission,
22+
user,
23+
}) => {
2424
if (!user) {
2525
return false;
2626
}
@@ -29,23 +29,25 @@ const DEFAULT_AUTHORISE: AuthServiceAuthorise<AzureEasyAuthUser> = (
2929
return true;
3030
}
3131

32-
if (action === "read") {
32+
if (permission.action === "read") {
3333
return true;
3434
}
3535

3636
return Boolean(user.roles && user.roles.length > 0);
3737
};
3838

3939
export class AzureEasyAuthService implements AuthService<AzureEasyAuthUser> {
40-
authorise: AuthServiceAuthorise<AzureEasyAuthUser>;
40+
authorise: AuthService<AzureEasyAuthUser>["authorise"];
4141

4242
constructor(
4343
authorise: AuthServiceAuthorise<AzureEasyAuthUser> = DEFAULT_AUTHORISE,
4444
) {
4545
this.authorise = authorise;
4646
}
4747

48-
async getUserDetails(request: Request): Promise<AzureEasyAuthUser> {
48+
getUserDetails: AuthService<AzureEasyAuthUser>["getUserDetails"] = async (
49+
request,
50+
) => {
4951
const principalHeader = request.headers.get("x-ms-client-principal");
5052
if (!principalHeader) {
5153
throw new Response(
@@ -95,9 +97,18 @@ export class AzureEasyAuthService implements AuthService<AzureEasyAuthUser> {
9597
title: roles.join(", "),
9698
type: "user",
9799
};
98-
}
100+
};
101+
102+
login: AuthService<AzureEasyAuthUser>["login"] = async (request) => {
103+
const url = new URL("/.auth/login", request.url);
104+
105+
return new Response(null, {
106+
headers: { Location: url.toString() },
107+
status: 302,
108+
});
109+
};
99110

100-
logout: (request: Request) => Promise<Response> = async (request) => {
111+
logout: AuthService<AzureEasyAuthUser>["logout"] = async (request) => {
101112
const url = new URL("/.auth/logout", request.url);
102113

103114
return new Response(null, {

packages/core/src/root/routes.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CONTENT_TYPES } from "#constants";
22
import { ProjectsModel } from "#projects/model";
33
import { defineRoute } from "#router";
44
import { getStore } from "#store";
5-
import { URLS } from "#urls";
5+
import { urlBuilder, URLS } from "#urls";
66
import { authenticateOrThrow } from "#utils/auth";
77
import { checkIsJSONRequest } from "#utils/request";
88
import {
@@ -89,38 +89,41 @@ export const login = defineRoute("get", URLS.ui.login, undefined, async () => {
8989
if (!auth?.login) {
9090
return responseError(translation.errorMessages.auth_setup_missing, 404);
9191
}
92-
const serviceUrl = url.replace(URLS.ui.login, "");
93-
const response = await auth.login(request, serviceUrl);
94-
95-
if (response.status >= 300 && response.status < 400) {
96-
return responseRedirect(
97-
response.headers.get("Location") || "/",
98-
response.status,
99-
);
92+
93+
const response = await auth.login(request);
94+
95+
if (response.status >= 400) {
96+
return response;
10097
}
10198

102-
return response;
99+
const redirectTo = new URL(url).searchParams.get("redirect") || "";
100+
101+
return responseRedirect(url.replace(URLS.ui.login, redirectTo), {
102+
headers: response.headers,
103+
status: 302,
104+
});
103105
});
104106

105107
export const logout = defineRoute(
106108
"get",
107109
URLS.ui.logout,
108110
undefined,
109111
async () => {
110-
const { auth, request, translation, user } = getStore();
112+
const { auth, request, translation, url, user } = getStore();
111113
if (!auth?.logout || !user) {
112114
return responseError(translation.errorMessages.auth_setup_missing, 404);
113115
}
114116

115117
const response = await auth.logout(request, user);
116-
if (response.status >= 300 && response.status < 400) {
117-
return responseRedirect(
118-
response.headers.get("Location") || "/",
119-
response.status,
120-
);
118+
if (response.status >= 400) {
119+
return response;
121120
}
122121

123-
return response;
122+
const serviceUrl = url.replace(URLS.ui.logout, "");
123+
return responseRedirect(serviceUrl, {
124+
headers: response.headers,
125+
status: 302,
126+
});
124127
},
125128
);
126129

@@ -138,13 +141,13 @@ export const account = defineRoute(
138141
const serviceUrl = url.replace(URLS.ui.account, "");
139142

140143
if (auth.login) {
141-
return await auth.login(request, serviceUrl);
144+
return responseRedirect(urlBuilder.login(URLS.ui.account), 302);
142145
}
143146

144147
return responseRedirect(serviceUrl, 404);
145148
}
146149

147-
const children = await auth.renderAccount?.(request, user);
150+
const children = await auth.renderAccountDetails?.(request, user);
148151

149152
return responseHTML(renderAccountPage({ children }));
150153
},

packages/core/src/services/auth.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Service adapter to manage authentication.
3+
*
4+
* The service is responsible to authorise users from
5+
* accessing the app.
6+
*/
7+
export interface AuthService<
8+
AuthUser extends StoryBookerUser = StoryBookerUser,
9+
> {
10+
/**
11+
* An optional method that is called on app boot-up
12+
* to run async setup functions.
13+
* Preferably, this function should not throw errors.
14+
*/
15+
init?: () => Promise<void>;
16+
17+
/**
18+
* This callback is called before every protected route and determines if user
19+
* has access to the route. It receives a permission object.
20+
*
21+
* - Respond with `true` to allow user to proceed.
22+
* - Respond with `false` to block user.
23+
* - Respond with `Response` to return custom response
24+
*/
25+
authorise: AuthServiceAuthorise<AuthUser>;
26+
27+
/**
28+
* Get details about the user based on incoming request.
29+
*
30+
* Throw an error or response (redirect-to-login) if it is a unauthenticated/unauthorised request.
31+
*/
32+
getUserDetails: (request: Request) => Promise<AuthUser | null>;
33+
34+
/**
35+
* Get user to login from UI. The returning response should create auth session.
36+
* User will be redirected automatically after function is resolved.
37+
*/
38+
login?: (request: Request) => Promise<Response> | Response;
39+
40+
/**
41+
* Get user to logout from UI. The returning response should clear auth session.
42+
* User will be redirected automatically after function is resolved.
43+
*/
44+
logout?: (request: Request, user: AuthUser) => Promise<Response> | Response;
45+
46+
/**
47+
* Render custom HTML in account page. Must return valid HTML string;
48+
*/
49+
renderAccountDetails?: (
50+
request: Request,
51+
user: AuthUser,
52+
) => Promise<string> | string;
53+
}
54+
55+
/**
56+
* Type for the callback function to check permissions.
57+
*
58+
* Return true to allow access, or following to deny:
59+
* - false - returns 403 response
60+
* - Response - returns the specified HTTP response
61+
*/
62+
export type AuthServiceAuthorise<
63+
AuthUser extends StoryBookerUser = StoryBookerUser,
64+
> = (params: {
65+
permission: PermissionWithKey;
66+
request: Request;
67+
user: AuthUser | undefined | null;
68+
}) => Promise<boolean | Response> | boolean | Response;
69+
70+
/** Type of permission to check */
71+
export interface Permission {
72+
action: PermissionAction;
73+
projectId: string | undefined;
74+
resource: PermissionResource;
75+
}
76+
/** Permission object with key */
77+
export type PermissionWithKey = Permission & { key: PermissionKey };
78+
/** Permission in a string format */
79+
export type PermissionKey =
80+
`${PermissionResource}:${PermissionAction}:${string}`;
81+
/** Type of possible resources to check permissions for */
82+
export type PermissionResource =
83+
| "project"
84+
| "build"
85+
| "label"
86+
| "openapi"
87+
| "ui";
88+
/** Type of possible actions to check permissions for */
89+
export type PermissionAction = "create" | "read" | "update" | "delete";
90+
91+
/**
92+
* Base representation of a generic User
93+
*/
94+
export interface StoryBookerUser {
95+
id: string;
96+
displayName: string;
97+
imageUrl?: string;
98+
title?: string;
99+
}

packages/core/src/types.ts

Lines changed: 2 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import type { AuthService, StoryBookerUser } from "./services/auth";
12
import type { DatabaseService } from "./services/database";
23
import type { Translation } from "./translations";
34

45
export type * from "./services/database";
6+
export type * from "./services/auth";
57

68
/**
79
* Options for creating a request handler.
@@ -86,12 +88,6 @@ export interface LoggerService {
8688
log: (...args: unknown[]) => void;
8789
}
8890

89-
export interface DatabaseDocumentListOptions<Item extends { id: string }> {
90-
limit?: number;
91-
filter?: string | ((item: Item) => boolean);
92-
select?: string[];
93-
sort?: "latest" | ((item1: Item, item2: Item) => number);
94-
}
9591
/**
9692
* Service to interact with file-storage.
9793
*
@@ -179,94 +175,3 @@ export interface BrandTheme {
179175
default: string;
180176
};
181177
}
182-
183-
/**
184-
* Service to manage authentication.
185-
*
186-
* The service is responsible to authorise users from
187-
* accessing the app.
188-
*/
189-
export interface AuthService<
190-
AuthUser extends StoryBookerUser = StoryBookerUser,
191-
> {
192-
/**
193-
* An optional method that is called on app boot-up
194-
* to run async setup functions.
195-
* Preferably, this function should not throw errors.
196-
*/
197-
init?: () => Promise<void>;
198-
/**
199-
* This callback is called before every protected route and determines if user
200-
* has access to the route. It receives a permission object.
201-
*
202-
* - Response with `true` to allow user to proceed.
203-
* - Respond with `false` to block user.
204-
* - Respond with `Response` to return custom response
205-
*/
206-
authorise: AuthServiceAuthorise<AuthUser>;
207-
/**
208-
* Get details about the user based on incoming request.
209-
*
210-
* Throw an error or response (redirect-to-login) if it is a unauthenticated/anauthorised request.
211-
*/
212-
getUserDetails: (request: Request) => Promise<AuthUser | null>;
213-
/**
214-
* Get user to login from UI. The returning response should redirect user back to serviceUrl.
215-
*/
216-
login?: (
217-
request: Request,
218-
serviceUrl: string,
219-
) => Promise<Response> | Response;
220-
/**
221-
* Get user to logout from UI. The returning response should clear auth session.
222-
*/
223-
logout?: (request: Request, user: AuthUser) => Promise<Response> | Response;
224-
/**
225-
* Render custom HTML in account page. Must return valid HTML string;
226-
*/
227-
renderAccount?: (
228-
request: Request,
229-
user: AuthUser,
230-
) => Promise<string> | string;
231-
}
232-
233-
/**
234-
* Type for the callback function to check permissions.
235-
*
236-
* Return true to allow access, or following to deny:
237-
* - false - returns 403 response
238-
* - Response - returns the specified HTTP response
239-
*/
240-
export type AuthServiceAuthorise<
241-
AuthUser extends StoryBookerUser = StoryBookerUser,
242-
> = (
243-
permission: PermissionWithKey,
244-
options: { request: Request; user: AuthUser | undefined | null },
245-
) => Promise<boolean | Response> | boolean | Response;
246-
247-
/** Type of permission to check */
248-
export interface Permission {
249-
action: PermissionAction;
250-
projectId: string | undefined;
251-
resource: PermissionResource;
252-
}
253-
export type PermissionWithKey = Permission & { key: PermissionKey };
254-
export type PermissionKey =
255-
`${PermissionResource}:${PermissionAction}:${string}`;
256-
/** Type of possible resources to check permissions for */
257-
export type PermissionResource =
258-
| "project"
259-
| "build"
260-
| "label"
261-
| "openapi"
262-
| "ui";
263-
264-
/** Type of possible actions to check permissions for */
265-
export type PermissionAction = "create" | "read" | "update" | "delete";
266-
267-
export interface StoryBookerUser {
268-
id: string;
269-
displayName: string;
270-
imageUrl?: string;
271-
title?: string;
272-
}

packages/core/src/urls.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ export const urlBuilder = {
5959
root: (): string => {
6060
return href(URLS.ui.root);
6161
},
62+
login: (redirect: string): string => {
63+
return href(URLS.ui.login, null, { redirect });
64+
},
6265
staticFile: (filepath: string): string => {
6366
return href(URLS.ui.catchAll, { filepath });
6467
},
@@ -69,12 +72,10 @@ export const urlBuilder = {
6972
return href(URLS.projects.create);
7073
},
7174
projectId: (projectId: string): string => {
72-
const url = href(URLS.projects.id, { projectId });
73-
return url.toString();
75+
return href(URLS.projects.id, { projectId });
7476
},
7577
projectIdUpdate: (projectId: string): string => {
76-
const url = href(URLS.projects.update, { projectId });
77-
return url.toString();
78+
return href(URLS.projects.update, { projectId });
7879
},
7980
allBuilds: (projectId: string): string => {
8081
const { prefix, request } = getStore();

0 commit comments

Comments
 (0)