From e5658b956550841f5a97fd1d49316f8ec72c35ba Mon Sep 17 00:00:00 2001 From: David Schwertfeger Date: Tue, 27 Aug 2024 10:47:06 +0200 Subject: [PATCH] feat: Add Twitter/X auth --- waspc/cli/src/Wasp/Cli/Command/Studio.hs | 3 + .../forms/internal/common/LoginSignupForm.tsx | 7 ++ .../forms/internal/social/SocialIcons.tsx | 11 +++ .../templates/sdk/wasp/auth/utils.ts | 1 + .../templates/sdk/wasp/client/auth/index.ts | 3 + .../templates/sdk/wasp/client/auth/twitter.ts | 2 + .../templates/sdk/wasp/client/auth/ui.ts | 3 + .../templates/sdk/wasp/server/auth/hooks.ts | 3 + .../sdk/wasp/server/auth/oauth/index.ts | 4 ++ .../server/auth/oauth/providers/twitter.ts | 28 ++++++++ .../templates/sdk/wasp/server/auth/user.ts | 6 ++ .../src/auth/providers/config/twitter.ts | 68 +++++++++++++++++++ waspc/examples/todoApp/.env.server.example | 4 ++ waspc/examples/todoApp/main.wasp | 4 ++ waspc/examples/todoApp/src/auth/twitter.ts | 10 +++ waspc/src/Wasp/AppSpec/App/Auth.hs | 8 ++- waspc/src/Wasp/Generator/AuthProviders.hs | 9 +++ .../Generator/SdkGenerator/Auth/AuthFormsG.hs | 2 + .../Generator/SdkGenerator/Auth/OAuthAuthG.hs | 5 +- .../Generator/SdkGenerator/Client/AuthG.hs | 7 ++ .../Generator/SdkGenerator/Server/OAuthG.hs | 3 +- .../ServerGenerator/Auth/OAuthAuthG.hs | 2 + .../Wasp/Generator/ServerGenerator/AuthG.hs | 2 + waspc/test/AnalyzerTest.hs | 1 + waspc/test/AppSpec/ValidTest.hs | 9 ++- web/docs/auth/Pills.css | 2 + web/docs/auth/Pills.jsx | 13 ++++ web/docs/auth/auth-hooks.md | 6 +- web/docs/auth/social-auth/SocialAuthGrid.tsx | 5 ++ web/docs/auth/ui.md | 23 ++++--- web/sidebars.js | 1 + web/src/components/AuthMethodsGrid.tsx | 5 ++ 32 files changed, 243 insertions(+), 17 deletions(-) create mode 100644 waspc/data/Generator/templates/sdk/wasp/client/auth/twitter.ts create mode 100644 waspc/data/Generator/templates/sdk/wasp/server/auth/oauth/providers/twitter.ts create mode 100644 waspc/data/Generator/templates/server/src/auth/providers/config/twitter.ts create mode 100644 waspc/examples/todoApp/src/auth/twitter.ts diff --git a/waspc/cli/src/Wasp/Cli/Command/Studio.hs b/waspc/cli/src/Wasp/Cli/Command/Studio.hs index d031bc6fca..3ab15de559 100644 --- a/waspc/cli/src/Wasp/Cli/Command/Studio.hs +++ b/waspc/cli/src/Wasp/Cli/Command/Studio.hs @@ -179,6 +179,9 @@ studio = do [ "discord" | isJust $ AS.App.Auth.discord methods ], + [ "twitter" + | isJust $ AS.App.Auth.twitter methods + ], [ "google" | isJust $ AS.App.Auth.google methods ], diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx index 97e2e8ac1e..9b9bb00883 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx +++ b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/common/LoginSignupForm.tsx @@ -117,6 +117,9 @@ const keycloakSignInUrl = `${config.apiUrl}{= keycloakSignInPath =}` {=# enabledProviders.isGitHubAuthEnabled =} const gitHubSignInUrl = `${config.apiUrl}{= gitHubSignInPath =}` {=/ enabledProviders.isGitHubAuthEnabled =} +{=# enabledProviders.isTwitterAuthEnabled =} +const twitterSignInUrl = `${config.apiUrl}{= twitterSignInPath =}` +{=/ enabledProviders.isTwitterAuthEnabled =} {=! // Since we allow users to add additional fields to the signup form, we don't @@ -208,6 +211,10 @@ export const LoginSignupForm = ({ {=# enabledProviders.isGitHubAuthEnabled =} {=/ enabledProviders.isGitHubAuthEnabled =} + + {=# enabledProviders.isTwitterAuthEnabled =} + + {=/ enabledProviders.isTwitterAuthEnabled =} {=/ isSocialAuthEnabled =} diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/social/SocialIcons.tsx b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/social/SocialIcons.tsx index 0c8bf0eb25..4fcdbb24b7 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/social/SocialIcons.tsx +++ b/waspc/data/Generator/templates/sdk/wasp/auth/forms/internal/social/SocialIcons.tsx @@ -65,3 +65,14 @@ export const Discord = () => ( ) + +export const Twitter = () => ( + +) diff --git a/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts b/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts index f5875de04c..0d0084af45 100644 --- a/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts +++ b/waspc/data/Generator/templates/sdk/wasp/auth/utils.ts @@ -44,6 +44,7 @@ export type PossibleProviderData = { google: OAuthProviderData; keycloak: OAuthProviderData; github: OAuthProviderData; + twitter: OAuthProviderData; } // PUBLIC API diff --git a/waspc/data/Generator/templates/sdk/wasp/client/auth/index.ts b/waspc/data/Generator/templates/sdk/wasp/client/auth/index.ts index a97231effd..73d8e1b2f1 100644 --- a/waspc/data/Generator/templates/sdk/wasp/client/auth/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/client/auth/index.ts @@ -18,6 +18,9 @@ export * from './keycloak' {=# isGitHubAuthEnabled =} export * from './github' {=/ isGitHubAuthEnabled =} +{=# isTwitterAuthEnabled =} +export * from './twitter' +{=/ isTwitterAuthEnabled =} export { default as useAuth, getMe, diff --git a/waspc/data/Generator/templates/sdk/wasp/client/auth/twitter.ts b/waspc/data/Generator/templates/sdk/wasp/client/auth/twitter.ts new file mode 100644 index 0000000000..a1851c4f93 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/client/auth/twitter.ts @@ -0,0 +1,2 @@ +// PUBLIC API +export { signInUrl as twitterSignInUrl } from '../../auth/helpers/Twitter' diff --git a/waspc/data/Generator/templates/sdk/wasp/client/auth/ui.ts b/waspc/data/Generator/templates/sdk/wasp/client/auth/ui.ts index ea35d385a8..4268b2f6fc 100644 --- a/waspc/data/Generator/templates/sdk/wasp/client/auth/ui.ts +++ b/waspc/data/Generator/templates/sdk/wasp/client/auth/ui.ts @@ -20,6 +20,9 @@ export { SignInButton as KeycloakSignInButton } from '../../auth/helpers/Keycloa {=# isGitHubAuthEnabled =} export { SignInButton as GitHubSignInButton } from '../../auth/helpers/GitHub' {=/ isGitHubAuthEnabled =} +{=# isTwitterAuthEnabled =} +export { SignInButton as TwitterSignInButton } from '../../auth/helpers/Twitter' +{=/ isTwitterAuthEnabled =} export { FormError, FormInput, diff --git a/waspc/data/Generator/templates/sdk/wasp/server/auth/hooks.ts b/waspc/data/Generator/templates/sdk/wasp/server/auth/hooks.ts index 6b5824dec8..2f8971c041 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/auth/hooks.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/auth/hooks.ts @@ -142,5 +142,8 @@ export type OAuthData = { {=# enabledProviders.isKeycloakAuthEnabled =} | { providerName: 'keycloak'; tokens: import('arctic').KeycloakTokens } {=/ enabledProviders.isKeycloakAuthEnabled =} + {=# enabledProviders.isTwitterAuthEnabled =} + | { providerName: 'twitter'; tokens: import('arctic').TwitterTokens } + {=/ enabledProviders.isTwitterAuthEnabled =} | never ) diff --git a/waspc/data/Generator/templates/sdk/wasp/server/auth/oauth/index.ts b/waspc/data/Generator/templates/sdk/wasp/server/auth/oauth/index.ts index 7fc28ed4c2..629a55b098 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/auth/oauth/index.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/auth/oauth/index.ts @@ -15,6 +15,10 @@ export { github } from './providers/github.js'; // PUBLIC API export { keycloak } from './providers/keycloak.js'; {=/ enabledProviders.isKeycloakAuthEnabled =} +{=# enabledProviders.isTwitterAuthEnabled =} +// PUBLIC API +export { twitter } from './providers/twitter.js'; +{=/ enabledProviders.isTwitterAuthEnabled =} // PRIVATE API export { diff --git a/waspc/data/Generator/templates/sdk/wasp/server/auth/oauth/providers/twitter.ts b/waspc/data/Generator/templates/sdk/wasp/server/auth/oauth/providers/twitter.ts new file mode 100644 index 0000000000..8038567483 --- /dev/null +++ b/waspc/data/Generator/templates/sdk/wasp/server/auth/oauth/providers/twitter.ts @@ -0,0 +1,28 @@ +{{={= =}=}} +import { Twitter } from "arctic"; + +import { defineProvider } from "../provider.js"; +import { ensureEnvVarsForProvider } from "../env.js"; +import { getRedirectUriForCallback } from "../redirect.js"; + +const id = "{= providerId =}"; +const displayName = "{= displayName =}"; + +const env = ensureEnvVarsForProvider( + ["TWITTER_CLIENT_ID", "TWITTER_CLIENT_SECRET"], + displayName +); + +const oAuthClient = new Twitter( + env.TWITTER_CLIENT_ID, + env.TWITTER_CLIENT_SECRET, + getRedirectUriForCallback(id).toString(), +); + +// PUBLIC API +export const twitter = defineProvider({ + id, + displayName, + env, + oAuthClient, +}); diff --git a/waspc/data/Generator/templates/sdk/wasp/server/auth/user.ts b/waspc/data/Generator/templates/sdk/wasp/server/auth/user.ts index 96ebf3c0fa..a85547e57d 100644 --- a/waspc/data/Generator/templates/sdk/wasp/server/auth/user.ts +++ b/waspc/data/Generator/templates/sdk/wasp/server/auth/user.ts @@ -49,6 +49,9 @@ export type AuthUserData = Omit> | null {=/ enabledProviders.isGitHubAuthEnabled =} + {=# enabledProviders.isTwitterAuthEnabled =} + twitter: Expand> | null + {=/ enabledProviders.isTwitterAuthEnabled =} }, } @@ -111,6 +114,9 @@ This should never happen, but it did which means there is a bug in the code.`) {=# enabledProviders.isGitHubAuthEnabled =} github: getProviderInfo<'github'>({= authFieldOnUserEntityName =}, 'github'), {=/ enabledProviders.isGitHubAuthEnabled =} + {=# enabledProviders.isTwitterAuthEnabled =} + twitter: getProviderInfo<'twitter'>({= authFieldOnUserEntityName =}, 'twitter'), + {=/ enabledProviders.isTwitterAuthEnabled =} } return { ...rest, diff --git a/waspc/data/Generator/templates/server/src/auth/providers/config/twitter.ts b/waspc/data/Generator/templates/server/src/auth/providers/config/twitter.ts new file mode 100644 index 0000000000..540ec16a33 --- /dev/null +++ b/waspc/data/Generator/templates/server/src/auth/providers/config/twitter.ts @@ -0,0 +1,68 @@ +{{={= =}=}} + +import type { ProviderConfig } from "wasp/auth/providers/types"; +import { twitter } from "wasp/server/auth"; +import { mergeDefaultAndUserConfig } from "../oauth/config.js"; +import { createOAuthProviderRouter } from "../oauth/handler.js"; + +{=# userSignupFields.isDefined =} +{=& userSignupFields.importStatement =} +const _waspUserSignupFields = {= userSignupFields.importIdentifier =} +{=/ userSignupFields.isDefined =} +{=^ userSignupFields.isDefined =} +const _waspUserSignupFields = undefined +{=/ userSignupFields.isDefined =} +{=# configFn.isDefined =} +{=& configFn.importStatement =} +const _waspUserDefinedConfigFn = {= configFn.importIdentifier =} +{=/ configFn.isDefined =} +{=^ configFn.isDefined =} +const _waspUserDefinedConfigFn = undefined +{=/ configFn.isDefined =} + +const _waspConfig: ProviderConfig = { + id: twitter.id, + displayName: twitter.displayName, + createRouter(provider) { + const config = mergeDefaultAndUserConfig({ + scopes: {=& requiredScopes =}, + }, _waspUserDefinedConfigFn); + + async function getTwitterProfile(accessToken: string): Promise<{ + providerProfile: unknown; + providerUserId: string; + }> { + const response = await fetch("https://api.twitter.com/2/users/me", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + const jsonResponse = await response.json(); + + const providerProfile = jsonResponse.data as { + id?: string; + name?: string; + username?: string; + }; + + + if (!providerProfile.id) { + throw new Error("Invalid profile"); + } + + return { providerProfile, providerUserId: providerProfile.id }; + } + + return createOAuthProviderRouter({ + provider, + oAuthType: 'OAuth2WithPKCE', + userSignupFields: _waspUserSignupFields, + getAuthorizationUrl: ({ state, codeVerifier }) => twitter.oAuthClient.createAuthorizationURL(state, codeVerifier, config), + getProviderTokens: ({ code, codeVerifier }) => twitter.oAuthClient.validateAuthorizationCode(code, codeVerifier), + getProviderInfo: ({ accessToken }) => getTwitterProfile(accessToken), + }); + }, +} + +export default _waspConfig; diff --git a/waspc/examples/todoApp/.env.server.example b/waspc/examples/todoApp/.env.server.example index 76df894076..07d9b1b938 100644 --- a/waspc/examples/todoApp/.env.server.example +++ b/waspc/examples/todoApp/.env.server.example @@ -22,3 +22,7 @@ GITHUB_CLIENT_SECRET='dummy-gh-client-secret' # Dummy values here will allow app to run, but you will need real values to get Discord Auth to work. DISCORD_CLIENT_SECRET='dummy-discord-client-secret' DISCORD_CLIENT_ID='dummy-discord-client-id' + +# Dummy values here will allow app to run, but you will need real values to get Twitter Auth to work. +TWITTER_CLIENT_SECRET='dummy-twitter-client-secret' +TWITTER_CLIENT_ID='dummy-twitter-client-id' diff --git a/waspc/examples/todoApp/main.wasp b/waspc/examples/todoApp/main.wasp index 0043e397b4..78dc0c7f1d 100644 --- a/waspc/examples/todoApp/main.wasp +++ b/waspc/examples/todoApp/main.wasp @@ -26,6 +26,10 @@ app todoApp { configFn: import { config } from "@src/auth/github.js", userSignupFields: import { userSignupFields } from "@src/auth/github.js" }, + twitter: { + configFn: import { config } from "@src/auth/twitter.js", + userSignupFields: import { userSignupFields } from "@src/auth/twitter.js" + }, // keycloak: {}, email: { userSignupFields: import { userSignupFields } from "@src/auth/email", diff --git a/waspc/examples/todoApp/src/auth/twitter.ts b/waspc/examples/todoApp/src/auth/twitter.ts new file mode 100644 index 0000000000..5b1e695df3 --- /dev/null +++ b/waspc/examples/todoApp/src/auth/twitter.ts @@ -0,0 +1,10 @@ +import { defineUserSignupFields } from 'wasp/server/auth' + +export function config() { + console.log('Inside user-supplied Twitter config') + return { + scopes: ['users.read', 'tweet.read'], + } +} + +export const userSignupFields = defineUserSignupFields({}) diff --git a/waspc/src/Wasp/AppSpec/App/Auth.hs b/waspc/src/Wasp/AppSpec/App/Auth.hs index 016dee74c1..aa59b80784 100644 --- a/waspc/src/Wasp/AppSpec/App/Auth.hs +++ b/waspc/src/Wasp/AppSpec/App/Auth.hs @@ -14,6 +14,7 @@ module Wasp.AppSpec.App.Auth isKeycloakAuthEnabled, isGitHubAuthEnabled, isEmailAuthEnabled, + isTwitterAuthEnabled, userSignupFieldsForEmailAuth, userSignupFieldsForUsernameAuth, userSignupFieldsForExternalAuth, @@ -49,6 +50,7 @@ data AuthMethods = AuthMethods google :: Maybe ExternalAuthConfig, gitHub :: Maybe ExternalAuthConfig, keycloak :: Maybe ExternalAuthConfig, + twitter :: Maybe ExternalAuthConfig, email :: Maybe EmailAuthConfig } deriving (Show, Eq, Data) @@ -83,7 +85,8 @@ isExternalAuthEnabled auth = [ isDiscordAuthEnabled, isGoogleAuthEnabled, isGitHubAuthEnabled, - isKeycloakAuthEnabled + isKeycloakAuthEnabled, + isTwitterAuthEnabled ] isDiscordAuthEnabled :: Auth -> Bool @@ -98,6 +101,9 @@ isKeycloakAuthEnabled = isJust . keycloak . methods isGitHubAuthEnabled :: Auth -> Bool isGitHubAuthEnabled = isJust . gitHub . methods +isTwitterAuthEnabled :: Auth -> Bool +isTwitterAuthEnabled = isJust . twitter . methods + isEmailAuthEnabled :: Auth -> Bool isEmailAuthEnabled = isJust . email . methods diff --git a/waspc/src/Wasp/Generator/AuthProviders.hs b/waspc/src/Wasp/Generator/AuthProviders.hs index ef867c5662..91f3389e79 100644 --- a/waspc/src/Wasp/Generator/AuthProviders.hs +++ b/waspc/src/Wasp/Generator/AuthProviders.hs @@ -55,6 +55,14 @@ discordAuthProvider = OA._requiredScope = ["identify"] } +twitterAuthProvider :: OA.OAuthAuthProvider +twitterAuthProvider = + OA.OAuthAuthProvider + { OA._providerId = fromJust $ makeProviderId "twitter", + OA._displayName = "Twitter", + OA._requiredScope = ["users.read", "tweet.read"] + } + getEnabledAuthProvidersJson :: AS.Auth.Auth -> Aeson.Value getEnabledAuthProvidersJson auth = object @@ -62,6 +70,7 @@ getEnabledAuthProvidersJson auth = "isGoogleAuthEnabled" .= AS.Auth.isGoogleAuthEnabled auth, "isKeycloakAuthEnabled" .= AS.Auth.isKeycloakAuthEnabled auth, "isGitHubAuthEnabled" .= AS.Auth.isGitHubAuthEnabled auth, + "isTwitterAuthEnabled" .= AS.Auth.isTwitterAuthEnabled auth, "isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth, "isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth ] diff --git a/waspc/src/Wasp/Generator/SdkGenerator/Auth/AuthFormsG.hs b/waspc/src/Wasp/Generator/SdkGenerator/Auth/AuthFormsG.hs index e24dd1b93b..1dc6206ab4 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator/Auth/AuthFormsG.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator/Auth/AuthFormsG.hs @@ -11,6 +11,7 @@ import Wasp.Generator.AuthProviders gitHubAuthProvider, googleAuthProvider, keycloakAuthProvider, + twitterAuthProvider, ) import qualified Wasp.Generator.AuthProviders as AuthProviders import qualified Wasp.Generator.AuthProviders.OAuth as OAuth @@ -124,6 +125,7 @@ genLoginSignupForm auth = "googleSignInPath" .= OAuth.serverLoginUrl googleAuthProvider, "keycloakSignInPath" .= OAuth.serverLoginUrl keycloakAuthProvider, "gitHubSignInPath" .= OAuth.serverLoginUrl gitHubAuthProvider, + "twitterSignInPath" .= OAuth.serverLoginUrl twitterAuthProvider, "enabledProviders" .= AuthProviders.getEnabledAuthProvidersJson auth ] areBothSocialAndPasswordBasedAuthEnabled = AS.Auth.isExternalAuthEnabled auth && isAnyPasswordBasedAuthEnabled diff --git a/waspc/src/Wasp/Generator/SdkGenerator/Auth/OAuthAuthG.hs b/waspc/src/Wasp/Generator/SdkGenerator/Auth/OAuthAuthG.hs index ecca128ed8..234749a27e 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator/Auth/OAuthAuthG.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator/Auth/OAuthAuthG.hs @@ -12,6 +12,7 @@ import Wasp.Generator.AuthProviders gitHubAuthProvider, googleAuthProvider, keycloakAuthProvider, + twitterAuthProvider, ) import Wasp.Generator.AuthProviders.OAuth (OAuthAuthProvider) import qualified Wasp.Generator.AuthProviders.OAuth as OAuth @@ -32,13 +33,15 @@ genHelpers auth = [ [discordHelpers | AS.Auth.isDiscordAuthEnabled auth], [gitHubHelpers | AS.Auth.isGitHubAuthEnabled auth], [googleHelpers | AS.Auth.isGoogleAuthEnabled auth], - [keycloakHelpers | AS.Auth.isKeycloakAuthEnabled auth] + [keycloakHelpers | AS.Auth.isKeycloakAuthEnabled auth], + [twitterHelpers | AS.Auth.isTwitterAuthEnabled auth] ] where discordHelpers = mkHelpersFd discordAuthProvider [relfile|Discord.tsx|] gitHubHelpers = mkHelpersFd gitHubAuthProvider [relfile|GitHub.tsx|] googleHelpers = mkHelpersFd googleAuthProvider [relfile|Google.tsx|] keycloakHelpers = mkHelpersFd keycloakAuthProvider [relfile|Keycloak.tsx|] + twitterHelpers = mkHelpersFd twitterAuthProvider [relfile|Twitter.tsx|] mkHelpersFd :: OAuthAuthProvider -> Path' Rel' File' -> FileDraft mkHelpersFd provider helpersFp = diff --git a/waspc/src/Wasp/Generator/SdkGenerator/Client/AuthG.hs b/waspc/src/Wasp/Generator/SdkGenerator/Client/AuthG.hs index c182d49e80..e3c98133b9 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator/Client/AuthG.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator/Client/AuthG.hs @@ -30,6 +30,7 @@ genNewClientAuth spec = <++> genAuthGoogle auth <++> genAuthKeycloak auth <++> genAuthGitHub auth + <++> genAuthTwitter auth where maybeAuth = AS.App.auth $ snd $ getApp spec @@ -87,5 +88,11 @@ genAuthGitHub auth = then sequence [genFileCopy [relfile|client/auth/github.ts|]] else return [] +genAuthTwitter :: AS.Auth.Auth -> Generator [FileDraft] +genAuthTwitter auth = + if AS.Auth.isTwitterAuthEnabled auth + then sequence [genFileCopy [relfile|client/auth/twitter.ts|]] + else return [] + genFileCopy :: Path' (Rel SdkTemplatesDir) File' -> Generator FileDraft genFileCopy = return . C.mkTmplFd diff --git a/waspc/src/Wasp/Generator/SdkGenerator/Server/OAuthG.hs b/waspc/src/Wasp/Generator/SdkGenerator/Server/OAuthG.hs index fb533e22f1..09c8c5fb0c 100644 --- a/waspc/src/Wasp/Generator/SdkGenerator/Server/OAuthG.hs +++ b/waspc/src/Wasp/Generator/SdkGenerator/Server/OAuthG.hs @@ -14,7 +14,7 @@ import qualified Wasp.AppSpec.App.Auth as AS.App.Auth import qualified Wasp.AppSpec.App.Auth as AS.Auth import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import qualified Wasp.AppSpec.Valid as AS.Valid -import Wasp.Generator.AuthProviders (discordAuthProvider, getEnabledAuthProvidersJson, gitHubAuthProvider, googleAuthProvider, keycloakAuthProvider) +import Wasp.Generator.AuthProviders (discordAuthProvider, getEnabledAuthProvidersJson, gitHubAuthProvider, googleAuthProvider, keycloakAuthProvider, twitterAuthProvider) import Wasp.Generator.AuthProviders.OAuth ( OAuthAuthProvider, clientOAuthCallbackPath, @@ -43,6 +43,7 @@ genOAuth auth <++> genOAuthProvider googleAuthProvider (AS.Auth.google . AS.Auth.methods $ auth) <++> genOAuthProvider keycloakAuthProvider (AS.Auth.keycloak . AS.Auth.methods $ auth) <++> genOAuthProvider gitHubAuthProvider (AS.Auth.gitHub . AS.Auth.methods $ auth) + <++> genOAuthProvider twitterAuthProvider (AS.Auth.twitter . AS.Auth.methods $ auth) | otherwise = return [] where genFileCopy = return . C.mkTmplFd diff --git a/waspc/src/Wasp/Generator/ServerGenerator/Auth/OAuthAuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/Auth/OAuthAuthG.hs index a44da128ce..231b197f3d 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/Auth/OAuthAuthG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/Auth/OAuthAuthG.hs @@ -25,6 +25,7 @@ import Wasp.Generator.AuthProviders gitHubAuthProvider, googleAuthProvider, keycloakAuthProvider, + twitterAuthProvider, ) import Wasp.Generator.AuthProviders.OAuth (OAuthAuthProvider) import qualified Wasp.Generator.AuthProviders.OAuth as OAuth @@ -45,6 +46,7 @@ genOAuthAuth auth <++> genOAuthProvider googleAuthProvider (AS.Auth.google . AS.Auth.methods $ auth) <++> genOAuthProvider keycloakAuthProvider (AS.Auth.keycloak . AS.Auth.methods $ auth) <++> genOAuthProvider gitHubAuthProvider (AS.Auth.gitHub . AS.Auth.methods $ auth) + <++> genOAuthProvider twitterAuthProvider (AS.Auth.twitter . AS.Auth.methods $ auth) | otherwise = return [] genOAuthHelpers :: AS.Auth.Auth -> Generator [FileDraft] diff --git a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs index 8ae16e80ed..b6e37dc922 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs @@ -30,6 +30,7 @@ import Wasp.Generator.AuthProviders googleAuthProvider, keycloakAuthProvider, localAuthProvider, + twitterAuthProvider, ) import qualified Wasp.Generator.AuthProviders.Email as EmailProvider import qualified Wasp.Generator.AuthProviders.Local as LocalProvider @@ -90,6 +91,7 @@ genProvidersIndex auth = return $ C.mkTmplFdWithData [relfile|src/auth/providers [OAuthProvider.providerId gitHubAuthProvider | AS.Auth.isGitHubAuthEnabled auth], [OAuthProvider.providerId googleAuthProvider | AS.Auth.isGoogleAuthEnabled auth], [OAuthProvider.providerId keycloakAuthProvider | AS.Auth.isKeycloakAuthEnabled auth], + [OAuthProvider.providerId twitterAuthProvider | AS.Auth.isTwitterAuthEnabled auth], [LocalProvider.providerId localAuthProvider | AS.Auth.isUsernameAndPasswordAuthEnabled auth], [EmailProvider.providerId emailAuthProvider | AS.Auth.isEmailAuthEnabled auth] ] diff --git a/waspc/test/AnalyzerTest.hs b/waspc/test/AnalyzerTest.hs index 1a93c29f1a..7b63320f29 100644 --- a/waspc/test/AnalyzerTest.hs +++ b/waspc/test/AnalyzerTest.hs @@ -146,6 +146,7 @@ spec_Analyzer = do Auth.google = Nothing, Auth.keycloak = Nothing, Auth.gitHub = Nothing, + Auth.twitter = Nothing, Auth.email = Nothing }, Auth.onAuthFailedRedirectTo = "/", diff --git a/waspc/test/AppSpec/ValidTest.hs b/waspc/test/AppSpec/ValidTest.hs index 7859f655ce..be3c35bdd1 100644 --- a/waspc/test/AppSpec/ValidTest.hs +++ b/waspc/test/AppSpec/ValidTest.hs @@ -117,6 +117,7 @@ spec_AppSpecValid = do AS.Auth.google = Nothing, AS.Auth.gitHub = Nothing, AS.Auth.keycloak = Nothing, + AS.Auth.twitter = Nothing, AS.Auth.email = Nothing }, AS.Auth.onAuthFailedRedirectTo = "/", @@ -208,7 +209,7 @@ spec_AppSpecValid = do } it "returns no error if app.auth is not set" $ do - ASV.validateAppSpec (makeSpec (AS.Auth.AuthMethods {usernameAndPassword = Nothing, discord = Nothing, google = Nothing, keycloak = Nothing, gitHub = Nothing, email = Nothing}) validUserEntity) `shouldBe` [] + ASV.validateAppSpec (makeSpec (AS.Auth.AuthMethods {usernameAndPassword = Nothing, discord = Nothing, google = Nothing, keycloak = Nothing, gitHub = Nothing, twitter = Nothing, email = Nothing}) validUserEntity) `shouldBe` [] it "returns no error if app.auth is set and only one of UsernameAndPassword and Email is used" $ do ASV.validateAppSpec @@ -223,13 +224,14 @@ spec_AppSpecValid = do google = Nothing, keycloak = Nothing, gitHub = Nothing, + twitter = Nothing, email = Nothing } ) validUserEntity ) `shouldBe` [] - ASV.validateAppSpec (makeSpec (AS.Auth.AuthMethods {usernameAndPassword = Nothing, discord = Nothing, google = Nothing, keycloak = Nothing, gitHub = Nothing, email = Just emailAuthConfig}) validUserEntity) `shouldBe` [] + ASV.validateAppSpec (makeSpec (AS.Auth.AuthMethods {usernameAndPassword = Nothing, discord = Nothing, google = Nothing, keycloak = Nothing, gitHub = Nothing, twitter = Nothing, email = Just emailAuthConfig}) validUserEntity) `shouldBe` [] it "returns an error if app.auth is set and both UsernameAndPassword and Email are used" $ do ASV.validateAppSpec @@ -244,6 +246,7 @@ spec_AppSpecValid = do google = Nothing, keycloak = Nothing, gitHub = Nothing, + twitter = Nothing, email = Just emailAuthConfig } ) @@ -308,7 +311,7 @@ spec_AppSpecValid = do Just AS.Auth.Auth { AS.Auth.methods = - AS.Auth.AuthMethods {email = Just emailAuthConfig, usernameAndPassword = Nothing, discord = Nothing, google = Nothing, keycloak = Nothing, gitHub = Nothing}, + AS.Auth.AuthMethods {email = Just emailAuthConfig, usernameAndPassword = Nothing, discord = Nothing, google = Nothing, keycloak = Nothing, gitHub = Nothing, twitter = Nothing}, AS.Auth.userEntity = AS.Core.Ref.Ref userEntityName, AS.Auth.externalAuthEntity = Nothing, AS.Auth.onAuthFailedRedirectTo = "/", diff --git a/web/docs/auth/Pills.css b/web/docs/auth/Pills.css index ac072b1195..a5e2633f06 100644 --- a/web/docs/auth/Pills.css +++ b/web/docs/auth/Pills.css @@ -5,6 +5,7 @@ --auth-pills-github: #f1f5f9; --auth-pills-google: #ecfccb; --auth-pills-keycloak: #d0ebf5; + --auth-pills-twitter: #dbeafe; --auth-pills-username-and-pass: #fce7f3; } @@ -15,5 +16,6 @@ --auth-pills-github: #334155; --auth-pills-google: #365314; --auth-pills-keycloak: #2d5866; + --auth-pills-twittter: #0e7490; --auth-pills-username-and-pass: #831843; } diff --git a/web/docs/auth/Pills.jsx b/web/docs/auth/Pills.jsx index 2bd63708b0..ec1074cb6c 100644 --- a/web/docs/auth/Pills.jsx +++ b/web/docs/auth/Pills.jsx @@ -97,3 +97,16 @@ export function KeycloakPill() { ) } + +export function TwitterPill() { + return ( + + Twitter + + ) +} diff --git a/web/docs/auth/auth-hooks.md b/web/docs/auth/auth-hooks.md index 9106e988fd..f6d6a8a83c 100644 --- a/web/docs/auth/auth-hooks.md +++ b/web/docs/auth/auth-hooks.md @@ -2,7 +2,7 @@ title: Auth Hooks --- -import { EmailPill, UsernameAndPasswordPill, GithubPill, GooglePill, KeycloakPill, DiscordPill } from "./Pills"; +import { EmailPill, UsernameAndPasswordPill, GithubPill, GooglePill, KeycloakPill, DiscordPill, TwitterPill } from "./Pills"; import ImgWithCaption from '@site/blog/components/ImgWithCaption' import { ShowForTs } from '@site/src/components/TsJsHelpers' @@ -108,7 +108,7 @@ Wasp calls the `onBeforeSignup` hook before the user is created. The `onBeforeSignup` hook can be useful if you want to reject a user based on some criteria before they sign up. -Works with +Works with @@ -198,7 +198,7 @@ The `onAfterSignup` hook can be useful if you want to send the user a welcome em Since the `onAfterSignup` hook receives the OAuth tokens, you can use this hook to store the OAuth access token and/or [refresh token](#refreshing-the-oauth-access-token) in your database. -Works with +Works with diff --git a/web/docs/auth/social-auth/SocialAuthGrid.tsx b/web/docs/auth/social-auth/SocialAuthGrid.tsx index 0cfaa05aab..8d4c76b9ec 100644 --- a/web/docs/auth/social-auth/SocialAuthGrid.tsx +++ b/web/docs/auth/social-auth/SocialAuthGrid.tsx @@ -26,6 +26,11 @@ export function SocialAuthGrid({ description: 'Users sign in with their Discord account.', linkToDocs: '/docs/auth/social-auth/discord' + pagePart, }, + { + title: 'Twitter', + description: 'Users sign in with their Twitter account.', + linkToDocs: '/docs/auth/social-auth/twitter' + pagePart, + }, ] return ( <> diff --git a/web/docs/auth/ui.md b/web/docs/auth/ui.md index 07b6508a6f..a1fac10db1 100644 --- a/web/docs/auth/ui.md +++ b/web/docs/auth/ui.md @@ -2,7 +2,7 @@ title: Auth UI --- -import { EmailPill, UsernameAndPasswordPill, GithubPill, GooglePill, KeycloakPill, DiscordPill } from "./Pills"; +import { EmailPill, UsernameAndPasswordPill, GithubPill, GooglePill, KeycloakPill, DiscordPill, TwitterPill } from "./Pills"; To make using authentication in your app as easy as possible, Wasp generates the server-side code but also the client-side UI for you. It enables you to quickly get the login, signup, password reset and email verification flows in your app. @@ -102,15 +102,22 @@ Let's go through all of the available components and how to use them. The following components are available for you to use in your app: -- [Login form](#login-form) -- [Signup form](#signup-form) -- [Forgot password form](#forgot-password-form) -- [Reset password form](#reset-password-form) -- [Verify email form](#verify-email-form) +- [Overview](#overview) +- [Auth Components](#auth-components) + - [Login Form](#login-form) + - [Signup Form](#signup-form) + - [Forgot Password Form](#forgot-password-form) + - [Reset Password Form](#reset-password-form) + - [Verify Email Form](#verify-email-form) +- [Customization 💅🏻](#customization-) + - [1. Customizing the Colors](#1-customizing-the-colors) + - [2. Using Your Logo](#2-using-your-logo) + - [3. Social Buttons Layout](#3-social-buttons-layout) + - [Let's Put Everything Together 🪄](#lets-put-everything-together-) ### Login Form -Used with , , , , , and authentication. +Used with , , , , , , and authentication. ![Login form](/img/authui/login.png) @@ -165,7 +172,7 @@ It will automatically show the correct authentication providers based on your `m ### Signup Form -Used with , , , , , and authentication. +Used with , , , , , , and authentication. ![Signup form](/img/authui/signup.png) diff --git a/web/sidebars.js b/web/sidebars.js index 48dd6ea98f..d4e898f6ca 100644 --- a/web/sidebars.js +++ b/web/sidebars.js @@ -73,6 +73,7 @@ module.exports = { 'auth/social-auth/google', 'auth/social-auth/keycloak', 'auth/social-auth/discord', + 'auth/social-auth/twitter', ], }, 'auth/entities/entities', diff --git a/web/src/components/AuthMethodsGrid.tsx b/web/src/components/AuthMethodsGrid.tsx index ff1962c323..adcfdd8e30 100644 --- a/web/src/components/AuthMethodsGrid.tsx +++ b/web/src/components/AuthMethodsGrid.tsx @@ -33,6 +33,11 @@ export function AuthMethodsGrid() { description: 'Users sign in with their Discord account', linkToDocs: '/docs/auth/social-auth/discord', }, + { + title: 'Twitter', + description: 'Users sign in with their Twitter account', + linkToDocs: '/docs/auth/social-auth/twitter', + }, ] return ( <>