From ae6de407d77b6902c37e9b5cf47013124339e164 Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 20 Apr 2025 12:43:17 +0200 Subject: [PATCH] carnet api --- app/api/auth/[...nextauth]/route.ts | 140 +++++++++------------------- app/carnet/page.tsx | 1 + components/carnet/editor.tsx | 8 +- lib/nextcloud.ts | 36 +++++-- types/next-auth.d.ts | 38 ++++---- 5 files changed, 94 insertions(+), 129 deletions(-) diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 641eb7ad..a001676d 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -35,6 +35,7 @@ declare module "next-auth" { role: string[]; }; accessToken: string; + nextcloudToken: string; } interface JWT { @@ -92,6 +93,29 @@ async function refreshAccessToken(token: JWT) { } } +// Add NextCloud token exchange logic +async function exchangeKeycloakTokenForNextCloud(token: string): Promise { + const response = await fetch(`${process.env.NEXTCLOUD_URL}/apps/oauth2/api/v1/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + subject_token: token, + subject_token_type: 'urn:ietf:params:oauth:token-type:access_token', + requested_token_type: 'urn:ietf:params:oauth:token-type:access_token' + }) + }); + + if (!response.ok) { + throw new Error('Failed to exchange token with NextCloud'); + } + + const data = await response.json(); + return data.access_token; +} + export const authOptions: NextAuthOptions = { providers: [ KeycloakProvider({ @@ -139,107 +163,31 @@ export const authOptions: NextAuthOptions = { maxAge: 30 * 24 * 60 * 60, // 30 days }, callbacks: { - async jwt({ token, account, profile }) { - console.log('JWT callback start:', { - hasAccount: !!account, - hasProfile: !!profile, - token - }); - - if (account && profile) { - const keycloakProfile = profile as KeycloakProfile; - console.log('JWT callback profile:', { - rawRoles: keycloakProfile.roles, - realmAccess: keycloakProfile.realm_access, - profile: keycloakProfile - }); - - // Get roles from realm_access - const roles = keycloakProfile.realm_access?.roles || []; - console.log('JWT callback raw roles:', roles); - - // Clean up roles by removing ROLE_ prefix and converting to lowercase - const cleanRoles = roles.map((role: string) => - role.replace(/^ROLE_/, '').toLowerCase() - ); - - console.log('JWT callback cleaned roles:', cleanRoles); - - token.accessToken = account.access_token ?? ''; - token.refreshToken = account.refresh_token ?? ''; - token.accessTokenExpires = account.expires_at ?? 0; - token.sub = keycloakProfile.sub; - token.role = cleanRoles; - token.username = keycloakProfile.preferred_username ?? ''; - token.first_name = keycloakProfile.given_name ?? ''; - token.last_name = keycloakProfile.family_name ?? ''; - - console.log('JWT callback final token:', { - tokenRoles: token.role, - token - }); - } else if (token.accessToken) { - // Decode the token to get roles - try { - const decoded = jwtDecode(token.accessToken); - console.log('Decoded token:', decoded); - - if (decoded.realm_access?.roles) { - const roles = decoded.realm_access.roles; - console.log('Decoded token roles:', roles); - - // Clean up roles by removing ROLE_ prefix and converting to lowercase - const cleanRoles = roles.map((role: string) => - role.replace(/^ROLE_/, '').toLowerCase() - ); - - console.log('Decoded token cleaned roles:', cleanRoles); - token.role = cleanRoles; - } - } catch (error) { - console.error('Error decoding token:', error); + async jwt({ token, account }) { + if (account?.access_token) { + token.accessToken = account.access_token; + // Only set refresh token if it exists + if (account.refresh_token) { + token.refreshToken = account.refresh_token; } + // Set expiry if it exists, otherwise set a default + token.accessTokenExpires = account.expires_at ? account.expires_at * 1000 : Date.now() + 3600 * 1000; } - - if (Date.now() < (token.accessTokenExpires as number) * 1000) { - return token; - } - - return refreshAccessToken(token); + return token; }, async session({ session, token }) { - if (token.error) { - throw new Error(token.error); + if (token) { + session.accessToken = token.accessToken; + // We'll handle Nextcloud authentication separately using app passwords + session.user = { + ...session.user, + id: token.sub || '', + role: token.role || [], + username: token.username || '', + first_name: token.first_name || '', + last_name: token.last_name || '', + }; } - - console.log('Session callback token:', { - tokenRoles: token.role, - tokenSub: token.sub, - tokenUsername: token.username, - token - }); - - // Ensure we have an array of roles - const userRoles = Array.isArray(token.role) ? token.role : []; - console.log('Session callback userRoles:', userRoles); - - session.user = { - id: token.sub ?? '', - email: token.email ?? null, - name: token.name ?? null, - image: null, - username: token.username ?? '', - first_name: token.first_name ?? '', - last_name: token.last_name ?? '', - role: userRoles, - }; - session.accessToken = token.accessToken; - - console.log('Session callback final session:', { - userRoles: session.user.role, - session - }); - return session; } }, diff --git a/app/carnet/page.tsx b/app/carnet/page.tsx index 5651984a..2d584ec8 100644 --- a/app/carnet/page.tsx +++ b/app/carnet/page.tsx @@ -100,6 +100,7 @@ export default function CarnetPage() { 'Content-Type': 'application/json', }, body: JSON.stringify({ + title: note.title, content: note.content, date: note.lastEdited.toISOString(), category: 'Notes' diff --git a/components/carnet/editor.tsx b/components/carnet/editor.tsx index 05178b22..83e80631 100644 --- a/components/carnet/editor.tsx +++ b/components/carnet/editor.tsx @@ -9,7 +9,7 @@ interface EditorProps { title: string; content: string; lastEdited: Date; - }; + } | null; onSave?: (note: { id: string; title: string; content: string; lastEdited: Date }) => void; } @@ -33,9 +33,9 @@ export const Editor: React.FC = ({ note, onSave }) => { }; const handleSave = () => { - if (note?.id) { - onSave?.({ - id: note.id, + if (onSave) { + onSave({ + id: note?.id || Date.now().toString(), title, content, lastEdited: new Date() diff --git a/lib/nextcloud.ts b/lib/nextcloud.ts index fd587992..9cdb228f 100644 --- a/lib/nextcloud.ts +++ b/lib/nextcloud.ts @@ -5,32 +5,52 @@ export class NextCloudService { private webdav: WebDAV; private basePath: string = '/Personal/Carnet'; private subFolders: string[] = ['Journal', 'Santé', 'Notes']; + private token: string; + private appPassword?: string; + private baseUrl: string; - constructor(token: string) { - const nextcloudUrl = process.env.NEXTCLOUD_URL; + constructor(token: string, appPassword?: string) { + this.token = token; + this.appPassword = appPassword; + this.baseUrl = process.env.NEXTCLOUD_URL || ''; console.log('NextCloud Configuration:', { - baseUrl: nextcloudUrl, + baseUrl: this.baseUrl, tokenLength: token?.length || 0, hasToken: !!token }); - if (!nextcloudUrl) { + if (!this.baseUrl) { throw new Error('NEXTCLOUD_URL environment variable is not set'); } - const webdavUrl = `${nextcloudUrl}/remote.php/dav/files`; + const webdavUrl = `${this.baseUrl}/remote.php/dav/files`; console.log('WebDAV endpoint:', webdavUrl); this.webdav = new WebDAV( webdavUrl, { - headers: { - Authorization: `Bearer ${token}`, - }, + headers: this.getHeaders(), } ); } + private isUsingAppPassword(): boolean { + return !!this.appPassword; + } + + private getHeaders(): HeadersInit { + if (this.isUsingAppPassword()) { + return { + 'Authorization': `Basic ${Buffer.from(`${this.token}:${this.appPassword}`).toString('base64')}`, + 'Content-Type': 'application/json', + }; + } + return { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + }; + } + private async testConnection() { try { console.log('Testing WebDAV connection...'); diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts index b660b33d..e71b4603 100644 --- a/types/next-auth.d.ts +++ b/types/next-auth.d.ts @@ -7,26 +7,23 @@ declare module "next-auth" { first_name: string; last_name: string; username: string; - email: string; + email?: string | null; role: string[]; } & DefaultSession["user"]; - accessToken?: string; + accessToken: string; refreshToken?: string; - rocketChatToken?: string | null; - rocketChatUserId?: string | null; error?: string; } interface JWT { - accessToken?: string; + sub?: string; + accessToken: string; refreshToken?: string; - accessTokenExpires?: number; - first_name?: string; - last_name?: string; - username?: string; - role?: string[]; - rocketChatToken?: string | null; - rocketChatUserId?: string | null; + accessTokenExpires: number; + first_name: string; + last_name: string; + username: string; + role: string[]; error?: string; } @@ -35,7 +32,7 @@ declare module "next-auth" { first_name: string; last_name: string; username: string; - email: string; + email?: string | null; role: string[]; } @@ -53,15 +50,14 @@ declare module "next-auth" { declare module "next-auth/jwt" { interface JWT { - accessToken?: string; + sub?: string; + accessToken: string; refreshToken?: string; - accessTokenExpires?: number; - first_name?: string; - last_name?: string; - username?: string; - role?: string[]; - rocketChatToken?: string; - rocketChatUserId?: string; + accessTokenExpires: number; + first_name: string; + last_name: string; + username: string; + role: string[]; error?: string; } }