feat: Add Twitter/X auth

This commit is contained in:
David Schwertfeger 2024-08-27 10:47:06 +02:00
parent ec15625d15
commit e5658b9565
32 changed files with 243 additions and 17 deletions

View File

@ -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
],

View File

@ -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 =}
<SocialButton href={gitHubSignInUrl}><SocialIcons.GitHub/></SocialButton>
{=/ enabledProviders.isGitHubAuthEnabled =}
{=# enabledProviders.isTwitterAuthEnabled =}
<SocialButton href={twitterSignInUrl}><SocialIcons.Twitter/></SocialButton>
{=/ enabledProviders.isTwitterAuthEnabled =}
</SocialAuthButtons>
</SocialAuth>
{=/ isSocialAuthEnabled =}

View File

@ -65,3 +65,14 @@ export const Discord = () => (
<path d="M13.545 2.907a13.227 13.227 0 00-3.257-1.011.05.05 0 00-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 00-3.658 0 8.258 8.258 0 00-.412-.833.051.051 0 00-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 00-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 003.995 2.02.05.05 0 00.056-.019c.308-.42.582-.863.818-1.329a.05.05 0 00-.01-.059.051.051 0 00-.018-.011 8.875 8.875 0 01-1.248-.595.05.05 0 01-.02-.066.051.051 0 01.015-.019c.084-.063.168-.129.248-.195a.05.05 0 01.051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 01.053.007c.08.066.164.132.248.195a.051.051 0 01-.004.085 8.254 8.254 0 01-1.249.594.05.05 0 00-.03.03.052.052 0 00.003.041c.24.465.515.909.817 1.329a.05.05 0 00.056.019 13.235 13.235 0 004.001-2.02.049.049 0 00.021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 00-.02-.019zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612z" />
</svg>
)
export const Twitter = () => (
<svg
className={defaultStyles()}
aria-hidden="true"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
)

View File

@ -44,6 +44,7 @@ export type PossibleProviderData = {
google: OAuthProviderData;
keycloak: OAuthProviderData;
github: OAuthProviderData;
twitter: OAuthProviderData;
}
// PUBLIC API

View File

@ -18,6 +18,9 @@ export * from './keycloak'
{=# isGitHubAuthEnabled =}
export * from './github'
{=/ isGitHubAuthEnabled =}
{=# isTwitterAuthEnabled =}
export * from './twitter'
{=/ isTwitterAuthEnabled =}
export {
default as useAuth,
getMe,

View File

@ -0,0 +1,2 @@
// PUBLIC API
export { signInUrl as twitterSignInUrl } from '../../auth/helpers/Twitter'

View File

@ -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,

View File

@ -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
)

View File

@ -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 {

View File

@ -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,
});

View File

@ -49,6 +49,9 @@ export type AuthUserData = Omit<CompleteUserEntityWithAuth, '{= authFieldOnUserE
{=# enabledProviders.isGitHubAuthEnabled =}
github: Expand<UserFacingProviderData<'github'>> | null
{=/ enabledProviders.isGitHubAuthEnabled =}
{=# enabledProviders.isTwitterAuthEnabled =}
twitter: Expand<UserFacingProviderData<'twitter'>> | 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,

View File

@ -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;

View File

@ -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'

View File

@ -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",

View File

@ -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({})

View File

@ -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

View File

@ -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
]

View File

@ -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

View File

@ -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 =

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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]
]

View File

@ -146,6 +146,7 @@ spec_Analyzer = do
Auth.google = Nothing,
Auth.keycloak = Nothing,
Auth.gitHub = Nothing,
Auth.twitter = Nothing,
Auth.email = Nothing
},
Auth.onAuthFailedRedirectTo = "/",

View File

@ -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 = "/",

View File

@ -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;
}

View File

@ -97,3 +97,16 @@ export function KeycloakPill() {
</Pill>
)
}
export function TwitterPill() {
return (
<Pill
style={{
backgroundColor: 'var(--auth-pills-twitter)',
}}
linkToPage="/docs/auth/social-auth/twitter"
>
Twitter
</Pill>
)
}

View File

@ -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 <EmailPill /> <UsernameAndPasswordPill /> <DiscordPill /> <GithubPill /> <GooglePill /> <KeycloakPill />
Works with <EmailPill /> <UsernameAndPasswordPill /> <DiscordPill /> <GithubPill /> <GooglePill /> <KeycloakPill /> <TwitterPill />
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
@ -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 <EmailPill /> <UsernameAndPasswordPill /> <DiscordPill /> <GithubPill /> <GooglePill /> <KeycloakPill />
Works with <EmailPill /> <UsernameAndPasswordPill /> <DiscordPill /> <GithubPill /> <GooglePill /> <KeycloakPill /> <TwitterPill />
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">

View File

@ -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 (
<>

View File

@ -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 <UsernameAndPasswordPill />, <EmailPill />, <GithubPill />, <GooglePill />, <KeycloakPill />, and <DiscordPill /> authentication.
Used with <UsernameAndPasswordPill />, <EmailPill />, <GithubPill />, <GooglePill />, <KeycloakPill />, <DiscordPill />, and <TwitterPill /> 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 <UsernameAndPasswordPill />, <EmailPill />, <GithubPill />, <GooglePill />, <KeycloakPill />, and <DiscordPill /> authentication.
Used with <UsernameAndPasswordPill />, <EmailPill />, <GithubPill />, <GooglePill />, <KeycloakPill />, <DiscordPill />, and <TwitterPill /> authentication.
![Signup form](/img/authui/signup.png)

View File

@ -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',

View File

@ -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 (
<>