feat: add support for OAuth-only login mode and update configuration handling
This commit is contained in:
@@ -27,6 +27,10 @@ JMAP_SERVER_URL=https://your-jmap-server.com
|
|||||||
# Set to "true" to use OAuth instead of basic JMAP authentication
|
# Set to "true" to use OAuth instead of basic JMAP authentication
|
||||||
# OAUTH_ENABLED=true
|
# OAUTH_ENABLED=true
|
||||||
|
|
||||||
|
# Set to "true" to only allow OAuth login (hides username/password form)
|
||||||
|
# Requires OAUTH_ENABLED=true
|
||||||
|
# OAUTH_ONLY=true
|
||||||
|
|
||||||
# OAuth client ID registered with your identity provider
|
# OAuth client ID registered with your identity provider
|
||||||
# OAUTH_CLIENT_ID=your-client-id
|
# OAUTH_CLIENT_ID=your-client-id
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function LoginPage() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { login, isLoading, error, clearError, isAuthenticated } = useAuthStore();
|
const { login, isLoading, error, clearError, isAuthenticated } = useAuthStore();
|
||||||
const { theme, setTheme, initializeTheme } = useThemeStore();
|
const { theme, setTheme, initializeTheme } = useThemeStore();
|
||||||
const { appName, jmapServerUrl: serverUrl, oauthEnabled, oauthClientId, oauthIssuerUrl, rememberMeEnabled, devMode, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError } = useConfig();
|
const { appName, jmapServerUrl: serverUrl, oauthEnabled, oauthOnly, oauthClientId, oauthIssuerUrl, rememberMeEnabled, devMode, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError } = useConfig();
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
username: "",
|
username: "",
|
||||||
@@ -491,6 +491,41 @@ export default function LoginPage() {
|
|||||||
Dev mode — logging in as dev@localhost
|
Dev mode — logging in as dev@localhost
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
) : oauthOnly ? (
|
||||||
|
/* OAuth-only mode: show SSO button only */
|
||||||
|
<div className="space-y-4">
|
||||||
|
{oauthMetadata ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="w-full h-11 font-medium text-[15px] bg-primary hover:bg-primary/90 transition-all duration-200 rounded-xl shadow-md shadow-primary/15 hover:shadow-lg hover:shadow-primary/20"
|
||||||
|
onClick={handleOAuthLogin}
|
||||||
|
disabled={oauthLoading}
|
||||||
|
>
|
||||||
|
{oauthLoading ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
{t("signing_in")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<LogIn className="w-4 h-4" />
|
||||||
|
{t("sign_in_sso")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : oauthDiscoveryDone ? (
|
||||||
|
<div className="p-3.5 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4 text-amber-700 dark:text-amber-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<p className="text-sm text-amber-700 dark:text-amber-400">
|
||||||
|
{t("error.oauth_discovery_failed")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Login Form */
|
/* Login Form */
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ const COOKIE_OPTIONS = {
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
if (process.env.OAUTH_ENABLED === 'true' && process.env.OAUTH_ONLY === 'true') {
|
||||||
|
return NextResponse.json({ error: 'Basic authentication is disabled' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
const { serverUrl, username, password } = await request.json();
|
const { serverUrl, username, password } = await request.json();
|
||||||
if (!serverUrl || !username || !password) {
|
if (!serverUrl || !username || !password) {
|
||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export async function GET() {
|
|||||||
appName: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || 'Webmail',
|
appName: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || 'Webmail',
|
||||||
jmapServerUrl: process.env.JMAP_SERVER_URL || process.env.NEXT_PUBLIC_JMAP_SERVER_URL || '',
|
jmapServerUrl: process.env.JMAP_SERVER_URL || process.env.NEXT_PUBLIC_JMAP_SERVER_URL || '',
|
||||||
oauthEnabled: process.env.OAUTH_ENABLED === 'true',
|
oauthEnabled: process.env.OAUTH_ENABLED === 'true',
|
||||||
|
oauthOnly: process.env.OAUTH_ENABLED === 'true' && process.env.OAUTH_ONLY === 'true',
|
||||||
oauthClientId: process.env.OAUTH_CLIENT_ID || '',
|
oauthClientId: process.env.OAUTH_CLIENT_ID || '',
|
||||||
oauthIssuerUrl: process.env.OAUTH_ISSUER_URL || '',
|
oauthIssuerUrl: process.env.OAUTH_ISSUER_URL || '',
|
||||||
rememberMeEnabled: !!process.env.SESSION_SECRET,
|
rememberMeEnabled: !!process.env.SESSION_SECRET,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface ConfigData {
|
|||||||
appName: string;
|
appName: string;
|
||||||
jmapServerUrl: string;
|
jmapServerUrl: string;
|
||||||
oauthEnabled: boolean;
|
oauthEnabled: boolean;
|
||||||
|
oauthOnly: boolean;
|
||||||
oauthClientId: string;
|
oauthClientId: string;
|
||||||
oauthIssuerUrl: string;
|
oauthIssuerUrl: string;
|
||||||
rememberMeEnabled: boolean;
|
rememberMeEnabled: boolean;
|
||||||
@@ -69,6 +70,7 @@ export function useConfig(): AppConfig {
|
|||||||
appName: configCache?.appName || 'Webmail',
|
appName: configCache?.appName || 'Webmail',
|
||||||
jmapServerUrl: configCache?.jmapServerUrl || '',
|
jmapServerUrl: configCache?.jmapServerUrl || '',
|
||||||
oauthEnabled: configCache?.oauthEnabled || false,
|
oauthEnabled: configCache?.oauthEnabled || false,
|
||||||
|
oauthOnly: configCache?.oauthOnly || false,
|
||||||
oauthClientId: configCache?.oauthClientId || '',
|
oauthClientId: configCache?.oauthClientId || '',
|
||||||
oauthIssuerUrl: configCache?.oauthIssuerUrl || '',
|
oauthIssuerUrl: configCache?.oauthIssuerUrl || '',
|
||||||
rememberMeEnabled: configCache?.rememberMeEnabled || false,
|
rememberMeEnabled: configCache?.rememberMeEnabled || false,
|
||||||
@@ -90,6 +92,7 @@ export function useConfig(): AppConfig {
|
|||||||
appName: configCache.appName,
|
appName: configCache.appName,
|
||||||
jmapServerUrl: configCache.jmapServerUrl,
|
jmapServerUrl: configCache.jmapServerUrl,
|
||||||
oauthEnabled: configCache.oauthEnabled,
|
oauthEnabled: configCache.oauthEnabled,
|
||||||
|
oauthOnly: configCache.oauthOnly,
|
||||||
oauthClientId: configCache.oauthClientId,
|
oauthClientId: configCache.oauthClientId,
|
||||||
oauthIssuerUrl: configCache.oauthIssuerUrl,
|
oauthIssuerUrl: configCache.oauthIssuerUrl,
|
||||||
rememberMeEnabled: configCache.rememberMeEnabled,
|
rememberMeEnabled: configCache.rememberMeEnabled,
|
||||||
@@ -112,6 +115,7 @@ export function useConfig(): AppConfig {
|
|||||||
appName: data.appName,
|
appName: data.appName,
|
||||||
jmapServerUrl: data.jmapServerUrl,
|
jmapServerUrl: data.jmapServerUrl,
|
||||||
oauthEnabled: data.oauthEnabled,
|
oauthEnabled: data.oauthEnabled,
|
||||||
|
oauthOnly: data.oauthOnly,
|
||||||
oauthClientId: data.oauthClientId,
|
oauthClientId: data.oauthClientId,
|
||||||
oauthIssuerUrl: data.oauthIssuerUrl,
|
oauthIssuerUrl: data.oauthIssuerUrl,
|
||||||
rememberMeEnabled: data.rememberMeEnabled,
|
rememberMeEnabled: data.rememberMeEnabled,
|
||||||
|
|||||||
Reference in New Issue
Block a user