+
+ ))}
)
}
diff --git a/admin-panel/translations/en.json b/admin-panel/translations/en.json
index bf8b30c..1f496f3 100644
--- a/admin-panel/translations/en.json
+++ b/admin-panel/translations/en.json
@@ -39,7 +39,14 @@
"deleteConfirmBtn": "Delete"
},
"dashboard": {
- "configs": "System Configs",
+ "brandingConfigs": "Branding Configs",
+ "localeConfigs": "Locale Configs",
+ "suppressionConfigs": "Suppression Configs",
+ "mfaConfigs": "MFA Configs",
+ "authConfigs": "Auth Configs",
+ "bruteForceConfigs": "Brute-force Configs",
+ "ssoConfigs": "SSO Configs",
+ "logConfigs": "Log Configs",
"configName": "Name",
"configValue": "Value",
"configSeconds": "seconds",
diff --git a/admin-panel/translations/fr.json b/admin-panel/translations/fr.json
index 0826bf6..40404a9 100644
--- a/admin-panel/translations/fr.json
+++ b/admin-panel/translations/fr.json
@@ -39,7 +39,14 @@
"deleteConfirmBtn": "Supprimer"
},
"dashboard": {
- "configs": "Configurations système",
+ "brandingConfigs": "Configurations de marque",
+ "localeConfigs": "Configurations de langue",
+ "suppressionConfigs": "Configurations de suppression",
+ "mfaConfigs": "Configurations MFA",
+ "authConfigs": "Configurations d'authentification",
+ "bruteForceConfigs": "Configurations de force brute",
+ "ssoConfigs": "Configurations SSO",
+ "logConfigs": "Configurations des journaux",
"configName": "Nom",
"configValue": "Valeur",
"configSeconds": "secondes",
diff --git a/docs/auth-server.md b/docs/auth-server.md
index a764ccf..884142b 100644
--- a/docs/auth-server.md
+++ b/docs/auth-server.md
@@ -219,157 +219,186 @@ cd server
npm run prod:deploy
```
-### AUTHORIZATION_CODE_EXPIRES_IN
-- **Default:** 300 (5 minutes)
-- **Description:** Determines how long the authorization code is valid before it expires.
-### SPA_ACCESS_TOKEN_EXPIRES_IN
-- **Default:** 1800 (30 minutes)
-- **Description:** Determines how long the access token granted for single page applications is valid before it expires.
+### Branding Configs
-### SPA_REFRESH_TOKEN_EXPIRES_IN
-- **Default:** 604800 (7 days)
-- **Description:** Determines how long the refresh token granted for single page applications is valid before it expires.
+#### COMPANY_LOGO_URL
+- **Default:** https://raw.githubusercontent.com/ValueMelody/melody-homepage/main/logo.jpg
+- **Description:** The logo used for branding.
-### S2S_ACCESS_TOKEN_EXPIRES_IN
-- **Default:** 3600 (1 hour)
-- **Description:** Determines how long the access token granted for server-to-server applications is valid before it expires.
+#### BG_COLOR
+- **Default:** lightgray
+- **Description:** Specifies the background color of authentication pages.
-### ID_TOKEN_EXPIRES_IN
-- **Default:** 1800 (30 minutes)
-- **Description:** Determines how long the ID token is valid before it expires.
+#### PRIMARY_BUTTON_BG_COLOR
+- **Default:** white
+- **Description:** Specifies the background color of primary buttons on authentication pages.
-### SERVER_SESSION_EXPIRES_IN
-- **Default:** 1800 (30 minutes)
-- **Description:** Determines how long the server session is valid before it expires. If set to 0, the server session will be disabled.
-
-### COMPANY_LOGO_URL
-- **Default:** https://raw.githubusercontent.com/ValueMelody/melody-homepage/main/logo.jpg
-- **Description:** The logo used for branding.
+#### PRIMARY_BUTTON_FG_COLOR
+- **Default:** black
+- **Description:** Specifies the foreground color of primary buttons on authentication pages.
-### EMAIL_SENDER_NAME
+#### EMAIL_SENDER_NAME
- **Default:** "Melody Auth"
- **Description:** The sender name that appears in emails.
-### GOOGLE_AUTH_CLIENT_ID
+#### TERMS_LINK
- **Default:** ""
-- **Description:** The Google Authentication Client ID is required to enable the Google Sign-In function. This ID is obtained from the Google Developer Console and uniquely identifies your application to Google. If this value is left empty, the Google Sign-In button will be suppressed and the Google sign-in functionality will not be available.
+- **Description:** URL to display a link to your Terms of Service on the sign-up page.
-### FACEBOOK_AUTH_CLIENT_ID
+#### PRIVACY_POLICY_LINK
- **Default:** ""
-- **Description:** The Facebook Authentication Client ID is required to enable the Facebook Sign-In function. This ID is obtained from the Facebook Developer Console and uniquely identifies your application to Facebook. If this value is left empty, the Facebook Sign-In button will be suppressed and the Facebook sign-in functionality will not be available. You also need to set FACEBOOK_AUTH_CLIENT_SECRET in .dev.vars for Cloudflare dev env as well as Node env, and in Cloudflare workers config for Cloudflare prod env.
+- **Description:** URL to display a link to your Privacy Policy on the sign-up page.
-### GITHUB_AUTH_CLIENT_ID & GITHUB_AUTH_APP_NAME
-- **Default:** ""
-- **Description:** The GitHub Authentication Client ID and App Name is required to enable the GitHub Sign-In function. This Client ID and App Name is obtained from the GitHub Developer Console and uniquely identifies your application to GitHub. If this value is left empty, the GitHub Sign-In button will be suppressed and the GitHub sign-in functionality will not be available. You also need to set GITHUB_AUTH_CLIENT_SECRET in .dev.vars for Cloudflare dev env as well as Node env, and in Cloudflare workers config for Cloudflare prod env. In your GitHub App settings, set the callback URL to [your auth server doamin]/identity/v1/authorize-github, e.g., http://localhost:8787/identity/v1/authorize-github
+### Locale Configs
-### ENABLE_SIGN_UP
+#### SUPPORTED_LOCALES
+- **Default:** ['en', 'fr']
+- **Description:** Specifies the locales supported for identity pages and emails.
+
+#### ENABLE_LOCALE_SELECTOR
+- **Default:** true
+- **Description:** Determines whether users can switch to a different locale on identity pages. If only one locale is supported (`SUPPORTED_LOCALE`), the locale selector will be suppressed, regardless of this setting.
+
+### Suppression Configs
+
+#### ENABLE_SIGN_UP
- **Default:** true
- **Description:** Determines if user sign-up is allowed. If set to false, the sign-up button will be suppressed on the sign-in page.
-### ENABLE_PASSWORD_SIGN_IN
+#### ENABLE_PASSWORD_SIGN_IN
- **Default:** true
- **Description:** Determines if password sign-in is allowed. If you only want to support social sign-in, you can set ENABLE_SIGN_UP, ENABLE_PASSWORD_SIGN_IN and ENABLE_PASSWORD_RESET to false.
-### ENABLE_PASSWORD_RESET
+#### ENABLE_PASSWORD_RESET
- **Default:** true
- **Description:** Determines if user password reset is allowed. If set to false, the reset password button will be suppressed on the sign-in page.
[Email functionality setup required](#email-functionality-setup)
-### PASSWORD_RESET_EMAIL_THRESHOLD
-- **Default:** 5
-- **Description:** Limits the number of password reset email requests allowed per email and IP address per day to protect against abuse. 0 means no restriction
-
-### ENABLE_NAMES
+#### ENABLE_NAMES
- **Default:** true
- **Description:** Provides fields for users to enter their first and last names during sign-up. If set to false, the first and last name fields will not show up on the sign-up page.
-### NAMES_IS_REQUIRED
+#### NAMES_IS_REQUIRED
- **Default:** false
- **Description:** Determines if users are required to provide their first and last names during sign-up.
-### ENABLE_USER_APP_CONSENT
+#### ENABLE_USER_APP_CONSENT
- **Default:** true
- **Description:** Requires users to consent to grant access to each app after authentication.
-### ENABLE_EMAIL_VERIFICATION
+#### ENABLE_EMAIL_VERIFICATION
- **Default:** true
- **Description:** If set to true, users will receive an email to verify their email address after signing up.
[Email functionality setup required](#email-functionality-setup)
-### OTP_MFA_IS_REQUIRED
+### Auth Configs
+
+#### AUTHORIZATION_CODE_EXPIRES_IN
+- **Default:** 300 (5 minutes)
+- **Description:** Determines how long the authorization code is valid before it expires.
+
+#### SPA_ACCESS_TOKEN_EXPIRES_IN
+- **Default:** 1800 (30 minutes)
+- **Description:** Determines how long the access token granted for single page applications is valid before it expires.
+
+#### SPA_REFRESH_TOKEN_EXPIRES_IN
+- **Default:** 604800 (7 days)
+- **Description:** Determines how long the refresh token granted for single page applications is valid before it expires.
+
+#### S2S_ACCESS_TOKEN_EXPIRES_IN
+- **Default:** 3600 (1 hour)
+- **Description:** Determines how long the access token granted for server-to-server applications is valid before it expires.
+
+#### ID_TOKEN_EXPIRES_IN
+- **Default:** 1800 (30 minutes)
+- **Description:** Determines how long the ID token is valid before it expires.
+
+#### SERVER_SESSION_EXPIRES_IN
+- **Default:** 1800 (30 minutes)
+- **Description:** Determines how long the server session is valid before it expires. If set to 0, the server session will be disabled.
+
+### MFA Configs
+
+#### OTP_MFA_IS_REQUIRED
- **Default:** false
- **Description:** Enables OTP-based multi-factor authentication (MFA) for user sign-in. When set to true, users are required to configure OTP using an app like Google Authenticator during the sign-in process.
-### SMS_MFA_IS_REQUIRED
+#### SMS_MFA_IS_REQUIRED
- **Default:** false
- **Description:** Controls sms-based multi-factor authentication (MFA) for user sign-in. If set to true, users receive an MFA code via sms to confirm their login.
[SMS functionality setup required](#sms-functionality-setup)
-### SMS_MFA_MESSAGE_THRESHOLD
-- **Default:** 5
-- **Description:** Maximum number of SMS MFA message requests allowed per 30 minutes for a single account based on ip address. 0 means no restriction.
-
-### EMAIL_MFA_IS_REQUIRED
+#### EMAIL_MFA_IS_REQUIRED
- **Default:** false
- **Description:** Controls email-based multi-factor authentication (MFA) for user sign-in. If set to true, users receive an MFA code via email to confirm their login.
[Email functionality setup required](#email-functionality-setup)
-### EMAIL_MFA_EMAIL_THRESHOLD
-- **Default:** 10
-- **Description:** Maximum number of Email MFA email requests allowed per 30 minutes for a single account based on ip address. 0 means no restriction.
-
-### CHANGE_EMAIL_EMAIL_THRESHOLD
-- **Default:** 5
-- **Description:** Maximum number of Change Email verification code requests allowed per 30 minutes for a single account. 0 means no restriction.
-
-### ENFORCE_ONE_MFA_ENROLLMENT
+#### ENFORCE_ONE_MFA_ENROLLMENT
- **Default:** ['otp', 'email']
- **Description:** Enforce one MFA type from the list. Available options are ‘email’, ‘otp’, and ‘sms’. This setting is only effective if OTP_MFA_IS_REQUIRED, SMS_MFA_IS_REQUIRED, and EMAIL_MFA_IS_REQUIRED are all set to false. An empty list means no MFA type will be enforced. You must enable email functionality for the email MFA option to work.
[Email functionality setup required](#email-functionality-setup)
-### ALLOW_EMAIL_MFA_AS_BACKUP
+#### ALLOW_EMAIL_MFA_AS_BACKUP
- **Default:** true
- **Description:** This setting allows users to use email-based MFA as an alternative method for signing in if they are enrolled in OTP MFA or SMS MFA and not enrolled in email MFA.
[Email functionality setup required](#email-functionality-setup)
-### ACCOUNT_LOCKOUT_THRESHOLD
-- **Default:** 5
-- **Description:** Number of failed login attempts before the user account is locked. 0 means no restriction.
+### Brute-force Configs
-### ACCOUNT_LOCKOUT_EXPIRES_IN
+#### ACCOUNT_LOCKOUT_EXPIRES_IN
- **Default:** 86400 (1 day)
- **Description:** Duration (in seconds) for which the account remains locked after reaching the lockout threshold. Set to 0 for indefinite lockout until manual intervention.
-### UNLOCK_ACCOUNT_VIA_PASSWORD_RESET
+#### UNLOCK_ACCOUNT_VIA_PASSWORD_RESET
- **Default:** true
- **Description:** User can unlock their account by reset password.
[Email functionality setup required](#email-functionality-setup)
-### SUPPORTED_LOCALES
-- **Default:** ['en', 'fr']
-- **Description:** Specifies the locales supported for identity pages and emails.
+#### PASSWORD_RESET_EMAIL_THRESHOLD
+- **Default:** 5
+- **Description:** Limits the number of password reset email requests allowed per email and IP address per day to protect against abuse. 0 means no restriction
-### ENABLE_LOCALE_SELECTOR
-- **Default:** true
-- **Description:** Determines whether users can switch to a different locale on identity pages. If only one locale is supported (`SUPPORTED_LOCALE`), the locale selector will be suppressed, regardless of this setting.
+#### EMAIL_MFA_EMAIL_THRESHOLD
+- **Default:** 10
+- **Description:** Maximum number of Email MFA email requests allowed per 30 minutes for a single account based on ip address. 0 means no restriction.
+
+#### CHANGE_EMAIL_EMAIL_THRESHOLD
+- **Default:** 5
+- **Description:** Maximum number of Change Email verification code requests allowed per 30 minutes for a single account. 0 means no restriction.
+
+#### SMS_MFA_MESSAGE_THRESHOLD
+- **Default:** 5
+- **Description:** Maximum number of SMS MFA message requests allowed per 30 minutes for a single account based on ip address. 0 means no restriction.
+
+#### ACCOUNT_LOCKOUT_THRESHOLD
+- **Default:** 5
+- **Description:** Number of failed login attempts before the user account is locked. 0 means no restriction.
+
+### Social Sign-in Configs
-### TERMS_LINK
+#### GOOGLE_AUTH_CLIENT_ID
- **Default:** ""
-- **Description:** URL to display a link to your Terms of Service on the sign-up page.
+- **Description:** The Google Authentication Client ID is required to enable the Google Sign-In function. This ID is obtained from the Google Developer Console and uniquely identifies your application to Google. If this value is left empty, the Google Sign-In button will be suppressed and the Google sign-in functionality will not be available.
-### PRIVACY_POLICY_LINK
+#### FACEBOOK_AUTH_CLIENT_ID
- **Default:** ""
-- **Description:** URL to display a link to your Privacy Policy on the sign-up page.
+- **Description:** The Facebook Authentication Client ID is required to enable the Facebook Sign-In function. This ID is obtained from the Facebook Developer Console and uniquely identifies your application to Facebook. If this value is left empty, the Facebook Sign-In button will be suppressed and the Facebook sign-in functionality will not be available. You also need to set FACEBOOK_AUTH_CLIENT_SECRET in .dev.vars for Cloudflare dev env as well as Node env, and in Cloudflare workers config for Cloudflare prod env.
+
+#### GITHUB_AUTH_CLIENT_ID & GITHUB_AUTH_APP_NAME
+- **Default:** ""
+- **Description:** The GitHub Authentication Client ID and App Name is required to enable the GitHub Sign-In function. This Client ID and App Name is obtained from the GitHub Developer Console and uniquely identifies your application to GitHub. If this value is left empty, the GitHub Sign-In button will be suppressed and the GitHub sign-in functionality will not be available. You also need to set GITHUB_AUTH_CLIENT_SECRET in .dev.vars for Cloudflare dev env as well as Node env, and in Cloudflare workers config for Cloudflare prod env. In your GitHub App settings, set the callback URL to [your auth server doamin]/identity/v1/authorize-github, e.g., http://localhost:8787/identity/v1/authorize-github
+
+### Log Configs
-### ENABLE_EMAIL_LOG
+#### ENABLE_EMAIL_LOG
- **Default:** false
- **Description:** Specify whether email should be logged. If enabled, ensure that you implement your own email log cleanup scheduler.
-### ENABLE_SMS_LOG
+#### ENABLE_SMS_LOG
- **Default:** false
- **Description:** Specify whether SMS should be logged. If enabled, ensure that you implement your own SMS log cleanup scheduler.
-### ENABLE_SIGN_IN_LOG
+#### ENABLE_SIGN_IN_LOG
- **Default:** false
- **Description:** Specify whether the user’s sign-in IP (only applicable in production environments) and location details (only applicable in Cloudflare environments) should be logged. If enabled, ensure that you implement your own sign-in log cleanup scheduler, clearly disclose the collection of IP and location data in your privacy policy, and comply with all relevant legal requirements.
diff --git a/server/src/__tests__/normal/other.test.tsx b/server/src/__tests__/normal/other.test.tsx
index afb8c5e..87fa4ab 100644
--- a/server/src/__tests__/normal/other.test.tsx
+++ b/server/src/__tests__/normal/other.test.tsx
@@ -74,6 +74,10 @@ describe(
ENABLE_EMAIL_LOG: false,
ENABLE_SMS_LOG: false,
ENABLE_SIGN_IN_LOG: false,
+ PRIMARY_BUTTON_BG_COLOR: 'white',
+ PRIMARY_BUTTON_FG_COLOR: 'black',
+ BG_COLOR: 'lightgray',
+ ENABLE_PASSWORD_SIGN_IN: true,
})
},
)
diff --git a/server/src/configs/type.ts b/server/src/configs/type.ts
index 8cbe5a2..bca5063 100644
--- a/server/src/configs/type.ts
+++ b/server/src/configs/type.ts
@@ -35,6 +35,9 @@ export type Bindings = {
ID_TOKEN_EXPIRES_IN: number;
SERVER_SESSION_EXPIRES_IN: number;
COMPANY_LOGO_URL: string;
+ BG_COLOR: string;
+ PRIMARY_BUTTON_BG_COLOR: string;
+ PRIMARY_BUTTON_FG_COLOR: string;
EMAIL_SENDER_NAME: string;
SMTP_SENDER_ADDRESS: string;
AUTH_SERVER_URL: string;
diff --git a/server/src/handlers/identity/main.tsx b/server/src/handlers/identity/main.tsx
index 19098d8..7e2e3de 100644
--- a/server/src/handlers/identity/main.tsx
+++ b/server/src/handlers/identity/main.tsx
@@ -10,7 +10,7 @@ import {
identityDto, oauthDto,
} from 'dtos'
import {
- appService, consentService, emailService, kvService, scopeService, userService,
+ appService, brandingService, consentService, emailService, kvService, scopeService, userService,
} from 'services'
import {
identityUtil,
@@ -27,7 +27,6 @@ export const getAuthorizePassword = async (c: Context) => {
const queryDto = await oauthHandler.parseGetAuthorizeDto(c)
const {
- COMPANY_LOGO_URL: logoUrl,
ENABLE_SIGN_UP: allowSignUp,
ENABLE_PASSWORD_RESET: allowPasswordReset,
ENABLE_PASSWORD_SIGN_IN: allowPasswordSignIn,
@@ -55,7 +54,7 @@ export const getAuthorizePassword = async (c: Context) => {
queryString={queryString}
locales={enableLocaleSelector ? locales : [queryDto.locale]}
queryDto={queryDto}
- logoUrl={logoUrl}
+ branding={brandingService.getDefaultBranding(c)}
enableSignUp={enableSignUp}
enablePasswordReset={enablePasswordReset}
enablePasswordSignIn={enablePasswordSignIn}
@@ -125,7 +124,6 @@ export const getAuthorizeAccount = async (c: Context) => {
const queryDto = await oauthHandler.parseGetAuthorizeDto(c)
const {
- COMPANY_LOGO_URL: logoUrl,
ENABLE_NAMES: enableNames,
NAMES_IS_REQUIRED: namesIsRequired,
SUPPORTED_LOCALES: locales,
@@ -142,7 +140,7 @@ export const getAuthorizeAccount = async (c: Context) => {
locales={enableLocaleSelector ? locales : [queryDto.locale]}
queryString={queryString}
queryDto={queryDto}
- logoUrl={logoUrl}
+ branding={brandingService.getDefaultBranding(c)}
enableNames={enableNames}
namesIsRequired={namesIsRequired}
/>)
@@ -241,14 +239,13 @@ export const getAuthorizeConsent = async (c: Context) => {
)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
} = env(c)
return c.html() => {
if (authCodeStore.user.mfaTypes.length) throw new errorConfig.Forbidden(localeConfig.Error.MfaEnrolled)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
ENFORCE_ONE_MFA_ENROLLMENT: mfaTypes,
} = env(c)
return c.html() => {
if (authCodeStore.user.otpVerified) throw new errorConfig.Forbidden(localeConfig.Error.OtpAlreadySet)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
} = env(c)
@@ -262,7 +261,7 @@ export const getAuthorizeOtpSetup = async (c: Context) => {
const otp = `otpauth://totp/${authCodeStore.appName}:${authCodeStore.user.email}?secret=${authCodeStore.user.otpSecret}&issuer=melody-auth&algorithm=SHA1&digits=6&period=30`
return c.html() => {
const queryDto = await identityDto.parseGetAuthorizeFollowUpReq(c)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
} = env(c)
@@ -291,7 +289,7 @@ export const getAuthorizeOtpMfa = async (c: Context) => {
)
return c.html() => {
await validateUtil.dto(queryDto)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
SMS_MFA_IS_REQUIRED: enableSmsMfa,
@@ -397,7 +394,7 @@ export const getAuthorizeSmsMfa = async (c: Context) => {
return c.html() => {
await validateUtil.dto(queryDto)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
} = env(c)
@@ -530,7 +526,7 @@ export const getAuthorizeEmailMfa = async (c: Context) => {
}
return c.html() => {
await validateUtil.dto(queryDto)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
} = env(c)
return c.html()
}
@@ -62,7 +62,6 @@ export const postVerifyEmail = async (c: Context) => {
export const getAuthorizeReset = async (c: Context) => {
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
} = env(c)
@@ -71,7 +70,7 @@ export const getAuthorizeReset = async (c: Context) => {
return c.html()
@@ -145,15 +144,12 @@ export const postAuthorizeReset = async (c: Context) => {
}
export const getAuthCodeExpired = async (c: Context) => {
- const {
- SUPPORTED_LOCALES: locales,
- COMPANY_LOGO_URL: logoUrl,
- } = env(c)
+ const { SUPPORTED_LOCALES: locales } = env(c)
const locale = c.req.query('locale') || locales[0]
return c.html()
}
diff --git a/server/src/handlers/identity/policy.tsx b/server/src/handlers/identity/policy.tsx
index 1fba781..2c134b2 100644
--- a/server/src/handlers/identity/policy.tsx
+++ b/server/src/handlers/identity/policy.tsx
@@ -7,6 +7,7 @@ import {
} from 'configs'
import { identityDto } from 'dtos'
import {
+ brandingService,
emailService,
kvService, userService,
} from 'services'
@@ -33,14 +34,13 @@ export const getChangePassword = async (c: Context) => {
checkAccount(authInfo.user)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
} = env(c)
return c.html()
@@ -79,14 +79,13 @@ export const getChangeEmail = async (c: Context) => {
checkAccount(authInfo.user)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
} = env(c)
return c.html()
@@ -181,14 +180,13 @@ export const getResetMfa = async (c: Context) => {
checkAccount(authInfo.user)
const {
- COMPANY_LOGO_URL: logoUrl,
SUPPORTED_LOCALES: locales,
ENABLE_LOCALE_SELECTOR: enableLocaleSelector,
} = env(c)
return c.html()
diff --git a/server/src/handlers/other.ts b/server/src/handlers/other.ts
index 5565f7d..eb57126 100644
--- a/server/src/handlers/other.ts
+++ b/server/src/handlers/other.ts
@@ -50,6 +50,10 @@ export const getSystemInfo = async (c: Context) => {
ENABLE_EMAIL_LOG: environment.ENABLE_EMAIL_LOG,
ENABLE_SMS_LOG: environment.ENABLE_SMS_LOG,
ENABLE_SIGN_IN_LOG: environment.ENABLE_SIGN_IN_LOG,
+ BG_COLOR: environment.BG_COLOR,
+ PRIMARY_BUTTON_BG_COLOR: environment.PRIMARY_BUTTON_BG_COLOR,
+ PRIMARY_BUTTON_FG_COLOR: environment.PRIMARY_BUTTON_FG_COLOR,
+ ENABLE_PASSWORD_SIGN_IN: environment.ENABLE_PASSWORD_SIGN_IN,
}
return c.json({ configs })
diff --git a/server/src/services/branding.ts b/server/src/services/branding.ts
new file mode 100644
index 0000000..dae165d
--- /dev/null
+++ b/server/src/services/branding.ts
@@ -0,0 +1,20 @@
+import { Context } from 'hono'
+import { env } from 'hono/adapter'
+import { typeConfig } from 'configs'
+import { Branding } from 'views/components/Layout'
+
+export const getDefaultBranding = (c: Context): Branding => {
+ const {
+ COMPANY_LOGO_URL: logoUrl,
+ BG_COLOR: bgColor,
+ PRIMARY_BUTTON_BG_COLOR: primaryButtonBgColor,
+ PRIMARY_BUTTON_FG_COLOR: primaryButtonFgColor,
+ } = env(c)
+
+ return {
+ logoUrl,
+ bgColor,
+ primaryButtonBgColor,
+ primaryButtonFgColor,
+ }
+}
diff --git a/server/src/services/index.ts b/server/src/services/index.ts
index 0d52f9c..1bc7063 100644
--- a/server/src/services/index.ts
+++ b/server/src/services/index.ts
@@ -9,3 +9,4 @@ export * as smsService from 'services/sms'
export * as roleService from 'services/role'
export * as scopeService from 'services/scope'
export * as logService from 'services/log'
+export * as brandingService from 'services/branding'
diff --git a/server/src/views/AuthCodeExpired.tsx b/server/src/views/AuthCodeExpired.tsx
index cd6a7c6..a7e49f6 100644
--- a/server/src/views/AuthCodeExpired.tsx
+++ b/server/src/views/AuthCodeExpired.tsx
@@ -2,17 +2,17 @@ import {
localeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
const AuthCodeExpired = ({
- logoUrl, locale,
+ branding, locale,
}: {
- logoUrl: string;
+ branding: Branding;
locale: typeConfig.Locale;
}) => {
return (
diff --git a/server/src/views/AuthorizeAccount.tsx b/server/src/views/AuthorizeAccount.tsx
index b6f100b..b6fb8df 100644
--- a/server/src/views/AuthorizeAccount.tsx
+++ b/server/src/views/AuthorizeAccount.tsx
@@ -4,7 +4,7 @@ import {
typeConfig,
} from 'configs'
import { oauthDto } from 'dtos'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import {
requestScript, resetErrorScript, responseScript, validateScript,
} from 'views/scripts'
@@ -15,7 +15,7 @@ import Field from 'views/components/Field'
const AuthorizeAccount = ({
queryDto,
- logoUrl,
+ branding,
enableNames,
namesIsRequired,
queryString,
@@ -24,7 +24,7 @@ const AuthorizeAccount = ({
privacyPolicyLink,
}: {
queryDto: oauthDto.GetAuthorizeReqDto;
- logoUrl: string;
+ branding: Branding;
enableNames: boolean;
namesIsRequired: boolean;
queryString: string;
@@ -35,7 +35,7 @@ const AuthorizeAccount = ({
return (
diff --git a/server/src/views/AuthorizeConsent.tsx b/server/src/views/AuthorizeConsent.tsx
index 690a590..7068bfc 100644
--- a/server/src/views/AuthorizeConsent.tsx
+++ b/server/src/views/AuthorizeConsent.tsx
@@ -4,7 +4,7 @@ import {
localeConfig, routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import { identityDto } from 'dtos'
import { responseScript } from 'views/scripts'
import SubmitError from 'views/components/SubmitError'
@@ -12,10 +12,10 @@ import Title from 'views/components/Title'
import { scopeModel } from 'models'
const AuthorizeConsent = ({
- queryDto, logoUrl, appName, scopes, locales, redirectUri,
+ queryDto, branding, appName, scopes, locales, redirectUri,
}: {
queryDto: identityDto.GetAuthorizeFollowUpReqDto;
- logoUrl: string;
+ branding: Branding;
appName: string;
scopes: scopeModel.ApiRecord[];
locales: typeConfig.Locale[];
@@ -24,7 +24,7 @@ const AuthorizeConsent = ({
return (
diff --git a/server/src/views/AuthorizeEmailMfa.tsx b/server/src/views/AuthorizeEmailMfa.tsx
index 0b4647e..d0042c5 100644
--- a/server/src/views/AuthorizeEmailMfa.tsx
+++ b/server/src/views/AuthorizeEmailMfa.tsx
@@ -3,7 +3,7 @@ import {
localeConfig, routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import { identityDto } from 'dtos'
import {
responseScript,
@@ -15,17 +15,17 @@ import SubmitButton from 'views/components/SubmitButton'
import CodeInput from 'views/components/CodeInput'
const AuthorizeEmailMfa = ({
- queryDto, logoUrl, locales, error,
+ queryDto, branding, locales, error,
}: {
queryDto: identityDto.GetAuthorizeFollowUpReqDto;
- logoUrl: string;
+ branding: Branding;
locales: typeConfig.Locale[];
error?: { en: string; fr: string };
}) => {
return (
{!error && (
diff --git a/server/src/views/AuthorizeMfaEnroll.tsx b/server/src/views/AuthorizeMfaEnroll.tsx
index aaf7a58..2648507 100644
--- a/server/src/views/AuthorizeMfaEnroll.tsx
+++ b/server/src/views/AuthorizeMfaEnroll.tsx
@@ -3,7 +3,7 @@ import {
localeConfig, routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import { identityDto } from 'dtos'
import { responseScript } from 'views/scripts'
import SubmitError from 'views/components/SubmitError'
@@ -11,17 +11,17 @@ import Title from 'views/components/Title'
import { userModel } from 'models'
const AuthorizeMfaEnroll = ({
- queryDto, logoUrl, locales, mfaTypes,
+ queryDto, branding, locales, mfaTypes,
}: {
queryDto: identityDto.GetAuthorizeFollowUpReqDto;
- logoUrl: string;
+ branding: Branding;
locales: typeConfig.Locale[];
mfaTypes: userModel.MfaType[];
}) => {
return (
diff --git a/server/src/views/AuthorizeOtpMfa.tsx b/server/src/views/AuthorizeOtpMfa.tsx
index c545caa..9190922 100644
--- a/server/src/views/AuthorizeOtpMfa.tsx
+++ b/server/src/views/AuthorizeOtpMfa.tsx
@@ -3,7 +3,7 @@ import {
localeConfig, routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import { identityDto } from 'dtos'
import {
responseScript,
@@ -15,10 +15,10 @@ import SubmitButton from 'views/components/SubmitButton'
import CodeInput from 'views/components/CodeInput'
const AuthorizeOtpMfa = ({
- queryDto, logoUrl, locales, otp, showEmailMfaBtn,
+ queryDto, branding, locales, otp, showEmailMfaBtn,
}: {
queryDto: identityDto.GetAuthorizeFollowUpReqDto;
- logoUrl: string;
+ branding: Branding;
locales: typeConfig.Locale[];
otp?: string;
showEmailMfaBtn: boolean;
@@ -26,7 +26,7 @@ const AuthorizeOtpMfa = ({
return (
{otp && (
diff --git a/server/src/views/AuthorizePassword.tsx b/server/src/views/AuthorizePassword.tsx
index d4884c6..5beeb01 100644
--- a/server/src/views/AuthorizePassword.tsx
+++ b/server/src/views/AuthorizePassword.tsx
@@ -3,7 +3,7 @@ import {
localeConfig, routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import { oauthDto } from 'dtos'
import {
requestScript, resetErrorScript, responseScript, validateScript,
@@ -23,13 +23,13 @@ const getFBLocale = (locale: typeConfig.Locale) => {
}
const AuthorizePassword = ({
- queryDto, logoUrl, enableSignUp,
+ queryDto, branding, enableSignUp,
enablePasswordReset, enablePasswordSignIn,
queryString, locales,
googleClientId, facebookClientId, githubClientId,
}: {
queryDto: oauthDto.GetAuthorizeReqDto;
- logoUrl: string;
+ branding: Branding;
enableSignUp: boolean;
enablePasswordReset: boolean;
enablePasswordSignIn: boolean;
@@ -42,7 +42,7 @@ const AuthorizePassword = ({
return (
{googleClientId && (
diff --git a/server/src/views/AuthorizeReset.tsx b/server/src/views/AuthorizeReset.tsx
index cdd97dd..bb183ef 100644
--- a/server/src/views/AuthorizeReset.tsx
+++ b/server/src/views/AuthorizeReset.tsx
@@ -5,7 +5,7 @@ import {
routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import {
resetErrorScript, responseScript, validateScript,
} from 'views/scripts'
@@ -16,16 +16,16 @@ import SubmitButton from 'views/components/SubmitButton'
import CodeInput from 'views/components/CodeInput'
const AuthorizeReset = ({
- logoUrl, queryString, queryDto, locales,
+ branding, queryString, queryDto, locales,
}: {
- logoUrl: string;
+ branding: Branding;
queryString: string;
queryDto: oauthDto.GetAuthorizeReqDto;
locales: typeConfig.Locale[];
}) => {
return (
diff --git a/server/src/views/AuthorizeSmsMfa.tsx b/server/src/views/AuthorizeSmsMfa.tsx
index 0678fe8..ce50334 100644
--- a/server/src/views/AuthorizeSmsMfa.tsx
+++ b/server/src/views/AuthorizeSmsMfa.tsx
@@ -3,7 +3,7 @@ import {
localeConfig, routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import { identityDto } from 'dtos'
import {
responseScript,
@@ -16,10 +16,10 @@ import SubmitButton from 'views/components/SubmitButton'
import CodeInput from 'views/components/CodeInput'
const AuthorizeSmsMfa = ({
- queryDto, logoUrl, locales, phoneNumber, showEmailMfaBtn,
+ queryDto, branding, locales, phoneNumber, showEmailMfaBtn,
}: {
queryDto: identityDto.GetAuthorizeFollowUpReqDto;
- logoUrl: string;
+ branding: Branding;
locales: typeConfig.Locale[];
phoneNumber: string | null;
showEmailMfaBtn: boolean;
@@ -27,7 +27,7 @@ const AuthorizeSmsMfa = ({
return (
diff --git a/server/src/views/ChangeEmail.tsx b/server/src/views/ChangeEmail.tsx
index 1bc17b7..7c31895 100644
--- a/server/src/views/ChangeEmail.tsx
+++ b/server/src/views/ChangeEmail.tsx
@@ -5,7 +5,7 @@ import {
routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import {
resetErrorScript, responseScript, validateScript,
} from 'views/scripts'
@@ -16,16 +16,16 @@ import SubmitButton from 'views/components/SubmitButton'
import CodeInput from 'views/components/CodeInput'
const ChangeEmail = ({
- logoUrl, queryDto, locales, redirectUri,
+ branding, queryDto, locales, redirectUri,
}: {
- logoUrl: string;
+ branding: Branding;
queryDto: identityDto.GetAuthorizeFollowUpReqDto;
locales: typeConfig.Locale[];
redirectUri: string;
}) => {
return (
diff --git a/server/src/views/ChangePassword.tsx b/server/src/views/ChangePassword.tsx
index 349d5d3..9c9040b 100644
--- a/server/src/views/ChangePassword.tsx
+++ b/server/src/views/ChangePassword.tsx
@@ -5,7 +5,7 @@ import {
routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import {
resetErrorScript, responseScript, validateScript,
} from 'views/scripts'
@@ -15,16 +15,16 @@ import { identityDto } from 'dtos'
import SubmitButton from 'views/components/SubmitButton'
const ChangePassword = ({
- logoUrl, queryDto, locales, redirectUri,
+ branding, queryDto, locales, redirectUri,
}: {
- logoUrl: string;
+ branding: Branding;
queryDto: identityDto.GetAuthorizeFollowUpReqDto;
locales: typeConfig.Locale[];
redirectUri: string;
}) => {
return (
diff --git a/server/src/views/ResetMfa.tsx b/server/src/views/ResetMfa.tsx
index e229313..0d4deed 100644
--- a/server/src/views/ResetMfa.tsx
+++ b/server/src/views/ResetMfa.tsx
@@ -5,22 +5,22 @@ import {
routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import { responseScript } from 'views/scripts'
import Title from 'views/components/Title'
import { identityDto } from 'dtos'
const ResetMfa = ({
- logoUrl, queryDto, locales, redirectUri,
+ branding, queryDto, locales, redirectUri,
}: {
- logoUrl: string;
+ branding: Branding;
queryDto: identityDto.GetAuthorizeFollowUpReqDto;
locales: typeConfig.Locale[];
redirectUri: string;
}) => {
return (
diff --git a/server/src/views/VerifyEmail.tsx b/server/src/views/VerifyEmail.tsx
index af0ab09..c7a8f95 100644
--- a/server/src/views/VerifyEmail.tsx
+++ b/server/src/views/VerifyEmail.tsx
@@ -5,7 +5,7 @@ import {
routeConfig,
typeConfig,
} from 'configs'
-import Layout from 'views/components/Layout'
+import Layout, { Branding } from 'views/components/Layout'
import { identityDto } from 'dtos'
import {
resetErrorScript, responseScript, validateScript,
@@ -15,15 +15,15 @@ import SubmitError from 'views/components/SubmitError'
import CodeInput from 'views/components/CodeInput'
const VerifyEmail = ({
- queryDto, logoUrl, locales,
+ queryDto, branding, locales,
}: {
queryDto: identityDto.GetVerifyEmailReqDto;
- logoUrl: string;
+ branding: Branding;
locales: typeConfig.Locale[];
}) => {
return (
diff --git a/server/src/views/components/Layout.tsx b/server/src/views/components/Layout.tsx
index bf28847..13d1a1f 100644
--- a/server/src/views/components/Layout.tsx
+++ b/server/src/views/components/Layout.tsx
@@ -6,9 +6,16 @@ import {
localeConfig, typeConfig,
} from 'configs'
+export interface Branding {
+ logoUrl: string;
+ bgColor: string;
+ primaryButtonBgColor: string;
+ primaryButtonFgColor: string;
+}
+
const Layout = ({
- logoUrl, children, locale, locales,
-}: { logoUrl: string; children: any; locale: typeConfig.Locale; locales: typeConfig.Locale[] }) => (
+ branding, children, locale, locales,
+}: { branding: Branding; children: any; locale: typeConfig.Locale; locales: typeConfig.Locale[] }) => (
@@ -16,7 +23,7 @@ const Layout = ({
+ href={branding.logoUrl} />
@@ -69,7 +76,7 @@ const Layout = ({
.w-half { width: 50%; }
.w-text { width: 280px; }
.main {
- background-color: lightgray;
+ background-color: ${branding.bgColor};
height: 100vh;
width: 100%;
}
@@ -85,7 +92,8 @@ const Layout = ({
width: 280px;
}
.button {
- background-color: white;
+ background-color: ${branding.primaryButtonBgColor};
+ color: ${branding.primaryButtonFgColor};
cursor: pointer;
border: 1px solid lightgray;
padding: 8px;
@@ -175,7 +183,7 @@ const Layout = ({
{locales.length > 1 && }
{locales.length > 1 && (
diff --git a/server/wrangler.toml b/server/wrangler.toml
index ee96c15..60167ad 100644
--- a/server/wrangler.toml
+++ b/server/wrangler.toml
@@ -3,42 +3,60 @@ compatibility_date = "2023-12-01"
keep_vars = true
[vars]
-AUTHORIZATION_CODE_EXPIRES_IN=300
-SPA_ACCESS_TOKEN_EXPIRES_IN=1800
-SPA_REFRESH_TOKEN_EXPIRES_IN=604800
-S2S_ACCESS_TOKEN_EXPIRES_IN=3600
-ID_TOKEN_EXPIRES_IN=1800
-SERVER_SESSION_EXPIRES_IN=1800 # Set to 0 to disable session
+# Branding
COMPANY_LOGO_URL="https://raw.githubusercontent.com/ValueMelody/melody-homepage/main/logo.jpg"
+BG_COLOR="lightgray"
+PRIMARY_BUTTON_BG_COLOR="white"
+PRIMARY_BUTTON_FG_COLOR="black"
EMAIL_SENDER_NAME="Melody Auth"
+TERMS_LINK="" # Display a link to your terms on sign-up page
+PRIVACY_POLICY_LINK="" # Display a link to your privacy policy on sign-up page
+
+# Locale
+SUPPORTED_LOCALES=['en', 'fr']
+ENABLE_LOCALE_SELECTOR=true # If there is only one SUPPORTED_LOCALE, the locale selector will be disabled regardless of this setting.
+
+# Suppression
ENABLE_SIGN_UP=true
ENABLE_PASSWORD_SIGN_IN=true
ENABLE_PASSWORD_RESET=true # Please set up your mailer first https://auth.valuemelody.com/auth-server.html#mailer-setup
-PASSWORD_RESET_EMAIL_THRESHOLD=5 # Maximum number of password reset email requests allowed per day for a single email address based on ip address. 0 means no restriction.
ENABLE_NAMES=true
NAMES_IS_REQUIRED=false
ENABLE_USER_APP_CONSENT=true
ENABLE_EMAIL_VERIFICATION=true # Please set up your mailer first https://auth.valuemelody.com/auth-server.html#mailer-setup
+
+# Auth
+AUTHORIZATION_CODE_EXPIRES_IN=300
+SPA_ACCESS_TOKEN_EXPIRES_IN=1800
+SPA_REFRESH_TOKEN_EXPIRES_IN=604800
+S2S_ACCESS_TOKEN_EXPIRES_IN=3600
+ID_TOKEN_EXPIRES_IN=1800
+SERVER_SESSION_EXPIRES_IN=1800 # Set to 0 to disable session
+
+# MFA
OTP_MFA_IS_REQUIRED=false
EMAIL_MFA_IS_REQUIRED=false # Please set up your mailer first https://auth.valuemelody.com/auth-server.html#mailer-setup
-EMAIL_MFA_EMAIL_THRESHOLD=10 # Maximum number of Email MFA email requests allowed per 30 minutes for a single account based on ip address. 0 means no restriction.
-CHANGE_EMAIL_EMAIL_THRESHOLD=5 # Maximum number of change email verification code requests allowed per 30 minutes for a single account. 0 means no restriction.
SMS_MFA_IS_REQUIRED=false # Please set up your sms provider first https://auth.valuemelody.com/auth-server.html#sms-setup
SMS_MFA_MESSAGE_THRESHOLD=5 # Maximum number of SMS MFA message requests allowed per 30 minutes for a single account based on ip address. 0 means no restriction.
ENFORCE_ONE_MFA_ENROLLMENT=['otp', 'email'] # Enforce one MFA type from the list. Available options are ‘email’, ‘otp’, and ‘sms’. This setting is only effective if OTP_MFA_IS_REQUIRED, SMS_MFA_IS_REQUIRED, and EMAIL_MFA_IS_REQUIRED are all set to false. An empty list means no MFA type will be enforced. You must enable email functionality for the email MFA option to work.
ALLOW_EMAIL_MFA_AS_BACKUP=true
+
+# Brute-force
+UNLOCK_ACCOUNT_VIA_PASSWORD_RESET=true
+PASSWORD_RESET_EMAIL_THRESHOLD=5 # Maximum number of password reset email requests allowed per day for a single email address based on ip address. 0 means no restriction.
ACCOUNT_LOCKOUT_THRESHOLD=5 # Number of failed login attempts before the user account is locked. 0 means no restriction.
+EMAIL_MFA_EMAIL_THRESHOLD=10 # Maximum number of Email MFA email requests allowed per 30 minutes for a single account based on ip address. 0 means no restriction.
+CHANGE_EMAIL_EMAIL_THRESHOLD=5 # Maximum number of change email verification code requests allowed per 30 minutes for a single account. 0 means no restriction.
ACCOUNT_LOCKOUT_EXPIRES_IN=86400 # Set to 0 for indefinite lockout until manual intervention.
-UNLOCK_ACCOUNT_VIA_PASSWORD_RESET=true
-SUPPORTED_LOCALES=['en', 'fr']
-ENABLE_LOCALE_SELECTOR=true # If there is only one SUPPORTED_LOCALE, the locale selector will be disabled regardless of this setting.
+
+# Social Sign-in
GOOGLE_AUTH_CLIENT_ID="" # Google Sign-in will be suppressed if it is empty
FACEBOOK_AUTH_CLIENT_ID="" # Facebook Sign-in In will be suppressed if it is empty. When enable, you also need to set FACEBOOK_AUTH_CLIENT_SECRET in .dev.vars for Cloudflare development and Node environments, and in workers config for Cloudflare production environments.
GITHUB_AUTH_CLIENT_ID="" # GitHub Sign-in In will be suppressed if it is empty. When enable, you also need to set GITHUB_AUTH_CLIENT_SECRET in .dev.vars for Cloudflare development and Node environments, and in workers config for Cloudflare production environments.
GITHUB_AUTH_APP_NAME="" # GitHub Sign-in In will be suppressed if it is empty. When enable, you also need to set GITHUB_AUTH_CLIENT_SECRET in .dev.vars for Cloudflare development and Node environments, and in workers config for Cloudflare production environments.
-TERMS_LINK="" # Display a link to your terms on sign-up page
-PRIVACY_POLICY_LINK="" # Display a link to your privacy policy on sign-up page
+
+# Log
ENABLE_EMAIL_LOG=false # Specify whether email should be logged. If enabled, ensure that you implement your own email log cleanup scheduler.
ENABLE_SMS_LOG=false # Specify whether SMS should be logged. If enabled, ensure that you implement your own SMS log cleanup scheduler
ENABLE_SIGN_IN_LOG=false # Specify whether the user’s sign-in IP (only applicable in production environments) and location details (only applicable in Cloudflare environments) should be logged. If enabled, ensure that you implement your own sign-in log cleanup scheduler, clearly disclose the collection of IP and location data in your privacy policy, and comply with all relevant legal requirements.