Add docker publish flow, node blocklist & API updates

Replace docker/metadata GH Action with a docker-publish.sh driven workflow (supports auto-versioning, extra tags, multi-arch builds and optional builder). Update docs and READMEs to use the updater and new publish flow. Add server-side swarm/node blocklist support and admin nodes API. Improve auth endpoints (me, logout, switch, session accounts) and support account switching. Extend bots API to support custom LLM provider and optional endpoint. Enforce node blocklist on chat send/receive, refactor link preview handling into dedicated preview modules, and enhance notifications and posts like/repost handlers to accept both signed actions and regular auth flows. Misc: remove admin update UI details, add helper libs (media previews, node-blocklist, reposts, notifications, etc.), and minor housekeeping (pyc, workflow tweaks).
This commit is contained in:
cyph3rasi
2026-03-10 12:40:05 -07:00
parent 044196cfa7
commit b4a9d82d05
79 changed files with 4714 additions and 1114 deletions
+92 -58
View File
@@ -14,8 +14,14 @@ export interface User {
privateKeyEncrypted?: string;
}
export interface AuthAccount extends User {
isActive: boolean;
}
interface AuthContextType {
user: User | null;
accounts: AuthAccount[];
activeAccountId: string | null;
isAdmin: boolean;
loading: boolean;
isIdentityUnlocked: boolean;
@@ -24,8 +30,10 @@ interface AuthContextType {
handle: string | null;
checkAdmin: () => Promise<void>;
unlockIdentity: (password: string, explicitUser?: User) => Promise<void>;
login: (user: User) => void;
logout: () => Promise<void>;
login: (user?: User) => Promise<void>;
logout: (userId?: string) => Promise<void>;
switchAccount: (userId: string) => Promise<void>;
refreshAuth: () => Promise<void>;
lockIdentity: () => Promise<void>; // New: manual lock
signUserAction: (action: string, data: any) => Promise<any>;
requiresUnlock: boolean; // True if user has encrypted key but not unlocked
@@ -35,6 +43,8 @@ interface AuthContextType {
const AuthContext = createContext<AuthContextType>({
user: null,
accounts: [],
activeAccountId: null,
isAdmin: false,
loading: true,
isIdentityUnlocked: false,
@@ -43,8 +53,10 @@ const AuthContext = createContext<AuthContextType>({
handle: null,
checkAdmin: async () => { },
unlockIdentity: async () => { },
login: () => { },
login: async () => { },
logout: async () => { },
switchAccount: async () => { },
refreshAuth: async () => { },
lockIdentity: async () => { },
signUserAction: async () => Promise.reject('Not initialized'),
requiresUnlock: false,
@@ -54,6 +66,7 @@ const AuthContext = createContext<AuthContextType>({
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [accounts, setAccounts] = useState<AuthAccount[]>([]);
const [isAdmin, setIsAdmin] = useState(false);
const [loading, setLoading] = useState(true);
const [showUnlockPrompt, setShowUnlockPrompt] = useState(false);
@@ -80,6 +93,41 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
}, []);
const applyAuthState = useCallback(async (data: { user: User | null; accounts?: AuthAccount[] | null }) => {
const nextAccounts = data.accounts ?? [];
setAccounts(nextAccounts);
setUser(data.user);
if (data.user?.did && data.user?.publicKey) {
await initializeIdentity({
did: data.user.did,
handle: data.user.handle,
publicKey: data.user.publicKey,
privateKeyEncrypted: data.user.privateKeyEncrypted,
});
await checkAdmin();
} else {
await clearIdentity();
setIsAdmin(false);
}
}, [checkAdmin, clearIdentity, initializeIdentity]);
const refreshAuth = useCallback(async () => {
setLoading(true);
try {
const res = await fetch('/api/auth/me', { cache: 'no-store' });
const data = await res.json();
await applyAuthState({
user: res.ok ? data.user ?? null : null,
accounts: res.ok ? data.accounts ?? [] : [],
});
} catch {
await applyAuthState({ user: null, accounts: [] });
} finally {
setLoading(false);
}
}, [applyAuthState]);
/**
* Unlock the user's identity with their password
* Persists the key for auto-unlock on refresh
@@ -112,81 +160,65 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
/**
* Manually set the user state (called after successful login)
*/
const login = useCallback((userData: User) => {
setUser(userData);
checkAdmin();
// Initialize identity - will try to auto-restore if possible
if (userData.did && userData.publicKey) {
initializeIdentity({
did: userData.did,
handle: userData.handle,
publicKey: userData.publicKey,
privateKeyEncrypted: userData.privateKeyEncrypted,
});
}
}, [checkAdmin, initializeIdentity]);
const login = useCallback(async (_userData?: User) => {
await refreshAuth();
}, [refreshAuth]);
/**
* Logout the user and clear their identity
*/
const logout = useCallback(async () => {
const logout = useCallback(async (userId?: string) => {
try {
await fetch('/api/auth/logout', { method: 'POST' });
await clearIdentity();
await fetch('/api/auth/logout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userId ? { userId } : {}),
});
setShowUnlockPrompt(false);
setUser(null);
setIsAdmin(false);
await refreshAuth();
} catch (error) {
console.error('[Auth] Logout failed:', error);
throw error;
}
}, [clearIdentity]);
}, [refreshAuth]);
const switchAccount = useCallback(async (userId: string) => {
try {
setLoading(true);
const res = await fetch('/api/auth/switch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Failed to switch account');
}
await refreshAuth();
} catch (error) {
console.error('[Auth] Switch account failed:', error);
throw error;
} finally {
setLoading(false);
}
}, [refreshAuth]);
// Load auth state on mount
useEffect(() => {
const loadAuth = async () => {
setLoading(true);
try {
const res = await fetch('/api/auth/me');
if (res.ok) {
const data = await res.json();
setUser(data.user);
// Initialize identity - will auto-restore if persisted
if (data.user?.did && data.user?.publicKey) {
await initializeIdentity({
did: data.user.did,
handle: data.user.handle,
publicKey: data.user.publicKey,
privateKeyEncrypted: data.user.privateKeyEncrypted,
});
}
if (data.user) {
await checkAdmin();
}
} else {
setUser(null);
await clearIdentity();
}
} catch {
setUser(null);
await clearIdentity();
} finally {
setLoading(false);
}
};
loadAuth();
}, [checkAdmin, initializeIdentity, clearIdentity]);
refreshAuth();
}, [refreshAuth]);
// Determine if unlock is required (has encrypted key but not unlocked)
const requiresUnlock = !!user?.privateKeyEncrypted && !isUnlocked && !isRestoring;
const activeAccountId = user?.id ?? null;
return (
<AuthContext.Provider value={{
user,
accounts,
activeAccountId,
isAdmin,
loading,
isIdentityUnlocked: isUnlocked,
@@ -197,6 +229,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
unlockIdentity,
login,
logout,
switchAccount,
refreshAuth,
lockIdentity,
signUserAction,
requiresUnlock,