feat: public invite landing page with federation redirect
Make /join/:code a public route with a standalone JoinPage that shows a space preview and handles authenticated, unauthenticated, and cross-instance users. Adds GET /api/spaces/invite/:code/preview (no auth) endpoint, ?redirect= param support on login/register, and cleans up dead invite handling from AppLayout and JoinSpace modal.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { RateLimitError } from '../../api/client';
|
||||
|
||||
@@ -11,6 +11,8 @@ export function LoginPage() {
|
||||
const login = useAuthStore((s) => s.login);
|
||||
const isLoading = useAuthStore((s) => s.isLoading);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const redirect = searchParams.get('redirect');
|
||||
|
||||
useEffect(() => {
|
||||
if (retryAfter <= 0) return;
|
||||
@@ -41,7 +43,11 @@ export function LoginPage() {
|
||||
|
||||
try {
|
||||
await login(username.trim(), password);
|
||||
navigate('/channels/@me');
|
||||
if (redirect && redirect.startsWith('/') && !redirect.startsWith('//')) {
|
||||
navigate(redirect);
|
||||
} else {
|
||||
navigate('/channels/@me');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof RateLimitError) {
|
||||
setRetryAfter(err.retryAfter);
|
||||
@@ -118,7 +124,7 @@ export function LoginPage() {
|
||||
|
||||
<p className="mt-3 text-sm text-txt-tertiary">
|
||||
Need an account?{' '}
|
||||
<Link to="/register" className="text-accent-primary hover:underline">
|
||||
<Link to={`/register${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''}`} className="text-accent-primary hover:underline">
|
||||
Register
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
import { ImageCropModal } from '../ui/ImageCropModal';
|
||||
@@ -43,6 +43,8 @@ export function RegisterPage() {
|
||||
const register = useAuthStore((s) => s.register);
|
||||
const updateProfile = useAuthStore((s) => s.updateProfile);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const redirect = searchParams.get('redirect');
|
||||
|
||||
// Cleanup blob URL on unmount
|
||||
useEffect(() => {
|
||||
@@ -202,7 +204,11 @@ export function RegisterPage() {
|
||||
}
|
||||
}
|
||||
|
||||
navigate('/channels/@me');
|
||||
if (redirect && redirect.startsWith('/') && !redirect.startsWith('//')) {
|
||||
navigate(redirect);
|
||||
} else {
|
||||
navigate('/channels/@me');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof RateLimitError) {
|
||||
setRetryAfter(err.retryAfter);
|
||||
@@ -318,7 +324,7 @@ export function RegisterPage() {
|
||||
|
||||
<p className="mt-3 text-sm text-txt-tertiary">
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" className="text-accent-primary hover:underline">
|
||||
<Link to={`/login${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''}`} className="text-accent-primary hover:underline">
|
||||
Log In
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user