diff --git a/.env.example b/.env.example index 2a47bc9..b1fc68c 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,13 @@ API_URL=http://localhost:3000/api #AUTH_GITHUB_CLIENT_SECRET= # Enable login with GitHub #AUTH_GITHUB_ENABLED=true +# Google Admin IDs (comma-separated) +#AUTH_GOOGLE_ADMIN_IDS= +# Google OAuth +#AUTH_GOOGLE_CLIENT_ID= +#AUTH_GOOGLE_CLIENT_SECRET= +# Enable login with Google +#AUTH_GOOGLE_ENABLED=true # Twitter Admin IDs (comma-separated) #AUTH_TWITTER_ADMIN_IDS=386584531353862154 # Twitter OAuth2 Consumer Key and Consumer Secret diff --git a/api-schema.graphql b/api-schema.graphql index c80b631..adfba83 100644 --- a/api-schema.graphql +++ b/api-schema.graphql @@ -38,6 +38,7 @@ input AdminUpdateUserInput { type AppConfig { authDiscordEnabled: Boolean! authGithubEnabled: Boolean! + authGoogleEnabled: Boolean! authPasswordEnabled: Boolean! authRegisterEnabled: Boolean! authSolanaEnabled: Boolean! @@ -80,6 +81,7 @@ type IdentityChallenge { enum IdentityProvider { Discord GitHub + Google Solana Twitter } diff --git a/libs/api/auth/data-access/src/index.ts b/libs/api/auth/data-access/src/index.ts index f5282f8..912d586 100644 --- a/libs/api/auth/data-access/src/index.ts +++ b/libs/api/auth/data-access/src/index.ts @@ -9,4 +9,5 @@ export * from './lib/guards/api-auth-graphql-user-guard' export * from './lib/interfaces/api-auth.request' export * from './lib/strategies/oauth/api-auth-strategy-discord-guard' export * from './lib/strategies/oauth/api-auth-strategy-github-guard' +export * from './lib/strategies/oauth/api-auth-strategy-google-guard' export * from './lib/strategies/oauth/api-auth-strategy-twitter-guard' diff --git a/libs/api/auth/data-access/src/lib/strategies/api-auth-strategy.module.ts b/libs/api/auth/data-access/src/lib/strategies/api-auth-strategy.module.ts index 3640052..cfa04ed 100644 --- a/libs/api/auth/data-access/src/lib/strategies/api-auth-strategy.module.ts +++ b/libs/api/auth/data-access/src/lib/strategies/api-auth-strategy.module.ts @@ -2,6 +2,7 @@ import { type DynamicModule, Module } from '@nestjs/common' import { ApiAuthStrategyDiscordModule } from './oauth/api-auth-strategy-discord.module' import { ApiAuthStrategyGithubModule } from './oauth/api-auth-strategy-github.module' +import { ApiAuthStrategyGoogleModule } from './oauth/api-auth-strategy-google.module' import { ApiAuthStrategyTwitterModule } from './oauth/api-auth-strategy-twitter.module' @Module({}) @@ -12,6 +13,7 @@ export class ApiAuthStrategyModule { imports: [ ApiAuthStrategyDiscordModule.register(), ApiAuthStrategyGithubModule.register(), + ApiAuthStrategyGoogleModule.register(), ApiAuthStrategyTwitterModule.register(), ], } diff --git a/libs/api/auth/data-access/src/lib/strategies/oauth/api-auth-strategy-google-guard.ts b/libs/api/auth/data-access/src/lib/strategies/oauth/api-auth-strategy-google-guard.ts new file mode 100644 index 0000000..507b75e --- /dev/null +++ b/libs/api/auth/data-access/src/lib/strategies/oauth/api-auth-strategy-google-guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common' +import { AuthGuard } from '@nestjs/passport' + +@Injectable() +export class ApiAuthStrategyGoogleGuard extends AuthGuard('google') {} diff --git a/libs/api/auth/data-access/src/lib/strategies/oauth/api-auth-strategy-google.module.ts b/libs/api/auth/data-access/src/lib/strategies/oauth/api-auth-strategy-google.module.ts new file mode 100644 index 0000000..d590555 --- /dev/null +++ b/libs/api/auth/data-access/src/lib/strategies/oauth/api-auth-strategy-google.module.ts @@ -0,0 +1,33 @@ +import { type DynamicModule, Logger, Module } from '@nestjs/common' +import { ApiCoreDataAccessModule } from '@pubkey-stack/api-core-data-access' +import { ApiAuthStrategyService } from '../api-auth-strategy.service' +import { ApiAuthStrategyGoogle } from './api-auth-strategy-google' + +@Module({}) +export class ApiAuthStrategyGoogleModule { + static logger = new Logger(ApiAuthStrategyGoogleModule.name) + static register(): DynamicModule { + const enabled = this.enabled + if (!enabled) { + this.logger.warn(`Google Auth DISABLED`) + return { module: ApiAuthStrategyGoogleModule } + } + this.logger.verbose(`Google Auth ENABLED`) + return { + module: ApiAuthStrategyGoogleModule, + imports: [ApiCoreDataAccessModule], + providers: [ApiAuthStrategyGoogle, ApiAuthStrategyService], + } + } + + // TODO: These should be coming from the ApiCoreConfigService instead of process.env + private static get enabled(): boolean { + return ( + // Google auth needs to be enabled + !!process.env['AUTH_GOOGLE_ENABLED'] && + // And we need to have the client ID and secret set + !!process.env['AUTH_GOOGLE_CLIENT_ID'] && + !!process.env['AUTH_GOOGLE_CLIENT_SECRET'] + ) + } +} diff --git a/libs/api/auth/data-access/src/lib/strategies/oauth/api-auth-strategy-google.ts b/libs/api/auth/data-access/src/lib/strategies/oauth/api-auth-strategy-google.ts new file mode 100644 index 0000000..ac23ea8 --- /dev/null +++ b/libs/api/auth/data-access/src/lib/strategies/oauth/api-auth-strategy-google.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common' +import { PassportStrategy } from '@nestjs/passport' +import { IdentityProvider } from '@prisma/client' +import { ApiCoreService } from '@pubkey-stack/api-core-data-access' +import { Profile, Strategy } from 'passport-google-oauth20' +import type { ApiAuthRequest } from '../../interfaces/api-auth.request' +import { ApiAuthStrategyService } from '../api-auth-strategy.service' + +@Injectable() +export class ApiAuthStrategyGoogle extends PassportStrategy(Strategy, 'google') { + constructor(private core: ApiCoreService, private service: ApiAuthStrategyService) { + super(core.config.authGoogleStrategyOptions) + } + + async validate(req: ApiAuthRequest, accessToken: string, refreshToken: string, profile: Profile) { + return this.service.validateRequest({ + req, + providerId: profile.id, + provider: IdentityProvider.Google, + accessToken, + refreshToken, + profile: createGoogleProfile(profile), + }) + } +} + +function createGoogleProfile(profile: Profile) { + return { + externalId: profile.id, + username: profile.username, + avatarUrl: profile.photos?.[0].value, + name: profile.displayName, + } +} diff --git a/libs/api/auth/feature/src/lib/api-auth-feature.module.ts b/libs/api/auth/feature/src/lib/api-auth-feature.module.ts index ad9c59a..7a40861 100644 --- a/libs/api/auth/feature/src/lib/api-auth-feature.module.ts +++ b/libs/api/auth/feature/src/lib/api-auth-feature.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common' import { ApiAuthDataAccessModule } from '@pubkey-stack/api-auth-data-access' import { ApiAuthStrategyDiscordController } from './api-auth-strategy-discord.controller' import { ApiAuthStrategyGithubController } from './api-auth-strategy-github.controller' +import { ApiAuthStrategyGoogleController } from './api-auth-strategy-google.controller' import { ApiAuthStrategyTwitterController } from './api-auth-strategy-twitter.controller' import { ApiAuthController } from './api-auth.controller' import { ApiAuthResolver } from './api-auth.resolver' @@ -11,6 +12,7 @@ import { ApiAuthResolver } from './api-auth.resolver' ApiAuthController, ApiAuthStrategyDiscordController, ApiAuthStrategyGithubController, + ApiAuthStrategyGoogleController, ApiAuthStrategyTwitterController, ], imports: [ApiAuthDataAccessModule], diff --git a/libs/api/auth/feature/src/lib/api-auth-strategy-google.controller.ts b/libs/api/auth/feature/src/lib/api-auth-strategy-google.controller.ts new file mode 100644 index 0000000..534b07f --- /dev/null +++ b/libs/api/auth/feature/src/lib/api-auth-strategy-google.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common' + +import { + ApiAnonJwtGuard, + ApiAuthRequest, + ApiAuthService, + ApiAuthStrategyGoogleGuard, +} from '@pubkey-stack/api-auth-data-access' +import { Response } from 'express-serve-static-core' + +@Controller('auth/google') +export class ApiAuthStrategyGoogleController { + constructor(private readonly service: ApiAuthService) {} + + @Get() + @UseGuards(ApiAuthStrategyGoogleGuard) + redirect() { + // This method triggers the OAuth2 flow + } + + @Get('callback') + @UseGuards(ApiAnonJwtGuard, ApiAuthStrategyGoogleGuard) + async callback(@Req() req: ApiAuthRequest, @Res({ passthrough: true }) res: Response) { + return this.service.userCookieRedirect(req, res) + } +} diff --git a/libs/api/core/data-access/src/lib/api-core-config.service.ts b/libs/api/core/data-access/src/lib/api-core-config.service.ts index 94e665e..e2524f6 100644 --- a/libs/api/core/data-access/src/lib/api-core-config.service.ts +++ b/libs/api/core/data-access/src/lib/api-core-config.service.ts @@ -18,6 +18,7 @@ export class ApiCoreConfigService { return { authDiscordEnabled: this.authDiscordEnabled, authGithubEnabled: this.authGithubEnabled, + authGoogleEnabled: this.authGoogleEnabled, authPasswordEnabled: this.authPasswordEnabled, authRegisterEnabled: this.authRegisterEnabled, authSolanaEnabled: this.authSolanaEnabled, @@ -92,6 +93,41 @@ export class ApiCoreConfigService { !this.service.get('authGithubEnabled') ) } + + get authGoogleAdminIds() { + return this.service.get('authGoogleAdminIds') + } + + get authGoogleClientId() { + return this.service.get('authGoogleClientId') + } + + get authGoogleClientSecret() { + return this.service.get('authGoogleClientSecret') + } + + get authGoogleScope(): string[] { + return ['email', 'profile'] + } + + get authGoogleStrategyOptions() { + return { + clientID: this.authGoogleClientId, + clientSecret: this.authGoogleClientSecret, + callbackURL: this.webUrl + '/api/auth/google/callback', + scope: this.authGoogleScope, + passReqToCallback: true, + } + } + + get authGoogleEnabled(): boolean { + return !( + !this.authGoogleClientId || + !this.authGoogleClientSecret || + !this.service.get('authGoogleEnabled') + ) + } + get authTwitterAdminIds() { return this.service.get('authTwitterAdminIds') } @@ -212,6 +248,8 @@ export class ApiCoreConfigService { return this.authDiscordAdminIds?.includes(providerId) ?? false case IdentityProvider.GitHub: return this.authGithubAdminIds?.includes(providerId) ?? false + case IdentityProvider.Google: + return this.authGoogleAdminIds?.includes(providerId) ?? false case IdentityProvider.Solana: return this.authSolanaAdminIds?.includes(providerId) ?? false case IdentityProvider.Twitter: diff --git a/libs/api/core/data-access/src/lib/config/configuration.ts b/libs/api/core/data-access/src/lib/config/configuration.ts index d9c6740..f015d1f 100644 --- a/libs/api/core/data-access/src/lib/config/configuration.ts +++ b/libs/api/core/data-access/src/lib/config/configuration.ts @@ -30,6 +30,11 @@ export interface ApiCoreConfig { authGithubClientId: string authGithubClientSecret: string authGithubEnabled: boolean + // Google Authentication + authGoogleAdminIds: string[] + authGoogleClientId: string + authGoogleClientSecret: string + authGoogleEnabled: boolean // Twitter Authentication authTwitterAdminIds: string[] authTwitterConsumerKey: string @@ -71,6 +76,10 @@ export function configuration(): ApiCoreConfig { authGithubClientId: process.env['AUTH_GITHUB_CLIENT_ID'] as string, authGithubClientSecret: process.env['AUTH_GITHUB_CLIENT_SECRET'] as string, authGithubEnabled: process.env['AUTH_GITHUB_ENABLED'] === 'true', + authGoogleAdminIds: getFromEnvironment('AUTH_GOOGLE_ADMIN_IDS'), + authGoogleClientId: process.env['AUTH_GOOGLE_CLIENT_ID'] as string, + authGoogleClientSecret: process.env['AUTH_GOOGLE_CLIENT_SECRET'] as string, + authGoogleEnabled: process.env['AUTH_GOOGLE_ENABLED'] === 'true', authTwitterAdminIds: getFromEnvironment('AUTH_TWITTER_ADMIN_IDS'), authTwitterConsumerKey: process.env['AUTH_TWITTER_CONSUMER_KEY'] as string, authTwitterConsumerSecret: process.env['AUTH_TWITTER_CONSUMER_SECRET'] as string, diff --git a/libs/api/core/data-access/src/lib/config/validation-schema.ts b/libs/api/core/data-access/src/lib/config/validation-schema.ts index 949fb50..63749e5 100644 --- a/libs/api/core/data-access/src/lib/config/validation-schema.ts +++ b/libs/api/core/data-access/src/lib/config/validation-schema.ts @@ -6,17 +6,22 @@ export const validationSchema = Joi.object({ AUTH_DISCORD_ADMIN_IDS: Joi.string(), AUTH_DISCORD_CLIENT_ID: Joi.string(), AUTH_DISCORD_CLIENT_SECRET: Joi.string(), - AUTH_DISCORD_ENABLED: Joi.boolean().default(true), // Client ID and Client Secret are also required + AUTH_DISCORD_ENABLED: Joi.boolean().default(true), // GitHub Authentication AUTH_GITHUB_ADMIN_IDS: Joi.string(), AUTH_GITHUB_CLIENT_ID: Joi.string(), AUTH_GITHUB_CLIENT_SECRET: Joi.string(), - AUTH_GITHUB_ENABLED: Joi.boolean().default(true), // Client ID and Client Secret are also required - // GitHub Authentication + AUTH_GITHUB_ENABLED: Joi.boolean().default(true), + // Google Authentication + AUTH_GOOGLE_ADMIN_IDS: Joi.string(), + AUTH_GOOGLE_CLIENT_ID: Joi.string(), + AUTH_GOOGLE_CLIENT_SECRET: Joi.string(), + AUTH_GOOGLE_ENABLED: Joi.boolean().default(true), + // Twitter Authentication AUTH_TWITTER_ADMIN_IDS: Joi.string(), AUTH_TWITTER_CONSUMER_KEY: Joi.string(), AUTH_TWITTER_CONSUMER_SECRET: Joi.string(), - AUTH_TWITTER_ENABLED: Joi.boolean().default(true), // Client ID and Client Secret are also required + AUTH_TWITTER_ENABLED: Joi.boolean().default(true), // Username and Password Authentication AUTH_PASSWORD_ENABLED: Joi.boolean().default(true), AUTH_REGISTER_ENABLED: Joi.boolean().default(true), diff --git a/libs/api/core/data-access/src/lib/entity/app-config.entity.ts b/libs/api/core/data-access/src/lib/entity/app-config.entity.ts index e84f15e..d0b3acd 100644 --- a/libs/api/core/data-access/src/lib/entity/app-config.entity.ts +++ b/libs/api/core/data-access/src/lib/entity/app-config.entity.ts @@ -7,6 +7,8 @@ export class AppConfig { @Field() authGithubEnabled!: boolean @Field() + authGoogleEnabled!: boolean + @Field() authPasswordEnabled!: boolean @Field() authRegisterEnabled!: boolean diff --git a/libs/sdk/src/generated/graphql-sdk.ts b/libs/sdk/src/generated/graphql-sdk.ts index 1f8ff77..5dc5d1f 100644 --- a/libs/sdk/src/generated/graphql-sdk.ts +++ b/libs/sdk/src/generated/graphql-sdk.ts @@ -61,6 +61,7 @@ export type AppConfig = { __typename?: 'AppConfig' authDiscordEnabled: Scalars['Boolean']['output'] authGithubEnabled: Scalars['Boolean']['output'] + authGoogleEnabled: Scalars['Boolean']['output'] authPasswordEnabled: Scalars['Boolean']['output'] authRegisterEnabled: Scalars['Boolean']['output'] authSolanaEnabled: Scalars['Boolean']['output'] @@ -100,6 +101,7 @@ export type IdentityChallenge = { export enum IdentityProvider { Discord = 'Discord', GitHub = 'GitHub', + Google = 'Google', Solana = 'Solana', Twitter = 'Twitter', } @@ -373,6 +375,7 @@ export type AppConfigDetailsFragment = { __typename?: 'AppConfig' authDiscordEnabled: boolean authGithubEnabled: boolean + authGoogleEnabled: boolean authPasswordEnabled: boolean authRegisterEnabled: boolean authSolanaEnabled: boolean @@ -402,6 +405,7 @@ export type AppConfigQuery = { __typename?: 'AppConfig' authDiscordEnabled: boolean authGithubEnabled: boolean + authGoogleEnabled: boolean authPasswordEnabled: boolean authRegisterEnabled: boolean authSolanaEnabled: boolean @@ -854,6 +858,7 @@ export const AppConfigDetailsFragmentDoc = gql` fragment AppConfigDetails on AppConfig { authDiscordEnabled authGithubEnabled + authGoogleEnabled authPasswordEnabled authRegisterEnabled authSolanaEnabled diff --git a/libs/sdk/src/graphql/feature-core.graphql b/libs/sdk/src/graphql/feature-core.graphql index 07e9d23..630a052 100644 --- a/libs/sdk/src/graphql/feature-core.graphql +++ b/libs/sdk/src/graphql/feature-core.graphql @@ -1,6 +1,7 @@ fragment AppConfigDetails on AppConfig { authDiscordEnabled authGithubEnabled + authGoogleEnabled authPasswordEnabled authRegisterEnabled authSolanaEnabled diff --git a/libs/web/auth/data-access/src/lib/auth.provider.tsx b/libs/web/auth/data-access/src/lib/auth.provider.tsx index 21ca2ff..af913d3 100644 --- a/libs/web/auth/data-access/src/lib/auth.provider.tsx +++ b/libs/web/auth/data-access/src/lib/auth.provider.tsx @@ -1,8 +1,8 @@ -import { AppConfig, LoginInput, RegisterInput, User } from '@pubkey-stack/sdk' +import { AppConfig, IdentityProvider, LoginInput, RegisterInput, User } from '@pubkey-stack/sdk' import { useSdk } from '@pubkey-stack/web-core-data-access' import { toastError, toastSuccess } from '@pubkey-ui/core' import { useQuery } from '@tanstack/react-query' -import { createContext, ReactNode, useContext, useEffect, useReducer } from 'react' +import { createContext, ReactNode, useContext, useEffect, useMemo, useReducer } from 'react' import { useMe } from './use-me' type AuthStatus = 'authenticated' | 'unauthenticated' | 'loading' | 'error' @@ -17,7 +17,9 @@ export interface AuthProviderContext extends AuthState { appConfig?: AppConfig | undefined appConfigLoading: boolean authenticated: boolean + authEnabled: boolean developer: boolean + enabledProviders: IdentityProvider[] error?: unknown | undefined loading: boolean login: (input: LoginInput) => Promise @@ -90,11 +92,49 @@ export function AuthProvider({ children }: { children: ReactNode }) { dispatchUser(me.data?.me) }, [me.isLoading, me.data?.me]) + const authEnabled = useMemo(() => { + if (!configQuery.data?.config) return false + const { + authDiscordEnabled, + authGithubEnabled, + authGoogleEnabled, + authPasswordEnabled, + authRegisterEnabled, + authSolanaEnabled, + authTwitterEnabled, + } = configQuery.data.config + return ( + authDiscordEnabled || + authGithubEnabled || + authGoogleEnabled || + authRegisterEnabled || + authPasswordEnabled || + authSolanaEnabled || + authTwitterEnabled + ) + }, [configQuery.data?.config]) + + const enabledProviders: IdentityProvider[] = useMemo( + () => + configQuery.data?.config + ? ([ + configQuery.data?.config.authDiscordEnabled && IdentityProvider.Discord, + configQuery.data?.config.authGithubEnabled && IdentityProvider.GitHub, + configQuery.data?.config.authGoogleEnabled && IdentityProvider.Google, + configQuery.data?.config.authSolanaEnabled && IdentityProvider.Solana, + configQuery.data?.config.authTwitterEnabled && IdentityProvider.Twitter, + ].filter(Boolean) as IdentityProvider[]) + : [], + [configQuery.data?.config], + ) + const value: AuthProviderContext = { appConfig: configQuery.data?.config, appConfigLoading: configQuery.isLoading, + authEnabled, authenticated: state.status === 'authenticated', developer: state.user?.developer ?? false, + enabledProviders, error: state.error, user: state.user, status: state.status, diff --git a/libs/web/auth/feature/src/lib/auth-login-feature.tsx b/libs/web/auth/feature/src/lib/auth-login-feature.tsx index fbd1fe3..4946eda 100644 --- a/libs/web/auth/feature/src/lib/auth-login-feature.tsx +++ b/libs/web/auth/feature/src/lib/auth-login-feature.tsx @@ -7,7 +7,7 @@ import { useState } from 'react' import { Link, useLocation, useNavigate } from 'react-router-dom' export default function AuthLoginFeature() { - const { login, logout, refresh, user, appConfig, appConfigLoading } = useAuth() + const { login, logout, refresh, user, appConfig, appConfigLoading, authEnabled, enabledProviders } = useAuth() const navigate = useNavigate() const location = useLocation() const [loading, setLoading] = useState(false) @@ -33,7 +33,8 @@ export default function AuthLoginFeature() { return ( refresh().then((res) => { if (res) { diff --git a/libs/web/auth/feature/src/lib/auth-register-feature.tsx b/libs/web/auth/feature/src/lib/auth-register-feature.tsx index 21241b9..bc70d59 100644 --- a/libs/web/auth/feature/src/lib/auth-register-feature.tsx +++ b/libs/web/auth/feature/src/lib/auth-register-feature.tsx @@ -7,7 +7,7 @@ import { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' export default function AuthRegisterFeature() { - const { logout, refresh, register, appConfig, appConfigLoading, user } = useAuth() + const { logout, refresh, register, appConfig, appConfigLoading, user, enabledProviders, authEnabled } = useAuth() const navigate = useNavigate() const [loading, setLoading] = useState(false) @@ -37,7 +37,8 @@ export default function AuthRegisterFeature() { return ( refresh().then((res) => { if (res) { diff --git a/libs/web/auth/ui/src/lib/auth-ui-enabled.tsx b/libs/web/auth/ui/src/lib/auth-ui-enabled.tsx index db15b32..45e8258 100644 --- a/libs/web/auth/ui/src/lib/auth-ui-enabled.tsx +++ b/libs/web/auth/ui/src/lib/auth-ui-enabled.tsx @@ -1,26 +1,8 @@ import { Group, Title } from '@mantine/core' -import type { AppConfig } from '@pubkey-stack/sdk' import { ReactNode } from 'react' -export function AuthUiEnabled({ appConfig, children }: { appConfig: AppConfig; children: ReactNode }) { - const { - authDiscordEnabled, - authGithubEnabled, - authPasswordEnabled, - authRegisterEnabled, - authSolanaEnabled, - authTwitterEnabled, - } = appConfig - - const enabled = - authDiscordEnabled || - authGithubEnabled || - authRegisterEnabled || - authPasswordEnabled || - authSolanaEnabled || - authTwitterEnabled - - return enabled ? ( +export function AuthUiEnabled({ authEnabled, children }: { authEnabled: boolean; children: ReactNode }) { + return authEnabled ? ( children ) : ( diff --git a/libs/web/auth/ui/src/lib/auth-ui-page.tsx b/libs/web/auth/ui/src/lib/auth-ui-page.tsx index ba1417a..8ffc560 100644 --- a/libs/web/auth/ui/src/lib/auth-ui-page.tsx +++ b/libs/web/auth/ui/src/lib/auth-ui-page.tsx @@ -1,14 +1,13 @@ import { Box, Group } from '@mantine/core' -import type { AppConfig } from '@pubkey-stack/sdk' import { UiLogoType, UiStack } from '@pubkey-ui/core' import { ReactNode } from 'react' import { AuthUiEnabled } from './auth-ui-enabled' import { AuthUiFull } from './auth-ui-full' -export function AuthUiPage({ appConfig, children }: { appConfig: AppConfig; children: ReactNode }) { +export function AuthUiPage({ authEnabled, children }: { authEnabled: boolean; children: ReactNode }) { return ( - + diff --git a/libs/web/auth/ui/src/lib/auth-ui-shell.tsx b/libs/web/auth/ui/src/lib/auth-ui-shell.tsx index 3e558f4..c44c77c 100644 --- a/libs/web/auth/ui/src/lib/auth-ui-shell.tsx +++ b/libs/web/auth/ui/src/lib/auth-ui-shell.tsx @@ -1,5 +1,5 @@ import { Button } from '@mantine/core' -import type { AppConfig, User } from '@pubkey-stack/sdk' +import { IdentityProvider, type User } from '@pubkey-stack/sdk' import { IdentityUiLoginButtons } from '@pubkey-stack/web-identity-ui' import { UserUiAvatar } from '@pubkey-stack/web-user-ui' import { UiStack } from '@pubkey-ui/core' @@ -8,16 +8,18 @@ import { ReactNode } from 'react' import { AuthUiPage } from './auth-ui-page' export function AuthUiShell({ - appConfig, + authEnabled, children, + enabledProviders, loading, logout, navigate, refresh, user, }: { - appConfig: AppConfig + authEnabled: boolean children: ReactNode + enabledProviders: IdentityProvider[] loading: boolean logout: () => Promise navigate: () => void @@ -25,7 +27,7 @@ export function AuthUiShell({ user?: User }) { return ( - + {user ? ( - ) -} diff --git a/libs/web/identity/ui/src/lib/identity-ui-github-link-button.tsx b/libs/web/identity/ui/src/lib/identity-ui-github-link-button.tsx deleted file mode 100644 index ce9f090..0000000 --- a/libs/web/identity/ui/src/lib/identity-ui-github-link-button.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Button, ButtonProps } from '@mantine/core' -import { IdentityProvider } from '@pubkey-stack/sdk' -import { IdentityUiIcon } from './identity-ui-icon' - -export function IdentityUiGithubLinkButton({ ...props }: ButtonProps) { - return ( - - ) -} - -export function IdentityUiTwitterLinkButton({ ...props }: ButtonProps) { - return ( - - ) -} diff --git a/libs/web/identity/ui/src/lib/identity-ui-group-list.tsx b/libs/web/identity/ui/src/lib/identity-ui-group-list.tsx index 7f48027..e27d7c3 100644 --- a/libs/web/identity/ui/src/lib/identity-ui-group-list.tsx +++ b/libs/web/identity/ui/src/lib/identity-ui-group-list.tsx @@ -1,11 +1,10 @@ import { Group, Text } from '@mantine/core' import { Identity, IdentityProvider } from '@pubkey-stack/sdk' import { UiStack } from '@pubkey-ui/core' -import { IdentityUiDiscordLinkButton } from './identity-ui-discord-link-button' -import { IdentityUiGithubLinkButton, IdentityUiTwitterLinkButton } from './identity-ui-github-link-button' import { IdentityUiIcon } from './identity-ui-icon' + +import { IdentityUiLinkButton } from './identity-ui-link-button' import { IdentityUiList } from './identity-ui-list' -import { IdentityUiSolanaLinkButton } from './identity-ui-solana-link-button' export function IdentityUiGroupList({ deleteIdentity, @@ -25,12 +24,13 @@ export function IdentityUiGroupList({ {group.provider} - {group.provider === IdentityProvider.Discord && } - {group.provider === IdentityProvider.GitHub && } - {refresh && group.provider === IdentityProvider.Solana && ( - - )} - {group.provider === IdentityProvider.Twitter && } + {group.items?.length ? ( diff --git a/libs/web/identity/ui/src/lib/identity-ui-icon.tsx b/libs/web/identity/ui/src/lib/identity-ui-icon.tsx index 436cdbe..2915180 100644 --- a/libs/web/identity/ui/src/lib/identity-ui-icon.tsx +++ b/libs/web/identity/ui/src/lib/identity-ui-icon.tsx @@ -1,8 +1,8 @@ -import { Group, Text } from '@mantine/core' import { IdentityProvider } from '@pubkey-stack/sdk' import { IconBrandDiscord, IconBrandGithub, + IconBrandGoogle, IconBrandTwitter, IconCurrencySolana, IconQuestionMark, @@ -14,6 +14,8 @@ export function IdentityUiIcon({ provider, size }: { provider: IdentityProvider; return case IdentityProvider.GitHub: return + case IdentityProvider.Google: + return case IdentityProvider.Solana: return case IdentityProvider.Twitter: @@ -22,16 +24,3 @@ export function IdentityUiIcon({ provider, size }: { provider: IdentityProvider; return } } - -export function IdentityUiBadge({ provider }: { provider: IdentityProvider }) { - return ( - - - - - - {provider} - - - ) -} diff --git a/libs/web/identity/ui/src/lib/identity-ui-link-button.tsx b/libs/web/identity/ui/src/lib/identity-ui-link-button.tsx new file mode 100644 index 0000000..1ca2ccc --- /dev/null +++ b/libs/web/identity/ui/src/lib/identity-ui-link-button.tsx @@ -0,0 +1,27 @@ +import type { ButtonProps } from '@mantine/core' +import { Identity, IdentityProvider } from '@pubkey-stack/sdk' +import { IdentityUiProviderButton } from './identity-ui-provider-button' +import { IdentityUiSolanaLinkButton } from './identity-ui-solana-link-button' + +export function IdentityUiLinkButton({ + identities, + provider, + refresh, + ...props +}: ButtonProps & { + identities: Identity[] + provider: IdentityProvider + refresh?: () => void +}) { + switch (provider) { + case IdentityProvider.Discord: + case IdentityProvider.GitHub: + case IdentityProvider.Google: + case IdentityProvider.Twitter: + return + case IdentityProvider.Solana: + return refresh ? : null + default: + return null + } +} diff --git a/libs/web/identity/ui/src/lib/identity-ui-login-button.tsx b/libs/web/identity/ui/src/lib/identity-ui-login-button.tsx index c975895..79195bb 100644 --- a/libs/web/identity/ui/src/lib/identity-ui-login-button.tsx +++ b/libs/web/identity/ui/src/lib/identity-ui-login-button.tsx @@ -1,6 +1,6 @@ import type { ButtonProps } from '@mantine/core' import { IdentityProvider } from '@pubkey-stack/sdk' -import { IdentityUiProviderLoginButton } from './identity-ui-provider-login-button' +import { IdentityUiProviderButton } from './identity-ui-provider-button' import { IdentityUiSolanaLoginButton } from './identity-ui-solana-login-button' export function IdentityUiLoginButton({ @@ -11,8 +11,9 @@ export function IdentityUiLoginButton({ switch (provider) { case IdentityProvider.Discord: case IdentityProvider.GitHub: + case IdentityProvider.Google: case IdentityProvider.Twitter: - return + return case IdentityProvider.Solana: return default: diff --git a/libs/web/identity/ui/src/lib/identity-ui-login-buttons.tsx b/libs/web/identity/ui/src/lib/identity-ui-login-buttons.tsx index 46b9270..8d9f030 100644 --- a/libs/web/identity/ui/src/lib/identity-ui-login-buttons.tsx +++ b/libs/web/identity/ui/src/lib/identity-ui-login-buttons.tsx @@ -1,19 +1,12 @@ import { Stack, type StackProps } from '@mantine/core' -import { type AppConfig, IdentityProvider } from '@pubkey-stack/sdk' +import { IdentityProvider } from '@pubkey-stack/sdk' import { IdentityUiLoginButton } from './identity-ui-login-button' export function IdentityUiLoginButtons({ - appConfig, + enabledProviders, refresh, ...props -}: StackProps & { appConfig: AppConfig; refresh: () => void }) { - const enabledProviders: IdentityProvider[] = [ - appConfig.authDiscordEnabled && IdentityProvider.Discord, - appConfig.authGithubEnabled && IdentityProvider.GitHub, - appConfig.authSolanaEnabled && IdentityProvider.Solana, - appConfig.authTwitterEnabled && IdentityProvider.Twitter, - ].filter(Boolean) as IdentityProvider[] - +}: StackProps & { enabledProviders: IdentityProvider[]; refresh: () => void }) { return ( {enabledProviders.map((provider) => ( diff --git a/libs/web/identity/ui/src/lib/identity-ui-provider-button.tsx b/libs/web/identity/ui/src/lib/identity-ui-provider-button.tsx new file mode 100644 index 0000000..aa491b1 --- /dev/null +++ b/libs/web/identity/ui/src/lib/identity-ui-provider-button.tsx @@ -0,0 +1,24 @@ +import { Button, ButtonProps } from '@mantine/core' +import { IdentityProvider } from '@pubkey-stack/sdk' +import { getIdentityProviderColor } from './get-identity-provider-color' +import { IdentityUiIcon } from './identity-ui-icon' + +export function IdentityUiProviderButton({ + action, + provider, + ...props +}: ButtonProps & { action: 'link' | 'login'; provider: IdentityProvider }) { + return ( + + ) +} diff --git a/libs/web/identity/ui/src/lib/identity-ui-provider-login-button.tsx b/libs/web/identity/ui/src/lib/identity-ui-provider-login-button.tsx deleted file mode 100644 index 4334520..0000000 --- a/libs/web/identity/ui/src/lib/identity-ui-provider-login-button.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Button, type ButtonProps } from '@mantine/core' -import { IdentityProvider } from '@pubkey-stack/sdk' -import { IdentityUiIcon } from './identity-ui-icon' - -export function IdentityUiProviderLoginButton({ provider, ...props }: ButtonProps & { provider: IdentityProvider }) { - return ( - - ) -} diff --git a/libs/web/identity/ui/src/lib/identity-ui-solana-login-button.tsx b/libs/web/identity/ui/src/lib/identity-ui-solana-login-button.tsx index 0098c35..3fe0680 100644 --- a/libs/web/identity/ui/src/lib/identity-ui-solana-login-button.tsx +++ b/libs/web/identity/ui/src/lib/identity-ui-solana-login-button.tsx @@ -1,8 +1,10 @@ import { Button, type ButtonProps, Modal, Tooltip } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' +import { IdentityProvider } from '@pubkey-stack/sdk' import { IdentityProviderSolanaLogin } from '@pubkey-stack/web-identity-data-access' import { SolanaClusterProvider } from '@pubkey-stack/web-solana-data-access' import { IconCurrencySolana } from '@tabler/icons-react' +import { getIdentityProviderColor } from './get-identity-provider-color' import { IdentityUiSolanaLoginWizard } from './identity-ui-solana-login-wizard' export function IdentityUiSolanaLoginButton({ refresh, ...props }: ButtonProps & { refresh: () => void }) { @@ -26,9 +28,8 @@ export function IdentityUiSolanaLoginButton({ refresh, ...props }: ButtonProps &