add now features

This commit is contained in:
Matthias Hochmeister
2026-03-01 11:50:27 +01:00
parent 73ab6cea07
commit 681acd8203
25 changed files with 1518 additions and 4 deletions

View File

@@ -0,0 +1,152 @@
import axios from 'axios';
import environment from '../config/environment';
import logger from '../utils/logger';
interface NextcloudLastMessage {
text: string;
author: string;
timestamp: number;
}
interface NextcloudConversation {
token: string;
displayName: string;
unreadMessages: number;
lastActivity: number;
lastMessage: NextcloudLastMessage | null;
type: number;
url: string;
}
interface ConversationsResult {
totalUnread: number;
conversations: NextcloudConversation[];
}
interface LoginFlowResult {
loginUrl: string;
pollToken: string;
pollEndpoint: string;
}
interface LoginFlowCredentials {
loginName: string;
appPassword: string;
}
async function initiateLoginFlow(): Promise<LoginFlowResult> {
const baseUrl = environment.nextcloudUrl;
if (!baseUrl) {
throw new Error('NEXTCLOUD_URL is not configured');
}
try {
const response = await axios.post(`${baseUrl}/index.php/login/v2`);
return {
loginUrl: response.data.login,
pollToken: response.data.poll.token,
pollEndpoint: response.data.poll.endpoint,
};
} catch (error) {
if (axios.isAxiosError(error)) {
logger.error('Nextcloud Login Flow v2 initiation failed', {
status: error.response?.status,
statusText: error.response?.statusText,
});
}
logger.error('NextcloudService.initiateLoginFlow failed', { error });
throw new Error('Failed to initiate Nextcloud login flow');
}
}
async function pollLoginFlow(pollEndpoint: string, pollToken: string): Promise<LoginFlowCredentials | null> {
try {
const response = await axios.post(pollEndpoint, `token=${pollToken}`, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
return {
loginName: response.data.loginName,
appPassword: response.data.appPassword,
};
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
if (axios.isAxiosError(error)) {
logger.error('Nextcloud Login Flow v2 poll failed', {
status: error.response?.status,
statusText: error.response?.statusText,
});
}
logger.error('NextcloudService.pollLoginFlow failed', { error });
throw new Error('Failed to poll Nextcloud login flow');
}
}
async function getConversations(loginName: string, appPassword: string): Promise<ConversationsResult> {
const baseUrl = environment.nextcloudUrl;
if (!baseUrl) {
throw new Error('NEXTCLOUD_URL is not configured');
}
try {
const response = await axios.get(
`${baseUrl}/ocs/v2.php/apps/spreed/api/v4/room?format=json`,
{
headers: {
'Authorization': `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`,
'OCS-APIRequest': 'true',
'Accept': 'application/json',
},
},
);
const rooms: any[] = response.data?.ocs?.data ?? [];
const filtered = rooms.filter((r: any) => r.type !== 4);
const totalUnread = filtered.reduce(
(sum: number, r: any) => sum + (r.unreadMessages ?? 0),
0,
);
const sorted = [...filtered].sort(
(a: any, b: any) => (b.lastActivity ?? 0) - (a.lastActivity ?? 0),
);
const conversations: NextcloudConversation[] = sorted.slice(0, 3).map((r: any) => ({
token: r.token,
displayName: r.displayName,
unreadMessages: r.unreadMessages ?? 0,
lastActivity: r.lastActivity ?? 0,
lastMessage: r.lastMessage
? {
text: r.lastMessage.message ?? '',
author: r.lastMessage.actorDisplayName ?? '',
timestamp: r.lastMessage.timestamp ?? 0,
}
: null,
type: r.type,
url: `${baseUrl}/call/${r.token}`,
}));
return { totalUnread, conversations };
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
const err = new Error('Nextcloud app password is invalid');
(err as any).code = 'NEXTCLOUD_AUTH_INVALID';
throw err;
}
if (axios.isAxiosError(error)) {
logger.error('Nextcloud API request failed', {
status: error.response?.status,
statusText: error.response?.statusText,
});
throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`);
}
logger.error('NextcloudService.getConversations failed', { error });
throw new Error('Failed to fetch Nextcloud conversations');
}
}
export default { initiateLoginFlow, pollLoginFlow, getConversations };

View File

@@ -273,6 +273,55 @@ class UserService {
}
}
async updateNextcloudCredentials(userId: string, loginName: string, appPassword: string): Promise<void> {
try {
await pool.query(
`UPDATE users SET nextcloud_login_name = $2, nextcloud_app_password = $3 WHERE id = $1`,
[userId, loginName, appPassword],
);
logger.debug('Updated Nextcloud credentials', { userId });
} catch (error) {
logger.error('Error updating Nextcloud credentials', { error, userId });
throw new Error('Failed to update Nextcloud credentials');
}
}
async getNextcloudCredentials(userId: string): Promise<{ loginName: string; appPassword: string } | null> {
try {
const result = await pool.query(
`SELECT nextcloud_login_name, nextcloud_app_password FROM users WHERE id = $1`,
[userId],
);
if (result.rows.length === 0) {
return null;
}
const row = result.rows[0];
if (!row.nextcloud_login_name || !row.nextcloud_app_password) {
return null;
}
return { loginName: row.nextcloud_login_name, appPassword: row.nextcloud_app_password };
} catch (error) {
logger.error('Error getting Nextcloud credentials', { error, userId });
throw new Error('Failed to get Nextcloud credentials');
}
}
async clearNextcloudCredentials(userId: string): Promise<void> {
try {
await pool.query(
`UPDATE users SET nextcloud_login_name = NULL, nextcloud_app_password = NULL WHERE id = $1`,
[userId],
);
logger.debug('Cleared Nextcloud credentials', { userId });
} catch (error) {
logger.error('Error clearing Nextcloud credentials', { error, userId });
throw new Error('Failed to clear Nextcloud credentials');
}
}
/**
* Sync Authentik groups for a user
*/