Skip to content

Commit 8148fb3

Browse files
committed
Seperate out OAuth API
1 parent 18f451c commit 8148fb3

19 files changed

Lines changed: 266 additions & 13 deletions

File tree

src/lib/api/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,4 @@ export const shockersV2Api = new ShockersV2Api(DefaultApiV2Configuration);
5959
export const publicShockerSharesApi = new PublicShockerSharesApi(DefaultApiV1Configuration);
6060
export const shockerSharesV1Api = new ShockerSharesV1Api(DefaultApiV1Configuration);
6161
export const shockerSharesV2Api = new ShockerSharesV2Api(DefaultApiV2Configuration);
62-
export const oauthApi = new OAuthApi(DefaultApiV1Configuration);
6362
export const usersApi = new UsersApi(DefaultApiV1Configuration);

src/lib/api/next/ResponseError.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export class ResponseError extends Error {
2+
override name = 'ResponseError';
3+
constructor(
4+
public response: Response,
5+
msg?: string
6+
) {
7+
super(msg ?? `HTTP ${response.status} ${response.statusText}`);
8+
}
9+
}

src/lib/api/next/TransformError.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class TransformError extends Error {
2+
override name = 'TransformError';
3+
constructor(message: string) {
4+
super(message);
5+
}
6+
}

src/lib/api/next/base.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { PUBLIC_BACKEND_API_DOMAIN } from '$env/static/public';
2+
import { ResponseError } from './ResponseError';
3+
4+
const BaseUrl = `https://${PUBLIC_BACKEND_API_DOMAIN}`;
5+
6+
type ApiVersion = 1 | 2;
7+
export type Path = `/${ApiVersion}/${string}`;
8+
9+
export function GetBackendUrl(path: Path) {
10+
return BaseUrl + path;
11+
}
12+
13+
export async function GetJson<T>(
14+
path: Path,
15+
expectedStatus = 200,
16+
transformer: (data: unknown) => T
17+
): Promise<T> {
18+
const res = await fetch(BaseUrl + path, {
19+
headers: { accept: 'application/json' },
20+
method: 'GET',
21+
redirect: 'error',
22+
});
23+
24+
if (res.status !== expectedStatus) {
25+
throw new ResponseError(res, `Unexpected status ${res.status}`);
26+
}
27+
28+
const contentType = res.headers.get('content-type') ?? '';
29+
if (!contentType.includes('application/json')) {
30+
throw new ResponseError(res, `Expected JSON but got ${contentType}`);
31+
}
32+
33+
const data = await res.json();
34+
35+
return transformer(data);
36+
}
37+
38+
export async function PostJson<T>(
39+
path: Path,
40+
body: unknown,
41+
expectedStatus = 200,
42+
transformer: (data: unknown) => T
43+
): Promise<T> {
44+
const url = GetBackendUrl(path);
45+
46+
const res = await fetch(url, {
47+
method: 'POST',
48+
headers: {
49+
'content-type': 'application/json',
50+
accept: 'application/json',
51+
},
52+
body: JSON.stringify(body),
53+
redirect: 'error',
54+
});
55+
56+
if (res.status !== expectedStatus) {
57+
throw new ResponseError(res, `Unexpected status ${res.status} for POST ${url}`);
58+
}
59+
60+
const contentType = res.headers.get('content-type') ?? '';
61+
if (!contentType.includes('application/json')) {
62+
throw new ResponseError(res, `Expected JSON but got ${contentType}`);
63+
}
64+
65+
const data = await res.json();
66+
67+
return transformer(data);
68+
}
69+
70+
export async function PostRedirect(
71+
path: Path,
72+
expectedStatus: 302 | 303 | 307 | 308 = 302
73+
): Promise<void> {
74+
const url = BaseUrl + path;
75+
const res = await fetch(url, { method: 'POST', redirect: 'manual' });
76+
77+
if (res.status !== expectedStatus) {
78+
throw new ResponseError(res, `Unexpected status ${res.status} for POST ${url}`);
79+
}
80+
81+
const loc = res.headers.get('Location');
82+
if (!loc) throw new ResponseError(res, 'Missing Location header');
83+
84+
const target = new URL(loc, url); // handles absolute or relative
85+
86+
// Restrict which domains we can be redirected to
87+
const allowedHosts = new Set([PUBLIC_BACKEND_API_DOMAIN, 'accounts.google.com', 'github.com']);
88+
89+
if (!allowedHosts.has(target.hostname)) {
90+
throw new ResponseError(res, `Blocked redirect to disallowed host ${target.href}`);
91+
}
92+
93+
if (target.protocol !== 'https:') {
94+
throw new ResponseError(res, `Blocked non-HTTPS redirect to ${target.href}`);
95+
}
96+
97+
window.location.assign(target.toString());
98+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { RoleType } from './RoleType';
2+
3+
export interface LoginOkResponse {
4+
accountId: string;
5+
accountName: string;
6+
accountEmail: string;
7+
isVerified: boolean;
8+
profileImage: string;
9+
accountRoles: RoleType[];
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface OAuthFinalizeRequest {
2+
username?: string | null;
3+
email: string | null;
4+
password: string | null;
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface OAuthSignupData {
2+
provider: string;
3+
email: string | null;
4+
displayName: string | null;
5+
expiresAt: Date;
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum RoleType {
2+
Support = 'Support',
3+
Staff = 'Staff',
4+
Admin = 'Admin',
5+
System = 'System',
6+
}

src/lib/api/next/models/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './LoginOkResponse';
2+
export * from './OAuthFinalizeRequest';
3+
export * from './OAuthSignupData';
4+
export * from './RoleType';

src/lib/api/next/oauth.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { GetJson, PostJson, PostRedirect } from './base';
2+
import type { LoginOkResponse, OAuthFinalizeRequest, OAuthSignupData } from './models';
3+
import {
4+
TransformLoginOkResponse,
5+
TransformOAuthSignupData,
6+
ValidateStringArray,
7+
} from './transformers';
8+
9+
export function OAuthListProviders(): Promise<string[]> {
10+
return GetJson<string[]>('/1/oauth/providers', 200, ValidateStringArray);
11+
}
12+
13+
export function OAuthAuthorize(provider: string, flow: 'LoginOrCreate' | 'Link') {
14+
const providerEnc = encodeURIComponent(provider);
15+
const flowEnc = encodeURIComponent(flow);
16+
return PostRedirect(`/1/oauth/${providerEnc}/authorize?flow=${flowEnc}`, 302);
17+
}
18+
19+
export async function OAuthSignupGetData(provider: string) {
20+
const providerEnc = encodeURIComponent(provider);
21+
return GetJson<OAuthSignupData>(`/1/oauth/${providerEnc}/data`, 200, TransformOAuthSignupData);
22+
}
23+
24+
export async function OAuthSignupFinalize(
25+
provider: string,
26+
payload: OAuthFinalizeRequest
27+
): Promise<LoginOkResponse> {
28+
const providerEnc = encodeURIComponent(provider);
29+
return PostJson(`/1/oauth/${providerEnc}/finalize`, payload, 200, TransformLoginOkResponse);
30+
}

0 commit comments

Comments
 (0)