diff --git a/dashboard/src/components/EnvComponents/OAuthConfig.tsx b/dashboard/src/components/EnvComponents/OAuthConfig.tsx index 5a24982..4536ade 100644 --- a/dashboard/src/components/EnvComponents/OAuthConfig.tsx +++ b/dashboard/src/components/EnvComponents/OAuthConfig.tsx @@ -9,7 +9,13 @@ import { Divider, useMediaQuery, } from '@chakra-ui/react'; -import { FaGoogle, FaGithub, FaFacebookF, FaLinkedin } from 'react-icons/fa'; +import { + FaGoogle, + FaGithub, + FaFacebookF, + FaLinkedin, + FaApple, +} from 'react-icons/fa'; import { TextInputType, HiddenInputType } from '../../constants'; const OAuthConfig = ({ @@ -216,7 +222,45 @@ const OAuthConfig = ({ fieldVisibility={fieldVisibility} setFieldVisibility={setFieldVisibility} inputType={HiddenInputType.LINKEDIN_CLIENT_SECRET} - placeholder="LinkedIn Secret" + placeholder="LinkedIn Client Secret" + /> + + + +
+ +
+
+ +
+
+
diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index 086bf82..8ceba5f 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -8,6 +8,8 @@ export const TextInputType = { GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID', FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID', LINKEDIN_CLIENT_ID: 'LINKEDIN_CLIENT_ID', + APPLE_CLIENT_ID: 'APPLE_CLIENT_ID', + APPLE_CLIENT_SECRET: 'APPLE_CLIENT_SECRET', JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM', REDIS_URL: 'REDIS_URL', SMTP_HOST: 'SMTP_HOST', @@ -33,6 +35,7 @@ export const HiddenInputType = { GITHUB_CLIENT_SECRET: 'GITHUB_CLIENT_SECRET', FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET', LINKEDIN_CLIENT_SECRET: 'LINKEDIN_CLIENT_SECRET', + APPLE_CLIENT_SECRET: 'APPLE_CLIENT_SECRET', JWT_SECRET: 'JWT_SECRET', SMTP_PASSWORD: 'SMTP_PASSWORD', ADMIN_SECRET: 'ADMIN_SECRET', @@ -103,6 +106,8 @@ export interface envVarTypes { FACEBOOK_CLIENT_SECRET: string; LINKEDIN_CLIENT_ID: string; LINKEDIN_CLIENT_SECRET: string; + APPLE_CLIENT_ID: string; + APPLE_CLIENT_SECRET: string; ROLES: [string] | []; DEFAULT_ROLES: [string] | []; PROTECTED_ROLES: [string] | []; diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index 221ed53..24cf670 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -28,6 +28,8 @@ export const EnvVariablesQuery = ` FACEBOOK_CLIENT_SECRET, LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, + APPLE_CLIENT_ID, + APPLE_CLIENT_SECRET, DEFAULT_ROLES, PROTECTED_ROLES, ROLES, diff --git a/dashboard/src/pages/Environment.tsx b/dashboard/src/pages/Environment.tsx index a9032cb..758f85c 100644 --- a/dashboard/src/pages/Environment.tsx +++ b/dashboard/src/pages/Environment.tsx @@ -48,6 +48,8 @@ const Environment = () => { FACEBOOK_CLIENT_SECRET: '', LINKEDIN_CLIENT_ID: '', LINKEDIN_CLIENT_SECRET: '', + APPLE_CLIENT_ID: '', + APPLE_CLIENT_SECRET: '', ROLES: [], DEFAULT_ROLES: [], PROTECTED_ROLES: [], @@ -86,6 +88,7 @@ const Environment = () => { GITHUB_CLIENT_SECRET: false, FACEBOOK_CLIENT_SECRET: false, LINKEDIN_CLIENT_SECRET: false, + APPLE_CLIENT_SECRET: false, JWT_SECRET: false, SMTP_PASSWORD: false, ADMIN_SECRET: false, diff --git a/server/constants/env.go b/server/constants/env.go index b5c3218..ad2db33 100644 --- a/server/constants/env.go +++ b/server/constants/env.go @@ -79,6 +79,10 @@ const ( EnvKeyLinkedInClientID = "LINKEDIN_CLIENT_ID" // EnvKeyLinkedinClientSecret key for env variable LINKEDIN_CLIENT_SECRET EnvKeyLinkedInClientSecret = "LINKEDIN_CLIENT_SECRET" + // EnvKeyAppleClientID key for env variable APPLE_CLIENT_ID + EnvKeyAppleClientID = "APPLE_CLIENT_ID" + // EnvKeyAppleClientSecret key for env variable APPLE_CLIENT_SECRET + EnvKeyAppleClientSecret = "APPLE_CLIENT_SECRET" // EnvKeyOrganizationName key for env variable ORGANIZATION_NAME EnvKeyOrganizationName = "ORGANIZATION_NAME" // EnvKeyOrganizationLogo key for env variable ORGANIZATION_LOGO diff --git a/server/env/env.go b/server/env/env.go index 397d052..11d8fed 100644 --- a/server/env/env.go +++ b/server/env/env.go @@ -70,6 +70,8 @@ func InitAllEnv() error { osFacebookClientSecret := os.Getenv(constants.EnvKeyFacebookClientSecret) osLinkedInClientID := os.Getenv(constants.EnvKeyLinkedInClientID) osLinkedInClientSecret := os.Getenv(constants.EnvKeyLinkedInClientSecret) + osAppleClientID := os.Getenv(constants.EnvKeyAppleClientID) + osAppleClientSecret := os.Getenv(constants.EnvKeyAppleClientSecret) osResetPasswordURL := os.Getenv(constants.EnvKeyResetPasswordURL) osOrganizationName := os.Getenv(constants.EnvKeyOrganizationName) osOrganizationLogo := os.Getenv(constants.EnvKeyOrganizationLogo) @@ -361,6 +363,20 @@ func InitAllEnv() error { envData[constants.EnvKeyLinkedInClientSecret] = osLinkedInClientSecret } + if val, ok := envData[constants.EnvKeyAppleClientID]; !ok || val == "" { + envData[constants.EnvKeyAppleClientID] = osAppleClientID + } + if osFacebookClientID != "" && envData[constants.EnvKeyAppleClientID] != osFacebookClientID { + envData[constants.EnvKeyAppleClientID] = osAppleClientID + } + + if val, ok := envData[constants.EnvKeyAppleClientSecret]; !ok || val == "" { + envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret + } + if osFacebookClientSecret != "" && envData[constants.EnvKeyAppleClientSecret] != osFacebookClientSecret { + envData[constants.EnvKeyAppleClientSecret] = osAppleClientSecret + } + if val, ok := envData[constants.EnvKeyResetPasswordURL]; !ok || val == "" { envData[constants.EnvKeyResetPasswordURL] = strings.TrimPrefix(osResetPasswordURL, "/") } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 5444d20..5a74a49 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -57,6 +57,8 @@ type ComplexityRoot struct { AdminSecret func(childComplexity int) int AllowedOrigins func(childComplexity int) int AppURL func(childComplexity int) int + AppleClientID func(childComplexity int) int + AppleClientSecret func(childComplexity int) int ClientID func(childComplexity int) int ClientSecret func(childComplexity int) int CustomAccessTokenScript func(childComplexity int) int @@ -113,6 +115,7 @@ type ComplexityRoot struct { Meta struct { ClientID func(childComplexity int) int + IsAppleLoginEnabled func(childComplexity int) int IsBasicAuthenticationEnabled func(childComplexity int) int IsEmailVerificationEnabled func(childComplexity int) int IsFacebookLoginEnabled func(childComplexity int) int @@ -335,6 +338,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Env.AppURL(childComplexity), true + case "Env.APPLE_CLIENT_ID": + if e.complexity.Env.AppleClientID == nil { + break + } + + return e.complexity.Env.AppleClientID(childComplexity), true + + case "Env.APPLE_CLIENT_SECRET": + if e.complexity.Env.AppleClientSecret == nil { + break + } + + return e.complexity.Env.AppleClientSecret(childComplexity), true + case "Env.CLIENT_ID": if e.complexity.Env.ClientID == nil { break @@ -664,6 +681,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Meta.ClientID(childComplexity), true + case "Meta.is_apple_login_enabled": + if e.complexity.Meta.IsAppleLoginEnabled == nil { + break + } + + return e.complexity.Meta.IsAppleLoginEnabled(childComplexity), true + case "Meta.is_basic_authentication_enabled": if e.complexity.Meta.IsBasicAuthenticationEnabled == nil { break @@ -1377,6 +1401,7 @@ type Meta { is_facebook_login_enabled: Boolean! is_github_login_enabled: Boolean! is_linkedin_login_enabled: Boolean! + is_apple_login_enabled: Boolean! is_email_verification_enabled: Boolean! is_basic_authentication_enabled: Boolean! is_magic_link_login_enabled: Boolean! @@ -1489,6 +1514,8 @@ type Env { FACEBOOK_CLIENT_SECRET: String LINKEDIN_CLIENT_ID: String LINKEDIN_CLIENT_SECRET: String + APPLE_CLIENT_ID: String + APPLE_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String } @@ -1538,6 +1565,8 @@ input UpdateEnvInput { FACEBOOK_CLIENT_SECRET: String LINKEDIN_CLIENT_ID: String LINKEDIN_CLIENT_SECRET: String + APPLE_CLIENT_ID: String + APPLE_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String } @@ -3695,6 +3724,70 @@ func (ec *executionContext) _Env_LINKEDIN_CLIENT_SECRET(ctx context.Context, fie return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } +func (ec *executionContext) _Env_APPLE_CLIENT_ID(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Env", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.AppleClientID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _Env_APPLE_CLIENT_SECRET(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Env", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.AppleClientSecret, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _Env_ORGANIZATION_NAME(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4135,6 +4228,41 @@ func (ec *executionContext) _Meta_is_linkedin_login_enabled(ctx context.Context, return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _Meta_is_apple_login_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Meta", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IsAppleLoginEnabled, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + func (ec *executionContext) _Meta_is_email_verification_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -8707,6 +8835,22 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob if err != nil { return it, err } + case "APPLE_CLIENT_ID": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("APPLE_CLIENT_ID")) + it.AppleClientID, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "APPLE_CLIENT_SECRET": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("APPLE_CLIENT_SECRET")) + it.AppleClientSecret, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } case "ORGANIZATION_NAME": var err error @@ -9179,6 +9323,10 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._Env_LINKEDIN_CLIENT_ID(ctx, field, obj) case "LINKEDIN_CLIENT_SECRET": out.Values[i] = ec._Env_LINKEDIN_CLIENT_SECRET(ctx, field, obj) + case "APPLE_CLIENT_ID": + out.Values[i] = ec._Env_APPLE_CLIENT_ID(ctx, field, obj) + case "APPLE_CLIENT_SECRET": + out.Values[i] = ec._Env_APPLE_CLIENT_SECRET(ctx, field, obj) case "ORGANIZATION_NAME": out.Values[i] = ec._Env_ORGANIZATION_NAME(ctx, field, obj) case "ORGANIZATION_LOGO": @@ -9295,6 +9443,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { invalids++ } + case "is_apple_login_enabled": + out.Values[i] = ec._Meta_is_apple_login_enabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "is_email_verification_enabled": out.Values[i] = ec._Meta_is_email_verification_enabled(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index d2fa5d7..d191cf8 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -67,6 +67,8 @@ type Env struct { FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` + AppleClientID *string `json:"APPLE_CLIENT_ID"` + AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` OrganizationName *string `json:"ORGANIZATION_NAME"` OrganizationLogo *string `json:"ORGANIZATION_LOGO"` } @@ -119,6 +121,7 @@ type Meta struct { IsFacebookLoginEnabled bool `json:"is_facebook_login_enabled"` IsGithubLoginEnabled bool `json:"is_github_login_enabled"` IsLinkedinLoginEnabled bool `json:"is_linkedin_login_enabled"` + IsAppleLoginEnabled bool `json:"is_apple_login_enabled"` IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"` IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"` IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"` @@ -221,6 +224,8 @@ type UpdateEnvInput struct { FacebookClientSecret *string `json:"FACEBOOK_CLIENT_SECRET"` LinkedinClientID *string `json:"LINKEDIN_CLIENT_ID"` LinkedinClientSecret *string `json:"LINKEDIN_CLIENT_SECRET"` + AppleClientID *string `json:"APPLE_CLIENT_ID"` + AppleClientSecret *string `json:"APPLE_CLIENT_SECRET"` OrganizationName *string `json:"ORGANIZATION_NAME"` OrganizationLogo *string `json:"ORGANIZATION_LOGO"` } diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 5303b34..caaf805 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -19,6 +19,7 @@ type Meta { is_facebook_login_enabled: Boolean! is_github_login_enabled: Boolean! is_linkedin_login_enabled: Boolean! + is_apple_login_enabled: Boolean! is_email_verification_enabled: Boolean! is_basic_authentication_enabled: Boolean! is_magic_link_login_enabled: Boolean! @@ -131,6 +132,8 @@ type Env { FACEBOOK_CLIENT_SECRET: String LINKEDIN_CLIENT_ID: String LINKEDIN_CLIENT_SECRET: String + APPLE_CLIENT_ID: String + APPLE_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String } @@ -180,6 +183,8 @@ input UpdateEnvInput { FACEBOOK_CLIENT_SECRET: String LINKEDIN_CLIENT_ID: String LINKEDIN_CLIENT_SECRET: String + APPLE_CLIENT_ID: String + APPLE_CLIENT_SECRET: String ORGANIZATION_NAME: String ORGANIZATION_LOGO: String } diff --git a/server/oauth/oauth.go b/server/oauth/oauth.go index 5e43f5b..02b90c3 100644 --- a/server/oauth/oauth.go +++ b/server/oauth/oauth.go @@ -19,6 +19,7 @@ type OAuthProvider struct { GithubConfig *oauth2.Config FacebookConfig *oauth2.Config LinkedInConfig *oauth2.Config + AppleConfig *oauth2.Config } // OIDCProviders is a struct that contains reference all the OpenID providers @@ -112,5 +113,26 @@ func InitOAuth() error { } } + appleClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppleClientID) + if err != nil { + appleClientID = "" + } + appleClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppleClientSecret) + if err != nil { + appleClientSecret = "" + } + if appleClientID != "" && appleClientSecret != "" { + OAuthProviders.AppleConfig = &oauth2.Config{ + ClientID: appleClientID, + ClientSecret: appleClientID, + RedirectURL: "/oauth_callback/apple", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://appleid.apple.com/auth/authorize", + TokenURL: "https://appleid.apple.com/auth/token", + }, + Scopes: []string{"email"}, + } + } + return nil } diff --git a/server/resolvers/env.go b/server/resolvers/env.go index a8b9a95..8578444 100644 --- a/server/resolvers/env.go +++ b/server/resolvers/env.go @@ -136,6 +136,12 @@ func EnvResolver(ctx context.Context) (*model.Env, error) { if val, ok := store[constants.EnvKeyLinkedInClientSecret]; ok { res.LinkedinClientSecret = utils.NewStringRef(val.(string)) } + if val, ok := store[constants.EnvKeyAppleClientID]; ok { + res.AppleClientID = utils.NewStringRef(val.(string)) + } + if val, ok := store[constants.EnvKeyAppleClientSecret]; ok { + res.AppleClientSecret = utils.NewStringRef(val.(string)) + } if val, ok := store[constants.EnvKeyOrganizationName]; ok { res.OrganizationName = utils.NewStringRef(val.(string)) } diff --git a/server/resolvers/meta.go b/server/resolvers/meta.go index f17775b..f8d938a 100644 --- a/server/resolvers/meta.go +++ b/server/resolvers/meta.go @@ -43,16 +43,28 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) { linkedClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientID) if err != nil { - log.Debug("Failed to get Facebook Client ID from environment variable", err) + log.Debug("Failed to get LinkedIn Client ID from environment variable", err) linkedClientID = "" } linkedInClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyLinkedInClientSecret) if err != nil { - log.Debug("Failed to get Facebook Client Secret from environment variable", err) + log.Debug("Failed to get LinkedIn Client Secret from environment variable", err) linkedInClientSecret = "" } + appleClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppleClientID) + if err != nil { + log.Debug("Failed to get Apple Client ID from environment variable", err) + appleClientID = "" + } + + appleClientSecret, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyAppleClientSecret) + if err != nil { + log.Debug("Failed to get Apple Client Secret from environment variable", err) + appleClientSecret = "" + } + githubClientID, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID) if err != nil { log.Debug("Failed to get Github Client ID from environment variable", err) @@ -96,6 +108,7 @@ func MetaResolver(ctx context.Context) (*model.Meta, error) { IsGithubLoginEnabled: githubClientID != "" && githubClientSecret != "", IsFacebookLoginEnabled: facebookClientID != "" && facebookClientSecret != "", IsLinkedinLoginEnabled: linkedClientID != "" && linkedInClientSecret != "", + IsAppleLoginEnabled: appleClientID != "" && appleClientSecret != "", IsBasicAuthenticationEnabled: !isBasicAuthDisabled, IsEmailVerificationEnabled: !isEmailVerificationDisabled, IsMagicLinkLoginEnabled: !isMagicLinkLoginDisabled,