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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user