refactor progress
This commit is contained in:
424
packages/schema/src/domains/oauth/OAuthSchemas.tsx
Normal file
424
packages/schema/src/domains/oauth/OAuthSchemas.tsx
Normal file
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {ApplicationFlags, BotFlags, BotFlagsDescriptions} from '@fluxer/constants/src/BotConstants';
|
||||
import {AVATAR_MAX_SIZE} from '@fluxer/constants/src/LimitConstants';
|
||||
import {
|
||||
PublicUserFlags,
|
||||
PublicUserFlagsDescriptions,
|
||||
UserAuthenticatorTypes,
|
||||
UserAuthenticatorTypesDescriptions,
|
||||
} from '@fluxer/constants/src/UserConstants';
|
||||
import {createBase64StringType} from '@fluxer/schema/src/primitives/FileValidators';
|
||||
import {
|
||||
createBitflagInt32Type,
|
||||
createInt32EnumType,
|
||||
createNamedStringLiteralUnion,
|
||||
createStringType,
|
||||
Int32Type,
|
||||
SnowflakeStringType,
|
||||
SnowflakeType,
|
||||
withOpenApiType,
|
||||
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {DiscriminatorType, UsernameType} from '@fluxer/schema/src/primitives/UserValidators';
|
||||
import {z} from 'zod';
|
||||
|
||||
const RedirectURIString = createStringType(1).refine((value) => {
|
||||
try {
|
||||
const u = new URL(value);
|
||||
return !!u.protocol && !!u.host;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, 'Invalid URL format');
|
||||
|
||||
export const OAuthScopes = ['identify', 'email', 'guilds', 'bot'] as const;
|
||||
|
||||
export type OAuthScope = (typeof OAuthScopes)[number];
|
||||
|
||||
const AuthenticatorTypeEnum = withOpenApiType(
|
||||
createInt32EnumType(
|
||||
[
|
||||
[UserAuthenticatorTypes.TOTP, 'TOTP', UserAuthenticatorTypesDescriptions.TOTP],
|
||||
[UserAuthenticatorTypes.SMS, 'SMS', UserAuthenticatorTypesDescriptions.SMS],
|
||||
[UserAuthenticatorTypes.WEBAUTHN, 'WEBAUTHN', UserAuthenticatorTypesDescriptions.WEBAUTHN],
|
||||
],
|
||||
'The type of authenticator',
|
||||
'AuthenticatorType',
|
||||
),
|
||||
'AuthenticatorType',
|
||||
);
|
||||
|
||||
const PromptType = withOpenApiType(
|
||||
createNamedStringLiteralUnion(
|
||||
[
|
||||
['consent', 'CONSENT', 'Always prompt the user for consent'],
|
||||
['none', 'NONE', 'Do not prompt the user for consent if already authorized'],
|
||||
] as const,
|
||||
'Whether to prompt the user for consent',
|
||||
),
|
||||
'PromptType',
|
||||
);
|
||||
|
||||
const DisableGuildSelectType = withOpenApiType(
|
||||
createNamedStringLiteralUnion(
|
||||
[
|
||||
['true', 'TRUE', 'Disable guild selection'],
|
||||
['false', 'FALSE', 'Allow guild selection'],
|
||||
] as const,
|
||||
'Whether to disable guild selection',
|
||||
),
|
||||
'DisableGuildSelectType',
|
||||
);
|
||||
|
||||
export const AuthorizeRequest = z.object({
|
||||
response_type: z.literal('code').optional().describe('The OAuth2 response type, must be "code"'),
|
||||
client_id: SnowflakeType.describe('The application client ID'),
|
||||
redirect_uri: RedirectURIString.optional().describe('The URI to redirect to after authorization'),
|
||||
scope: createStringType(1).describe('The space-separated list of requested scopes'),
|
||||
state: createStringType(1).optional().describe('A random string for CSRF protection'),
|
||||
prompt: PromptType.optional(),
|
||||
guild_id: SnowflakeType.optional().describe('The guild ID to pre-select for bot authorization'),
|
||||
permissions: z.string().optional().describe('The bot permissions to request'),
|
||||
disable_guild_select: DisableGuildSelectType.optional(),
|
||||
});
|
||||
|
||||
export type AuthorizeRequest = z.infer<typeof AuthorizeRequest>;
|
||||
|
||||
export const AuthorizeConsentRequest = z.object({
|
||||
response_type: z.string().optional().describe('The OAuth2 response type'),
|
||||
client_id: SnowflakeType.describe('The application client ID'),
|
||||
redirect_uri: RedirectURIString.optional().describe('The URI to redirect to after authorization'),
|
||||
scope: createStringType(1).describe('The space-separated list of requested scopes'),
|
||||
state: createStringType(1).optional().describe('A random string for CSRF protection'),
|
||||
permissions: z.string().optional().describe('The bot permissions to request'),
|
||||
guild_id: SnowflakeType.optional().describe('The guild ID to add the bot to'),
|
||||
});
|
||||
|
||||
export type AuthorizeConsentRequest = z.infer<typeof AuthorizeConsentRequest>;
|
||||
|
||||
export const TokenRequest = z.discriminatedUnion('grant_type', [
|
||||
z.object({
|
||||
grant_type: z.literal('authorization_code').describe('The grant type for exchanging an authorization code'),
|
||||
code: createStringType(1).describe('The authorization code received from the authorize endpoint'),
|
||||
redirect_uri: RedirectURIString.describe('The redirect URI used in the authorization request'),
|
||||
client_id: SnowflakeType.optional().describe('The application client ID'),
|
||||
client_secret: createStringType(1).optional().describe('The application client secret'),
|
||||
}),
|
||||
z.object({
|
||||
grant_type: z.literal('refresh_token').describe('The grant type for refreshing an access token'),
|
||||
refresh_token: createStringType(1).describe('The refresh token to exchange for a new access token'),
|
||||
client_id: SnowflakeType.optional().describe('The application client ID'),
|
||||
client_secret: createStringType(1).optional().describe('The application client secret'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type TokenRequest = z.infer<typeof TokenRequest>;
|
||||
|
||||
export const IntrospectRequestForm = z.object({
|
||||
token: createStringType(1).describe('The token to introspect'),
|
||||
client_id: SnowflakeType.optional().describe('The application client ID'),
|
||||
client_secret: createStringType(1).optional().describe('The application client secret'),
|
||||
});
|
||||
|
||||
export type IntrospectRequestForm = z.infer<typeof IntrospectRequestForm>;
|
||||
|
||||
const TokenTypeHint = withOpenApiType(
|
||||
createNamedStringLiteralUnion(
|
||||
[
|
||||
['access_token', 'ACCESS_TOKEN', 'An OAuth2 access token'],
|
||||
['refresh_token', 'REFRESH_TOKEN', 'An OAuth2 refresh token'],
|
||||
] as const,
|
||||
'A hint about the type of token being revoked',
|
||||
),
|
||||
'TokenTypeHint',
|
||||
);
|
||||
|
||||
export const RevokeRequestForm = z.object({
|
||||
token: createStringType(1).describe('The token to revoke'),
|
||||
token_type_hint: TokenTypeHint.optional(),
|
||||
client_id: SnowflakeType.optional().describe('The application client ID'),
|
||||
client_secret: createStringType(1).optional().describe('The application client secret'),
|
||||
});
|
||||
|
||||
export type RevokeRequestForm = z.infer<typeof RevokeRequestForm>;
|
||||
|
||||
export const ApplicationBotResponse = z
|
||||
.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier of the bot user'),
|
||||
username: z.string().describe('The username of the bot'),
|
||||
discriminator: z.string().describe('The discriminator of the bot'),
|
||||
avatar: z.string().nullable().optional().describe('The avatar hash of the bot'),
|
||||
banner: z.string().nullable().optional().describe('The banner hash of the bot'),
|
||||
bio: z.string().nullable().describe('The bio or description of the bot'),
|
||||
token: z.string().optional().describe('The bot token for authentication'),
|
||||
mfa_enabled: z.boolean().optional().describe('Whether the bot has MFA enabled'),
|
||||
authenticator_types: z
|
||||
.array(AuthenticatorTypeEnum)
|
||||
.max(10)
|
||||
.optional()
|
||||
.describe('The types of authenticators enabled'),
|
||||
flags: createBitflagInt32Type(BotFlags, BotFlagsDescriptions, 'The bot user flags', 'BotFlags'),
|
||||
})
|
||||
.describe('Detailed bot user metadata');
|
||||
|
||||
export type ApplicationBotResponse = z.infer<typeof ApplicationBotResponse>;
|
||||
|
||||
export const ApplicationResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier of the application'),
|
||||
name: z.string().describe('The name of the application'),
|
||||
redirect_uris: z.array(z.string()).max(20).describe('The registered redirect URIs for OAuth2'),
|
||||
bot_public: z.boolean().describe('Whether the bot can be invited by anyone'),
|
||||
bot_require_code_grant: z.boolean().describe('Whether the bot requires OAuth2 code grant'),
|
||||
client_secret: z.string().optional().describe('The client secret for OAuth2 authentication'),
|
||||
bot: ApplicationBotResponse.optional().describe('The bot user associated with the application'),
|
||||
});
|
||||
|
||||
export type ApplicationResponse = z.infer<typeof ApplicationResponse>;
|
||||
|
||||
export const ApplicationListResponse = z.array(ApplicationResponse);
|
||||
|
||||
export type ApplicationListResponse = z.infer<typeof ApplicationListResponse>;
|
||||
|
||||
export const BotTokenResetResponse = z.object({
|
||||
token: z.string().describe('The new bot token'),
|
||||
bot: ApplicationBotResponse,
|
||||
});
|
||||
|
||||
export type BotTokenResetResponse = z.infer<typeof BotTokenResetResponse>;
|
||||
|
||||
export const BotProfileResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier of the bot user'),
|
||||
username: z.string().describe('The username of the bot'),
|
||||
discriminator: z.string().describe('The discriminator of the bot'),
|
||||
avatar: z.string().nullable().describe('The avatar hash of the bot'),
|
||||
banner: z.string().nullable().describe('The banner hash of the bot'),
|
||||
bio: z.string().nullable().describe('The bio or description of the bot'),
|
||||
});
|
||||
|
||||
export type BotProfileResponse = z.infer<typeof BotProfileResponse>;
|
||||
|
||||
export const OAuth2TokenResponse = z.object({
|
||||
access_token: z.string().describe('The access token for API authorization'),
|
||||
token_type: z.string().describe('The type of token, typically "Bearer"'),
|
||||
expires_in: Int32Type.describe('The number of seconds until the access token expires'),
|
||||
refresh_token: z.string().describe('The refresh token for obtaining new access tokens'),
|
||||
scope: z.string().describe('The space-separated list of granted scopes'),
|
||||
});
|
||||
|
||||
export type OAuth2TokenResponse = z.infer<typeof OAuth2TokenResponse>;
|
||||
|
||||
export const OAuth2UserInfoResponse = z.object({
|
||||
sub: SnowflakeStringType.describe('The subject identifier of the user'),
|
||||
id: SnowflakeStringType.describe('The unique identifier of the user'),
|
||||
username: z.string().describe('The username of the user'),
|
||||
discriminator: z.string().describe('The discriminator of the user'),
|
||||
global_name: z.string().nullable().describe('The global display name of the user'),
|
||||
avatar: z.string().nullable().describe('The avatar hash of the user'),
|
||||
email: z.string().nullable().optional().describe('The email address of the user'),
|
||||
verified: z.boolean().nullable().optional().describe('Whether the user has verified their email'),
|
||||
flags: createBitflagInt32Type(
|
||||
PublicUserFlags,
|
||||
PublicUserFlagsDescriptions,
|
||||
'The user flags',
|
||||
'PublicUserFlags',
|
||||
).optional(),
|
||||
});
|
||||
|
||||
export type OAuth2UserInfoResponse = z.infer<typeof OAuth2UserInfoResponse>;
|
||||
|
||||
export const OAuth2IntrospectResponse = z.object({
|
||||
active: z.boolean().describe('Whether the token is currently active'),
|
||||
scope: z.string().optional().describe('The space-separated list of scopes'),
|
||||
client_id: SnowflakeStringType.optional().describe('The client identifier for the token'),
|
||||
username: z.string().optional().describe('The username of the token owner'),
|
||||
token_type: z.string().optional().describe('The type of token'),
|
||||
exp: Int32Type.optional().describe('The expiration timestamp in seconds'),
|
||||
iat: Int32Type.optional().describe('The issued-at timestamp in seconds'),
|
||||
sub: SnowflakeStringType.optional().describe('The subject identifier (user ID)'),
|
||||
});
|
||||
|
||||
export type OAuth2IntrospectResponse = z.infer<typeof OAuth2IntrospectResponse>;
|
||||
|
||||
export const OAuth2ConsentResponse = z.object({
|
||||
redirect_to: z.string().describe('The URL to redirect the user to after consent'),
|
||||
});
|
||||
|
||||
export type OAuth2ConsentResponse = z.infer<typeof OAuth2ConsentResponse>;
|
||||
|
||||
export const OAuth2MeResponse = z.object({
|
||||
application: z
|
||||
.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier of the application'),
|
||||
name: z.string().describe('The name of the application'),
|
||||
icon: z.null().describe('The icon hash of the application'),
|
||||
description: z.null().describe('The description of the application'),
|
||||
bot_public: z.boolean().describe('Whether the bot can be invited by anyone'),
|
||||
bot_require_code_grant: z.boolean().describe('Whether the bot requires OAuth2 code grant'),
|
||||
flags: createBitflagInt32Type(ApplicationFlags, 'The application flags', undefined, 'ApplicationFlags'),
|
||||
})
|
||||
.describe('The application associated with the token'),
|
||||
scopes: z.array(z.string()).max(50).describe('The list of granted OAuth2 scopes'),
|
||||
expires: z.string().describe('The expiration timestamp of the token'),
|
||||
user: z
|
||||
.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier of the user'),
|
||||
username: z.string().describe('The username of the user'),
|
||||
discriminator: z.string().describe('The discriminator of the user'),
|
||||
global_name: z.string().nullable().describe('The global display name of the user'),
|
||||
avatar: z.string().nullable().describe('The avatar hash of the user'),
|
||||
avatar_color: Int32Type.nullable().describe('The default avatar color of the user'),
|
||||
bot: z.boolean().optional().describe('Whether the user is a bot'),
|
||||
system: z.boolean().optional().describe('Whether the user is a system user'),
|
||||
flags: createBitflagInt32Type(PublicUserFlags, PublicUserFlagsDescriptions, 'The user flags', 'PublicUserFlags'),
|
||||
email: z.string().nullable().optional().describe('The email address of the user'),
|
||||
verified: z.boolean().nullable().optional().describe('Whether the user has verified their email'),
|
||||
})
|
||||
.optional()
|
||||
.describe('The user associated with the token'),
|
||||
});
|
||||
|
||||
export type OAuth2MeResponse = z.infer<typeof OAuth2MeResponse>;
|
||||
|
||||
export const ApplicationPublicResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier of the application'),
|
||||
name: z.string().describe('The name of the application'),
|
||||
icon: z.string().nullable().describe('The icon hash of the application'),
|
||||
description: z.null().describe('The description of the application'),
|
||||
redirect_uris: z.array(z.string()).max(20).describe('The registered redirect URIs for OAuth2'),
|
||||
scopes: z.array(z.string()).max(50).describe('The available OAuth2 scopes'),
|
||||
bot_public: z.boolean().describe('Whether the bot can be invited by anyone'),
|
||||
bot: ApplicationBotResponse.nullable().describe('The bot user associated with the application'),
|
||||
});
|
||||
|
||||
export type ApplicationPublicResponse = z.infer<typeof ApplicationPublicResponse>;
|
||||
|
||||
export const ApplicationsMeResponse = z.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier of the application'),
|
||||
name: z.string().describe('The name of the application'),
|
||||
icon: z.null().describe('The icon hash of the application'),
|
||||
description: z.null().describe('The description of the application'),
|
||||
bot_public: z.boolean().describe('Whether the bot can be invited by anyone'),
|
||||
bot_require_code_grant: z.boolean().describe('Whether the bot requires OAuth2 code grant'),
|
||||
flags: createBitflagInt32Type(ApplicationFlags, 'The application flags', undefined, 'ApplicationFlags'),
|
||||
bot: ApplicationBotResponse.optional().describe('The bot user associated with the application'),
|
||||
});
|
||||
|
||||
export type ApplicationsMeResponse = z.infer<typeof ApplicationsMeResponse>;
|
||||
|
||||
export const OAuth2AuthorizationResponse = z.object({
|
||||
application: z
|
||||
.object({
|
||||
id: SnowflakeStringType.describe('The unique identifier of the application'),
|
||||
name: z.string().describe('The name of the application'),
|
||||
icon: z.string().nullable().describe('The icon hash of the application'),
|
||||
description: z.null().describe('The description of the application'),
|
||||
bot_public: z.boolean().describe('Whether the bot can be invited by anyone'),
|
||||
})
|
||||
.describe('The application that was authorized'),
|
||||
scopes: z.array(z.string()).max(50).describe('The list of granted OAuth2 scopes'),
|
||||
authorized_at: z.string().describe('The timestamp when the authorization was granted'),
|
||||
});
|
||||
|
||||
export type OAuth2AuthorizationResponse = z.infer<typeof OAuth2AuthorizationResponse>;
|
||||
|
||||
export const OAuth2AuthorizationsListResponse = z.array(OAuth2AuthorizationResponse);
|
||||
|
||||
export type OAuth2AuthorizationsListResponse = z.infer<typeof OAuth2AuthorizationsListResponse>;
|
||||
|
||||
function isLoopbackHost(hostname: string) {
|
||||
const lowercaseHost = hostname.toLowerCase();
|
||||
return (
|
||||
lowercaseHost === 'localhost' ||
|
||||
lowercaseHost === '127.0.0.1' ||
|
||||
lowercaseHost === '[::1]' ||
|
||||
lowercaseHost.endsWith('.localhost')
|
||||
);
|
||||
}
|
||||
|
||||
function isValidRedirectURI(value: string, allowAnyHttp: boolean) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!allowAnyHttp && url.protocol === 'http:' && !isLoopbackHost(url.hostname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!url.host;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const createRedirectURIType = (allowAnyHttp: boolean, message: string) =>
|
||||
createStringType(1).refine((value) => isValidRedirectURI(value, allowAnyHttp), message);
|
||||
|
||||
export const OAuth2RedirectURICreateType = createRedirectURIType(
|
||||
false,
|
||||
'Redirect URIs must use HTTPS, or HTTP for localhost only',
|
||||
);
|
||||
export const OAuth2RedirectURIUpdateType = createRedirectURIType(true, 'Redirect URIs must use HTTP or HTTPS');
|
||||
|
||||
export const ApplicationCreateRequest = z.object({
|
||||
name: createStringType(1, 100).describe('The name of the application'),
|
||||
redirect_uris: z
|
||||
.array(OAuth2RedirectURICreateType)
|
||||
.max(10, 'Maximum of 10 redirect URIs allowed')
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((value) => value ?? [])
|
||||
.describe('The redirect URIs for OAuth2 flows'),
|
||||
bot_public: z.boolean().optional().describe('Whether the bot can be invited by anyone'),
|
||||
bot_require_code_grant: z.boolean().optional().describe('Whether the bot requires OAuth2 code grant'),
|
||||
});
|
||||
|
||||
export type ApplicationCreateRequest = z.infer<typeof ApplicationCreateRequest>;
|
||||
|
||||
export const ApplicationUpdateRequest = z.object({
|
||||
name: createStringType(1, 100).optional().describe('The name of the application'),
|
||||
redirect_uris: z
|
||||
.array(OAuth2RedirectURIUpdateType)
|
||||
.max(10, 'Maximum of 10 redirect URIs allowed')
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((value) => (value === undefined ? undefined : (value ?? [])))
|
||||
.describe('The redirect URIs for OAuth2 flows'),
|
||||
bot_public: z.boolean().optional().describe('Whether the bot can be invited by anyone'),
|
||||
bot_require_code_grant: z.boolean().optional().describe('Whether the bot requires OAuth2 code grant'),
|
||||
});
|
||||
|
||||
export type ApplicationUpdateRequest = z.infer<typeof ApplicationUpdateRequest>;
|
||||
|
||||
export const BotProfileUpdateRequest = z.object({
|
||||
username: UsernameType.optional().describe('The username of the bot'),
|
||||
discriminator: DiscriminatorType.optional().describe('The discriminator of the bot'),
|
||||
avatar: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3)))
|
||||
.nullish()
|
||||
.describe('The avatar image as base64'),
|
||||
banner: createBase64StringType(1, Math.ceil(AVATAR_MAX_SIZE * (4 / 3)))
|
||||
.nullish()
|
||||
.describe('The banner image as base64'),
|
||||
bio: createStringType(0, 1024).nullish().describe('The bio or description of the bot'),
|
||||
bot_flags: createBitflagInt32Type(BotFlags, BotFlagsDescriptions, 'The bot user flags', 'BotFlags').optional(),
|
||||
});
|
||||
|
||||
export type BotProfileUpdateRequest = z.infer<typeof BotProfileUpdateRequest>;
|
||||
Reference in New Issue
Block a user