feat: UmbrelOS-style CE with iStore, Casdoor auth, and App Manager

- iStore service (port 3021): fetches app manifests from git.falahos.my/falahos
  org via Gitea API, 5-min cache, query/search support
- UmmahID login: replaced stub form with Casdoor OAuth2 PKCE flow (RFC 7636),
  no client secret required; /api/auth/config and /api/auth/token endpoints
  added to server.cjs
- App Manager service (port 3022): installs/uninstalls Docker apps via
  /var/run/docker.sock, pulls images, starts containers on falah-net,
  persists state to /data/installed.json
- docker-compose.yml: orchestrates falah-app + istore + app-manager with
  health checks, named volumes, and shared falah-net network
- install.sh: one-line curl installer — checks prereqs, clones repo,
  prompts for credentials, builds and starts all containers
- .env.example: all env vars documented (CASDOOR_CLIENT_ID, GITEA_TOKEN,
  APP_MANAGER_PORT, etc.)
- Frontend: IStore overlay now fetches live apps from Gitea, real
  install/uninstall via App Manager with 2s polling, indeterminate
  progress bar, Uninstall button for installed apps
- app.yml.example: developer manifest spec for publishing to iStore
This commit is contained in:
Antigravity AI
2026-07-06 10:05:37 +08:00
parent 8f1295f59d
commit 21b48d3d53
19 changed files with 1206 additions and 252 deletions
+69 -99
View File
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { useApp } from '../store'
import { useState, useCallback } from 'react'
import { fetchAuthConfig, initiateLogin } from '../hooks/useAuth'
function Logo({ size }: { size: number }) {
return (
@@ -25,37 +25,21 @@ function Logo({ size }: { size: number }) {
}
export default function Login() {
const { navigate } = useApp()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [emailFocused, setEmailFocused] = useState(false)
const [passwordFocused, setPasswordFocused] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const inputStyle = (focused: boolean): React.CSSProperties => ({
width: '100%',
padding: '12px 16px',
background: focused ? 'rgba(16,185,129,0.08)' : 'rgba(255,255,255,0.06)',
border: `1px solid ${focused ? 'var(--brand)' : 'var(--glass-border)'}`,
borderRadius: 8,
color: 'var(--text)',
fontSize: 14,
outline: 'none',
transition: 'border-color 0.15s, background 0.15s',
})
const labelStyle: React.CSSProperties = {
display: 'block',
fontSize: 11,
fontWeight: 600,
color: 'var(--text-muted)',
letterSpacing: 1,
marginBottom: 6,
}
const handleSignIn = (e: React.FormEvent) => {
e.preventDefault()
navigate('desktop')
}
const handleSignIn = useCallback(async () => {
setLoading(true)
setError('')
try {
const config = await fetchAuthConfig()
await initiateLogin(config)
// page will redirect — no further state updates needed
} catch (err) {
setError(err instanceof Error ? err.message : 'Could not reach auth server')
setLoading(false)
}
}, [])
return (
<div
@@ -68,8 +52,7 @@ export default function Login() {
padding: 24,
}}
>
<form
onSubmit={handleSignIn}
<div
style={{
width: '100%',
maxWidth: 400,
@@ -83,7 +66,6 @@ export default function Login() {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 0,
}}
>
{/* Arabic */}
@@ -100,83 +82,71 @@ export default function Login() {
بِسْمِ اللَّهِ
</div>
{/* Logo */}
<Logo size={72} />
{/* Title */}
<h1
style={{
fontSize: 26,
fontWeight: 700,
color: 'var(--text)',
margin: '16px 0 4px',
}}
>
<h1 style={{ fontSize: 26, fontWeight: 700, color: 'var(--text)', margin: '16px 0 4px' }}>
Falah OS
</h1>
{/* Subtitle */}
<p
style={{
fontSize: 13,
color: 'var(--text-muted)',
margin: '0 0 32px',
}}
>
<p style={{ fontSize: 13, color: 'var(--text-muted)', margin: '0 0 36px', textAlign: 'center' }}>
Sovereign Digital Economy
</p>
{/* Email field */}
<div style={{ width: '100%', marginBottom: 16 }}>
<label style={labelStyle}>EMAIL</label>
<input
type="email"
placeholder="operator@falah.os"
value={email}
onChange={(e) => setEmail(e.target.value)}
onFocus={() => setEmailFocused(true)}
onBlur={() => setEmailFocused(false)}
style={inputStyle(emailFocused)}
autoComplete="email"
/>
</div>
{/* Password field */}
<div style={{ width: '100%', marginBottom: 28 }}>
<label style={labelStyle}>PASSWORD</label>
<input
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
onFocus={() => setPasswordFocused(true)}
onBlur={() => setPasswordFocused(false)}
style={inputStyle(passwordFocused)}
autoComplete="current-password"
/>
</div>
{/* Sign In button */}
<button
type="submit"
className="btn-primary"
style={{ width: '100%', marginBottom: 24 }}
>
Sign In
</button>
{/* Footer */}
<p
{/* UmmahID sign-in block */}
<div
style={{
fontSize: 11,
color: 'var(--text-dim)',
textAlign: 'center',
margin: 0,
width: '100%',
background: 'rgba(16,185,129,0.06)',
border: '1px solid rgba(16,185,129,0.18)',
borderRadius: 16,
padding: '20px 20px 24px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 12,
marginBottom: 24,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 22 }}>🪪</span>
<div>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>UmmahID</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)' }}>Zero-knowledge identity · auth.falahos.my</div>
</div>
</div>
<button
onClick={handleSignIn}
disabled={loading}
className="btn-primary"
style={{ width: '100%', marginTop: 4, opacity: loading ? 0.6 : 1 }}
>
{loading ? 'Redirecting to UmmahID…' : 'Sign in with UmmahID'}
</button>
</div>
{error && (
<div
style={{
width: '100%',
fontSize: 12,
color: '#F87171',
background: 'rgba(239,68,68,0.08)',
border: '1px solid rgba(239,68,68,0.2)',
borderRadius: 8,
padding: '10px 14px',
textAlign: 'center',
marginBottom: 16,
}}
>
{error}
</div>
)}
<p style={{ fontSize: 11, color: 'var(--text-dim)', textAlign: 'center', margin: 0 }}>
Falah OS CE v1.3 · Community Edition
</p>
</form>
</div>
</div>
)
}