feat: implement multiple oauth providers in the frontend

This commit is contained in:
Stavros
2025-09-12 14:38:06 +03:00
parent fbf5843592
commit e5ecf6336f
18 changed files with 77 additions and 62 deletions

View File

@@ -1,6 +1,6 @@
import type { SVGProps } from "react";
export function GenericIcon(props: SVGProps<SVGSVGElement>) {
export function OAuthIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,7 +1,5 @@
import { LoginForm } from "@/components/auth/login-form";
import { GenericIcon } from "@/components/icons/generic";
import { GithubIcon } from "@/components/icons/github";
import { GoogleIcon } from "@/components/icons/google";
import { OAuthIcon } from "@/components/icons/oauth";
import {
Card,
CardHeader,
@@ -24,8 +22,7 @@ import { toast } from "sonner";
export const LoginPage = () => {
const { isLoggedIn } = useUserContext();
const { configuredProviders, title, oauthAutoRedirect, genericName } =
useAppContext();
const { providers, title, oauthAutoRedirect } = useAppContext();
const { search } = useLocation();
const { t } = useTranslation();
const isMounted = useIsMounted();
@@ -35,10 +32,11 @@ export const LoginPage = () => {
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri");
const oauthConfigured =
configuredProviders.filter((provider) => provider !== "username").length >
0;
const userAuthConfigured = configuredProviders.includes("username");
const oauthProviders = providers.filter(
(provider) => provider.id !== "username",
);
const userAuthConfigured =
providers.find((provider) => provider.id === "username") !== undefined;
const oauthMutation = useMutation({
mutationFn: (provider: string) =>
@@ -96,8 +94,8 @@ export const LoginPage = () => {
useEffect(() => {
if (isMounted()) {
if (
oauthConfigured &&
configuredProviders.includes(oauthAutoRedirect) &&
oauthProviders.length !== 0 &&
providers.find((provider) => provider.id === oauthAutoRedirect) &&
!isLoggedIn &&
redirectUri
) {
@@ -130,57 +128,33 @@ export const LoginPage = () => {
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-center text-3xl">{title}</CardTitle>
{configuredProviders.length > 0 && (
{providers.length > 0 && (
<CardDescription className="text-center">
{oauthConfigured ? t("loginTitle") : t("loginTitleSimple")}
{oauthProviders.length !== 0
? t("loginTitle")
: t("loginTitleSimple")}
</CardDescription>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
{oauthConfigured && (
{oauthProviders.length !== 0 && (
<div className="flex flex-col gap-2 items-center justify-center">
{configuredProviders.includes("google") && (
{oauthProviders.map((provider) => (
<OAuthButton
title="Google"
icon={<GoogleIcon />}
title={provider.name}
icon={<OAuthIcon />}
className="w-full"
onClick={() => oauthMutation.mutate("google")}
onClick={() => oauthMutation.mutate(provider.id)}
loading={
oauthMutation.isPending &&
oauthMutation.variables === "google"
oauthMutation.variables === provider.id
}
disabled={oauthMutation.isPending || loginMutation.isPending}
/>
)}
{configuredProviders.includes("github") && (
<OAuthButton
title="Github"
icon={<GithubIcon />}
className="w-full"
onClick={() => oauthMutation.mutate("github")}
loading={
oauthMutation.isPending &&
oauthMutation.variables === "github"
}
disabled={oauthMutation.isPending || loginMutation.isPending}
/>
)}
{configuredProviders.includes("generic") && (
<OAuthButton
title={genericName}
icon={<GenericIcon />}
className="w-full"
onClick={() => oauthMutation.mutate("generic")}
loading={
oauthMutation.isPending &&
oauthMutation.variables === "generic"
}
disabled={oauthMutation.isPending || loginMutation.isPending}
/>
)}
))}
</div>
)}
{userAuthConfigured && oauthConfigured && (
{userAuthConfigured && oauthProviders.length !== 0 && (
<SeperatorWithChildren>{t("loginDivider")}</SeperatorWithChildren>
)}
{userAuthConfigured && (
@@ -189,7 +163,7 @@ export const LoginPage = () => {
loading={loginMutation.isPending || oauthMutation.isPending}
/>
)}
{configuredProviders.length == 0 && (
{providers.length == 0 && (
<p className="text-center text-red-600 max-w-sm">
{t("failedToFetchProvidersTitle")}
</p>

View File

@@ -6,9 +6,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useAppContext } from "@/context/app-context";
import { useUserContext } from "@/context/user-context";
import { capitalize } from "@/lib/utils";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { useEffect, useRef } from "react";
@@ -17,8 +15,7 @@ import { Navigate } from "react-router";
import { toast } from "sonner";
export const LogoutPage = () => {
const { provider, username, isLoggedIn, email } = useUserContext();
const { genericName } = useAppContext();
const { provider, username, isLoggedIn, email, oauthName } = useUserContext();
const { t } = useTranslation();
const redirectTimer = useRef<number | null>(null);
@@ -67,8 +64,7 @@ export const LogoutPage = () => {
}}
values={{
username: email,
provider:
provider === "generic" ? genericName : capitalize(provider),
provider: oauthName,
}}
/>
) : (

View File

@@ -1,14 +1,19 @@
import { z } from "zod";
export const providerSchema = z.object({
id: z.string(),
name: z.string(),
oauth: z.boolean(),
});
export const appContextSchema = z.object({
configuredProviders: z.array(z.string()),
providers: z.array(providerSchema),
title: z.string(),
genericName: z.string(),
appUrl: z.string(),
cookieDomain: z.string(),
forgotPasswordMessage: z.string(),
oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]),
backgroundImage: z.string(),
oauthAutoRedirect: z.string(),
});
export type AppContextSchema = z.infer<typeof appContextSchema>;

View File

@@ -8,6 +8,7 @@ export const userContextSchema = z.object({
provider: z.string(),
oauth: z.boolean(),
totpPending: z.boolean(),
oauthName: z.string(),
});
export type UserContextSchema = z.infer<typeof userContextSchema>;

View File

@@ -0,0 +1 @@
ALTER TABLE "sessions" DROP COLUMN "oauth_name";

View File

@@ -0,0 +1,8 @@
ALTER TABLE "sessions" ADD COLUMN "oauth_name" TEXT;
UPDATE
"sessions"
SET
"oauth_name" = "Generic"
WHERE
"oauth_name" IS NULL AND "provider" IS NOT NULL;

View File

@@ -151,11 +151,13 @@ func (app *BootstrapApp) Setup() error {
continue
}
if provider.Name == "" && babysit[id] != "" {
provider.Name = babysit[id]
if provider.Name == "" {
if name, ok := babysit[id]; ok {
provider.Name = name
} else {
provider.Name = utils.Capitalize(id)
}
}
configuredProviders = append(configuredProviders, controller.Provider{
Name: provider.Name,

View File

@@ -84,6 +84,7 @@ type SessionCookie struct {
Provider string
TotpPending bool
OAuthGroups string
OAuthName string
}
type UserContext struct {
@@ -96,6 +97,7 @@ type UserContext struct {
TotpPending bool
OAuthGroups string
TotpEnabled bool
OAuthName string
}
// API responses and queries

View File

@@ -19,6 +19,7 @@ type UserContextResponse struct {
Provider string `json:"provider"`
OAuth bool `json:"oauth"`
TotpPending bool `json:"totpPending"`
OAuthName string `json:"oauthName"`
}
type AppContextResponse struct {
@@ -80,6 +81,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
Provider: context.Provider,
OAuth: context.OAuth,
TotpPending: context.TotpPending,
OAuthName: context.OAuthName,
}
if err != nil {

View File

@@ -186,6 +186,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
Email: user.Email,
Provider: req.Provider,
OAuthGroups: utils.CoalesceToString(user.Groups),
OAuthName: service.GetName(),
})
if err != nil {

View File

@@ -95,6 +95,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
Email: cookie.Email,
Provider: cookie.Provider,
OAuthGroups: cookie.OAuthGroups,
OAuthName: cookie.OAuthName,
IsLoggedIn: true,
OAuth: true,
})

View File

@@ -9,4 +9,5 @@ type Session struct {
TOTPPending bool `gorm:"column:totp_pending"`
OAuthGroups string `gorm:"column:oauth_groups"`
Expiry int64 `gorm:"column:expiry"`
OAuthName string `gorm:"column:oauth_name"`
}

View File

@@ -210,6 +210,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio
TOTPPending: data.TotpPending,
OAuthGroups: data.OAuthGroups,
Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
OAuthName: data.OAuthName,
}
err = auth.database.Create(&session).Error
@@ -278,6 +279,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
Provider: session.Provider,
TotpPending: session.TOTPPending,
OAuthGroups: session.OAuthGroups,
OAuthName: session.OAuthName,
}, nil
}

View File

@@ -22,6 +22,7 @@ type GenericOAuthService struct {
verifier string
insecureSkipVerify bool
userinfoUrl string
name string
}
func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthService {
@@ -38,6 +39,7 @@ func NewGenericOAuthService(config config.OAuthServiceConfig) *GenericOAuthServi
},
insecureSkipVerify: config.InsecureSkipVerify,
userinfoUrl: config.UserinfoURL,
name: config.Name,
}
}
@@ -115,3 +117,7 @@ func (generic *GenericOAuthService) Userinfo() (config.Claims, error) {
return user, nil
}
func (generic *GenericOAuthService) GetName() string {
return generic.name
}

View File

@@ -33,6 +33,7 @@ type GithubOAuthService struct {
context context.Context
token *oauth2.Token
verifier string
name string
}
func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService {
@@ -44,6 +45,7 @@ func NewGithubOAuthService(config config.OAuthServiceConfig) *GithubOAuthService
Scopes: GithubOAuthScopes,
Endpoint: endpoints.GitHub,
},
name: config.Name,
}
}
@@ -167,3 +169,7 @@ func (github *GithubOAuthService) Userinfo() (config.Claims, error) {
return user, nil
}
func (github *GithubOAuthService) GetName() string {
return github.name
}

View File

@@ -28,6 +28,7 @@ type GoogleOAuthService struct {
context context.Context
token *oauth2.Token
verifier string
name string
}
func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService {
@@ -39,6 +40,7 @@ func NewGoogleOAuthService(config config.OAuthServiceConfig) *GoogleOAuthService
Scopes: GoogleOAuthScopes,
Endpoint: endpoints.Google,
},
name: config.Name,
}
}
@@ -111,3 +113,7 @@ func (google *GoogleOAuthService) Userinfo() (config.Claims, error) {
return user, nil
}
func (google *GoogleOAuthService) GetName() string {
return google.name
}

View File

@@ -14,6 +14,7 @@ type OAuthService interface {
GetAuthURL(state string) string
VerifyCode(code string) error
Userinfo() (config.Claims, error)
GetName() string
}
type OAuthBrokerService struct {