chore: prepare standalone public repo

- Remove Mattermost integration (OAuth, channel bridge, file sync watcher,
  frontend import modal). RefBoard now ships as a self-contained app.
- Replace SSO Login screen with email/password form (+ optional register link
  gated by ALLOW_SELF_REGISTRATION).
- Add SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD env-var bootstrap so a fresh
  install ships with an admin account on first boot (idempotent).
- ALLOW_SELF_REGISTRATION flag (default false) gates POST /api/auth/register.
  First user can always register (auto-promoted to admin).
- Drop mattermost_id and mm_file_id columns + board_channel_links table
  from the schema; remove related db helpers and exports.
- Add MIT LICENSE, comprehensive README, .env.example, docker-compose.yml
  (bundles MinIO so one command boots a working stack).
- Expand .gitignore for typical Node + Docker dev artefacts.
This commit is contained in:
Hiren Kangad
2026-04-28 19:58:53 +05:30
parent 24fa9d1252
commit 69b58f73f8
15 changed files with 539 additions and 1327 deletions
-16
View File
@@ -24,7 +24,6 @@ import TextFormatToolbar from '../components/TextFormatToolbar';
import { getStickyWidthForSize } from '../canvas/stickyPresets';
import VideoControls from '../components/VideoControls';
import ShortcutsHelp from '../components/ShortcutsHelp';
import MattermostImport from '../components/MattermostImport';
import Minimap from '../components/Minimap';
import UploadPanel from '../components/UploadPanel';
import ExportDialog from '../components/ExportDialog';
@@ -118,7 +117,6 @@ export default function Editor({ isPublicView }: EditorProps) {
const [showLayers, setShowLayers] = useState(false);
const [showGrid, setShowGrid] = useState(true);
const [showHelp, setShowHelp] = useState(false);
const [showMmImport, setShowMmImport] = useState(false);
const [showExport, setShowExport] = useState(false);
const [refreshingPreview, setRefreshingPreview] = useState(false);
const [reviewMode, setReviewMode] = useState(false);
@@ -1093,7 +1091,6 @@ export default function Editor({ isPublicView }: EditorProps) {
onToggleLayers={() => setShowLayers((v) => !v)}
showLayers={showLayers}
onToggleHelp={() => setShowHelp((v) => !v)}
onMmImport={() => setShowMmImport(true)}
onExport={() => setShowExport(true)}
onRefreshPreview={readOnly || isPublicView ? undefined : handleRefreshPreview}
previewRefreshing={refreshingPreview}
@@ -1701,19 +1698,6 @@ export default function Editor({ isPublicView }: EditorProps) {
<ShortcutsHelp shortcuts={shortcutDefs} onClose={() => setShowHelp(false)} />
)}
{/* Mattermost import modal */}
{showMmImport && resolvedBoardId && (
<MattermostImport
boardId={resolvedBoardId}
onClose={() => setShowMmImport(false)}
onMediaArrived={(assets) => {
if (inboxZoneRef.current) {
inboxZoneRef.current.addMedia(assets);
}
}}
/>
)}
{/* Markdown card portals (read-only previews on canvas) */}
{mdCardIds.map(id => {
const mountPoint = mdOverlay?.getMountPoint(id);
+111 -58
View File
@@ -1,48 +1,55 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../auth';
import api from '../api';
const ALLOW_REGISTER = (import.meta.env.VITE_ALLOW_SELF_REGISTRATION || '').toLowerCase() === 'true';
export default function Login() {
const [mode, setMode] = useState<'login' | 'register'>('login');
const [email, setEmail] = useState('');
const [username, setUsername] = useState('');
const [displayName, setDisplayName] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { login, user } = useAuth();
const [searchParams] = useSearchParams();
// Handle OAuth callback — token + user in URL params
useEffect(() => {
const token = searchParams.get('token');
const userParam = searchParams.get('user');
const errorParam = searchParams.get('error');
if (errorParam) {
setError(errorParam);
window.history.replaceState({}, '', '/login');
return;
}
if (token && userParam) {
try {
const userData = JSON.parse(userParam);
login(token, userData);
navigate('/', { replace: true });
} catch {
setError('Failed to complete login. Please try again.');
window.history.replaceState({}, '', '/login');
}
}
}, [searchParams, login, navigate]);
// If already logged in, redirect
useEffect(() => {
if (user) {
navigate('/', { replace: true });
}
React.useEffect(() => {
if (user) navigate('/', { replace: true });
}, [user, navigate]);
function handleMattermostLogin() {
window.location.href = '/api/auth/mattermost';
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
setLoading(true);
try {
const path = mode === 'login' ? '/api/auth/login' : '/api/auth/register';
const body = mode === 'login'
? { email, password }
: { email, username, displayName: displayName || username, password };
const res = await api.post(path, body);
if (res.data?.token && res.data?.user) {
login(res.data.token, res.data.user);
navigate('/', { replace: true });
} else {
setError('Unexpected response from server.');
}
} catch (err: any) {
setError(err?.response?.data?.error || 'Authentication failed.');
} finally {
setLoading(false);
}
}
const inputStyle: React.CSSProperties = {
width: '100%', padding: '12px 14px', marginBottom: '12px',
background: '#0d0d0d', color: '#f0f0f0',
border: '1px solid #2a2a2a', borderRadius: '8px',
fontSize: '14px', boxSizing: 'border-box',
};
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
@@ -50,12 +57,11 @@ export default function Login() {
backgroundImage: 'radial-gradient(ellipse at 50% 0%, rgba(74,158,255,0.08) 0%, transparent 60%)',
}}>
<div style={{
background: '#161616', borderRadius: '16px', padding: '48px 40px',
background: '#161616', borderRadius: '16px', padding: '40px 36px',
width: '100%', maxWidth: '400px',
border: '1px solid #222', boxShadow: '0 24px 64px rgba(0,0,0,0.5)',
}}>
{/* Logo */}
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div style={{ textAlign: 'center', marginBottom: '28px' }}>
<div style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: '48px', height: '48px', borderRadius: '12px',
@@ -72,39 +78,86 @@ export default function Login() {
<h1 style={{ margin: '0 0 4px', fontSize: '24px', fontWeight: 700, color: '#f0f0f0', letterSpacing: '-0.5px' }}>
RefBoard
</h1>
<p style={{ margin: 0, fontSize: '13px', color: '#555', letterSpacing: '0.2px' }}>
Sign in with your team account
<p style={{ margin: 0, fontSize: '13px', color: '#666' }}>
{mode === 'login' ? 'Sign in to continue' : 'Create your account'}
</p>
</div>
{error && (
<div style={{
background: 'rgba(255,107,107,0.08)', border: '1px solid rgba(255,107,107,0.15)',
color: '#ff8a8a', padding: '10px 14px', borderRadius: '8px', marginBottom: '20px',
color: '#ff8a8a', padding: '10px 14px', borderRadius: '8px', marginBottom: '16px',
fontSize: '13px', lineHeight: '1.4',
}}>
{error}
</div>
)}
<button
onClick={handleMattermostLogin}
style={{
width: '100%', padding: '14px', marginBottom: '0',
background: '#386fe5', color: '#fff', border: 'none', borderRadius: '8px',
fontSize: '15px', fontWeight: 600, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '10px',
transition: 'background 0.2s',
boxShadow: '0 2px 12px rgba(56,111,229,0.3)',
}}
onMouseEnter={(e) => (e.currentTarget.style.background = '#2f5fc4')}
onMouseLeave={(e) => (e.currentTarget.style.background = '#386fe5')}
>
<svg width="18" height="18" viewBox="0 0 500 500" fill="currentColor">
<path d="M250 0C111.93 0 0 111.93 0 250s111.93 250 250 250 250-111.93 250-250S388.07 0 250 0zm127.55 354.07c-2.93 5.77-9.18 8.85-15.57 8.85-2.68 0-5.4-.59-7.97-1.85l-72.76-35.72c-19.7 14.88-43.19 22.66-67.58 22.66-6.2 0-12.5-.5-18.73-1.53-24.83-4.07-47.17-16.41-63.38-34.95-16.63-19.03-25.78-43.42-25.78-68.68 0-12.07 2.1-23.86 6.15-35.12.58-1.62 1.54-3.07 2.79-4.22L250.07 91.53c3.08-2.62 7.51-2.62 10.59 0l135.28 112c1.56 1.29 2.65 3.04 3.13 5-.04 11.84-2.08 23.56-6.02 34.7l-72.76-35.72c-7.72-3.79-17.03-.59-20.82 7.13-3.79 7.72-.59 17.03 7.13 20.82l72.76 35.72c-9.28 21.84-26.16 39.95-47.98 50.72l.01-.01 72.76 35.72c7.72 3.79 10.92 13.1 7.13 20.82l.27-.36z"/>
</svg>
Sign in with Mattermost
</button>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
autoComplete="email"
required
style={inputStyle}
/>
{mode === 'register' && (
<>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete="username"
required
style={inputStyle}
/>
<input
type="text"
placeholder="Display name (optional)"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
style={inputStyle}
/>
</>
)}
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
required
style={inputStyle}
/>
<button
type="submit"
disabled={loading}
style={{
width: '100%', padding: '14px', marginTop: '4px',
background: loading ? '#2a4f9a' : '#386fe5', color: '#fff',
border: 'none', borderRadius: '8px',
fontSize: '15px', fontWeight: 600,
cursor: loading ? 'wait' : 'pointer',
transition: 'background 0.2s',
boxShadow: '0 2px 12px rgba(56,111,229,0.3)',
}}
>
{loading ? 'Please wait…' : (mode === 'login' ? 'Sign in' : 'Create account')}
</button>
</form>
{ALLOW_REGISTER && (
<div style={{ textAlign: 'center', marginTop: '20px', fontSize: '13px', color: '#666' }}>
{mode === 'login' ? (
<>Need an account? <a href="#" onClick={(e) => { e.preventDefault(); setMode('register'); setError(''); }} style={{ color: '#4a9eff' }}>Register</a></>
) : (
<>Already have an account? <a href="#" onClick={(e) => { e.preventDefault(); setMode('login'); setError(''); }} style={{ color: '#4a9eff' }}>Sign in</a></>
)}
</div>
)}
</div>
</div>
);