From cc6e8a69d3a36bb145f63988abda7080a1b30701 Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 20 Apr 2025 12:47:21 +0200 Subject: [PATCH] carnet api rest --- app/api/auth/[...nextauth]/route.ts | 140 +++++++++---- app/api/carnet/[date]/route.ts | 48 ----- app/api/carnet/route.ts | 59 ------ app/api/carnet/test/route.ts | 58 ------ app/carnet/page.tsx | 27 +-- components/carnet/editor.tsx | 14 +- lib/nextcloud-utils.ts | 37 ---- lib/nextcloud.ts | 305 ---------------------------- types/next-auth.d.ts | 38 ++-- 9 files changed, 126 insertions(+), 600 deletions(-) delete mode 100644 app/api/carnet/[date]/route.ts delete mode 100644 app/api/carnet/route.ts delete mode 100644 app/api/carnet/test/route.ts delete mode 100644 lib/nextcloud-utils.ts delete mode 100644 lib/nextcloud.ts diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index a001676d..641eb7ad 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -35,7 +35,6 @@ declare module "next-auth" { role: string[]; }; accessToken: string; - nextcloudToken: string; } interface JWT { @@ -93,29 +92,6 @@ 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({ @@ -163,31 +139,107 @@ export const authOptions: NextAuthOptions = { maxAge: 30 * 24 * 60 * 60, // 30 days }, callbacks: { - 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; + 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); } - // Set expiry if it exists, otherwise set a default - token.accessTokenExpires = account.expires_at ? account.expires_at * 1000 : Date.now() + 3600 * 1000; } - return token; + + if (Date.now() < (token.accessTokenExpires as number) * 1000) { + return token; + } + + return refreshAccessToken(token); }, async session({ session, token }) { - 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 || '', - }; + if (token.error) { + throw new Error(token.error); } + + 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/api/carnet/[date]/route.ts b/app/api/carnet/[date]/route.ts deleted file mode 100644 index 001125bc..00000000 --- a/app/api/carnet/[date]/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { getNextCloudService } from '@/lib/nextcloud-utils'; - -export async function GET( - request: Request, - { params }: { params: { date: string } } -) { - try { - const session = await getServerSession(); - if (!session?.user?.email) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const { searchParams } = new URL(request.url); - const category = searchParams.get('category') || 'Notes'; - - const service = await getNextCloudService(); - const date = new Date(params.date); - - if (isNaN(date.getTime())) { - return NextResponse.json( - { error: 'Invalid date format' }, - { status: 400 } - ); - } - - const content = await service.getNote(session.user.email, category, date); - - if (!content) { - return NextResponse.json( - { error: 'Note not found' }, - { status: 404 } - ); - } - - return NextResponse.json({ content, category }); - } catch (error) { - console.error('Failed to get note:', error); - return NextResponse.json( - { error: 'Failed to get note' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/carnet/route.ts b/app/api/carnet/route.ts deleted file mode 100644 index eb2c1c02..00000000 --- a/app/api/carnet/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { getNextCloudService } from '@/lib/nextcloud-utils'; - -export async function GET(request: Request) { - try { - const session = await getServerSession(); - if (!session?.user?.email) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const { searchParams } = new URL(request.url); - const category = searchParams.get('category'); - - const service = await getNextCloudService(); - const notes = await service.listNotes(session.user.email, category || undefined); - - return NextResponse.json({ notes }); - } catch (error) { - console.error('Failed to list notes:', error); - return NextResponse.json( - { error: 'Failed to list notes' }, - { status: 500 } - ); - } -} - -export async function POST(request: Request) { - try { - const session = await getServerSession(); - if (!session?.user?.email) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const service = await getNextCloudService(); - const { content, date, category = 'Notes' } = await request.json(); - - const result = await service.saveNote( - session.user.email, - content, - category, - date ? new Date(date) : undefined - ); - - return NextResponse.json(result); - } catch (error) { - console.error('Failed to save note:', error); - return NextResponse.json( - { error: 'Failed to save note' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/carnet/test/route.ts b/app/api/carnet/test/route.ts deleted file mode 100644 index c90781c5..00000000 --- a/app/api/carnet/test/route.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { getNextCloudService } from '@/lib/nextcloud-utils'; - -export async function GET() { - console.log('πŸ” Test endpoint called - Starting execution'); - - try { - // Check session - console.log('πŸ‘€ Checking user session...'); - const session = await getServerSession(authOptions); - - if (!session?.user?.email) { - console.error('❌ No valid session found'); - return NextResponse.json( - { error: 'Unauthorized - No valid session' }, - { status: 401 } - ); - } - console.log('βœ… Session validated for user:', session.user.email); - - // Initialize NextCloud service - console.log('πŸ”„ Initializing NextCloud service...'); - const service = await getNextCloudService(); - console.log('βœ… NextCloud service initialized'); - - // Initialize user folders - console.log('πŸ“ Initializing user folders...'); - await service.initializeUserFolders(session.user.email); - console.log('βœ… User folders initialized'); - - // List notes - console.log('πŸ“ Attempting to list notes...'); - const notes = await service.listNotes(session.user.email); - console.log('βœ… Notes retrieved successfully:', notes.length, 'notes found'); - - return NextResponse.json({ - status: 'success', - message: 'Test completed successfully', - data: { - userEmail: session.user.email, - notesCount: notes.length, - notes: notes - } - }); - - } catch (error) { - console.error('❌ Test failed with error:', error); - return NextResponse.json( - { - error: 'Test failed', - details: error instanceof Error ? error.message : 'Unknown error' - }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/carnet/page.tsx b/app/carnet/page.tsx index 2d584ec8..80eff925 100644 --- a/app/carnet/page.tsx +++ b/app/carnet/page.tsx @@ -92,30 +92,9 @@ export default function CarnetPage() { } }; - const handleNoteSave = async (note: Note) => { - try { - const response = await fetch('/api/carnet', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - title: note.title, - content: note.content, - date: note.lastEdited.toISOString(), - category: 'Notes' - }), - }); - - if (!response.ok) { - throw new Error('Failed to save note'); - } - - const result = await response.json(); - console.log('Note saved successfully:', result); - } catch (error) { - console.error('Error saving note:', error); - } + const handleNoteSave = (note: Note) => { + // TODO: Implement note saving logic + console.log('Saving note:', note); }; if (isLoading) { diff --git a/components/carnet/editor.tsx b/components/carnet/editor.tsx index 83e80631..5406d7bc 100644 --- a/components/carnet/editor.tsx +++ b/components/carnet/editor.tsx @@ -8,9 +8,8 @@ interface EditorProps { id: string; title: string; content: string; - lastEdited: Date; - } | null; - onSave?: (note: { id: string; title: string; content: string; lastEdited: Date }) => void; + }; + onSave?: (note: { id: string; title: string; content: string }) => void; } export const Editor: React.FC = ({ note, onSave }) => { @@ -33,12 +32,11 @@ export const Editor: React.FC = ({ note, onSave }) => { }; const handleSave = () => { - if (onSave) { - onSave({ - id: note?.id || Date.now().toString(), + if (note?.id) { + onSave?.({ + id: note.id, title, - content, - lastEdited: new Date() + content }); } }; diff --git a/lib/nextcloud-utils.ts b/lib/nextcloud-utils.ts deleted file mode 100644 index efe9c98e..00000000 --- a/lib/nextcloud-utils.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getServerSession } from 'next-auth'; -import { NextCloudService } from './nextcloud'; - -export async function getNextCloudService() { - const session = await getServerSession(); - if (!session?.user?.email) { - throw new Error('Not authenticated'); - } - - // Get the NextCloud token from the session - const token = session.nextcloudToken; - if (!token) { - console.error('Session details:', { - hasSession: !!session, - hasUser: !!session?.user, - email: session?.user?.email, - sessionKeys: Object.keys(session || {}), - // Don't log sensitive data - tokenPresent: !!token, - hasNextCloudToken: 'nextcloudToken' in session - }); - throw new Error('No NextCloud token available'); - } - - try { - console.log('Initializing NextCloud service with token length:', token.length); - const service = new NextCloudService(token); - return service; - } catch (error) { - console.error('Failed to initialize NextCloud service:', { - error: error instanceof Error ? error.message : error, - hasToken: !!token, - tokenLength: token.length - }); - throw error; - } -} \ No newline at end of file diff --git a/lib/nextcloud.ts b/lib/nextcloud.ts deleted file mode 100644 index 9cdb228f..00000000 --- a/lib/nextcloud.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { WebDAV } from 'webdav'; -import { getSession } from 'next-auth/react'; - -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, appPassword?: string) { - this.token = token; - this.appPassword = appPassword; - this.baseUrl = process.env.NEXTCLOUD_URL || ''; - console.log('NextCloud Configuration:', { - baseUrl: this.baseUrl, - tokenLength: token?.length || 0, - hasToken: !!token - }); - - if (!this.baseUrl) { - throw new Error('NEXTCLOUD_URL environment variable is not set'); - } - - const webdavUrl = `${this.baseUrl}/remote.php/dav/files`; - console.log('WebDAV endpoint:', webdavUrl); - - this.webdav = new WebDAV( - webdavUrl, - { - 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...'); - // Try to list root directory - const rootContents = await this.webdav.getDirectoryContents('/'); - console.log('WebDAV connection successful, root directory contents:', rootContents); - return true; - } catch (error) { - console.error('WebDAV connection test failed:', { - error: error instanceof Error ? error.message : error, - response: error.response?.data, - status: error.response?.status, - headers: error.response?.headers - }); - return false; - } - } - - async initializeUserFolders(username: string) { - console.log('=== Starting folder initialization ==='); - console.log(`User: ${username}`); - - // Test connection first - const connectionOk = await this.testConnection(); - if (!connectionOk) { - throw new Error('Failed to connect to NextCloud WebDAV service'); - } - - const userBasePath = `${username}${this.basePath}`; - console.log('Target base path:', userBasePath); - - // Create base Carnet folder - try { - console.log('Checking base folder existence...'); - const baseExists = await this.webdav.exists(userBasePath); - console.log('Base folder check result:', baseExists); - - if (!baseExists) { - console.log('Creating base folder structure...'); - try { - await this.webdav.createDirectory(userBasePath, { recursive: true }); - console.log('Base folder created successfully'); - } catch (createError) { - console.error('Failed to create base folder:', { - error: createError instanceof Error ? createError.message : createError, - response: createError.response?.data, - status: createError.response?.status - }); - throw createError; - } - } - } catch (error) { - console.error('Error in base folder operation:', { - path: userBasePath, - error: error instanceof Error ? error.message : error, - response: error.response?.data, - status: error.response?.status, - headers: error.response?.headers - }); - throw new Error(`Failed to initialize base folder: ${error.message || 'Unknown error'}`); - } - - // Create subfolders - for (const folder of this.subFolders) { - const folderPath = `${userBasePath}/${folder}`; - console.log(`\nProcessing subfolder: ${folder}`); - console.log('Target path:', folderPath); - - try { - console.log(`Checking if ${folder} exists...`); - const exists = await this.webdav.exists(folderPath); - console.log(`${folder} existence check result:`, exists); - - if (!exists) { - console.log(`Creating ${folder} folder...`); - try { - await this.webdav.createDirectory(folderPath, { recursive: true }); - console.log(`${folder} folder created successfully`); - } catch (createError) { - console.error(`Failed to create ${folder} folder:`, { - error: createError instanceof Error ? createError.message : createError, - response: createError.response?.data, - status: createError.response?.status - }); - throw createError; - } - } - } catch (error) { - console.error(`Error processing ${folder} folder:`, { - path: folderPath, - error: error instanceof Error ? error.message : error, - response: error.response?.data, - status: error.response?.status, - headers: error.response?.headers - }); - throw new Error(`Failed to initialize ${folder} folder: ${error.message || 'Unknown error'}`); - } - } - - console.log('\n=== Folder initialization completed ==='); - - // Verify final structure - try { - console.log('\nVerifying folder structure...'); - const contents = await this.webdav.getDirectoryContents(userBasePath, { deep: true }); - console.log('Final folder structure:', contents); - } catch (error) { - console.error('Error verifying folder structure:', { - error: error instanceof Error ? error.message : error, - response: error.response?.data, - status: error.response?.status - }); - } - } - - async saveNote(username: string, content: string, category: string = 'Notes', date: Date = new Date()) { - console.log('Saving note:', { username, category, date }); - - if (!this.subFolders.includes(category)) { - console.error('Invalid category provided:', category); - throw new Error(`Invalid category: ${category}`); - } - - const fileName = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}.md`; - const filePath = `${username}${this.basePath}/${category}/${fileName}`; - console.log('File path for save:', filePath); - - try { - await this.initializeUserFolders(username); - console.log('Saving file content to:', filePath); - await this.webdav.putFileContents(filePath, content, { overwrite: true }); - console.log('File saved successfully'); - - return { fileName, category }; - } catch (error) { - console.error('Error saving note:', { - path: filePath, - error: error instanceof Error ? error.message : error, - fullError: error - }); - throw error; - } - } - - async getNote(username: string, category: string, date: Date) { - console.log('Getting note:', { username, category, date }); - - if (!this.subFolders.includes(category)) { - console.error('Invalid category provided:', category); - throw new Error(`Invalid category: ${category}`); - } - - const fileName = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}.md`; - const filePath = `${username}${this.basePath}/${category}/${fileName}`; - console.log('File path for get:', filePath); - - try { - console.log('Fetching file content from:', filePath); - const content = await this.webdav.getFileContents(filePath, { format: 'text' }); - console.log('File content fetched successfully'); - return content; - } catch (error) { - if (error.response?.status === 404) { - console.log('Note not found:', filePath); - return null; - } - console.error('Error getting note:', { - path: filePath, - error: error instanceof Error ? error.message : error, - fullError: error - }); - throw error; - } - } - - async listNotes(username: string, category?: string) { - console.log('Listing notes:', { username, category }); - - try { - await this.initializeUserFolders(username); - - const userPath = `${username}${this.basePath}`; - const results = []; - - if (category) { - if (!this.subFolders.includes(category)) { - console.error('Invalid category provided:', category); - throw new Error(`Invalid category: ${category}`); - } - const folderPath = `${userPath}/${category}`; - console.log('Listing files in category folder:', folderPath); - const files = await this.webdav.getDirectoryContents(folderPath); - console.log(`Found ${files.length} files in ${category}`); - results.push(...this.processFiles(files, category)); - } else { - for (const folder of this.subFolders) { - const folderPath = `${userPath}/${folder}`; - try { - console.log('Listing files in folder:', folderPath); - const files = await this.webdav.getDirectoryContents(folderPath); - console.log(`Found ${files.length} files in ${folder}`); - results.push(...this.processFiles(files, folder)); - } catch (error) { - if (error.response?.status !== 404) { - console.error(`Error listing files in ${folder}:`, { - path: folderPath, - error: error instanceof Error ? error.message : error, - fullError: error - }); - throw error; - } - console.log(`No files found in ${folder}`); - } - } - } - - console.log(`Total notes found: ${results.length}`); - return results; - } catch (error) { - if (error.response?.status === 404) { - console.log('No notes found'); - return []; - } - console.error('Error listing notes:', { - username, - category, - error: error instanceof Error ? error.message : error, - fullError: error - }); - throw error; - } - } - - private processFiles(files: any[], category: string) { - const processed = files - .filter(file => file.basename.endsWith('.md')) - .map(file => ({ - date: this.fileNameToDate(file.basename), - name: file.basename, - path: file.filename, - category - })); - console.log(`Processed ${processed.length} files in ${category}`); - return processed; - } - - private fileNameToDate(fileName: string): Date { - const [year, month, day] = fileName.split('.')[0].split('-').map(Number); - return new Date(year, month - 1, day); - } -} -} \ No newline at end of file diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts index e71b4603..b660b33d 100644 --- a/types/next-auth.d.ts +++ b/types/next-auth.d.ts @@ -7,23 +7,26 @@ declare module "next-auth" { first_name: string; last_name: string; username: string; - email?: string | null; + email: string; role: string[]; } & DefaultSession["user"]; - accessToken: string; + accessToken?: string; refreshToken?: string; + rocketChatToken?: string | null; + rocketChatUserId?: string | null; error?: string; } interface JWT { - sub?: string; - accessToken: string; + accessToken?: string; refreshToken?: string; - accessTokenExpires: number; - first_name: string; - last_name: string; - username: string; - role: string[]; + accessTokenExpires?: number; + first_name?: string; + last_name?: string; + username?: string; + role?: string[]; + rocketChatToken?: string | null; + rocketChatUserId?: string | null; error?: string; } @@ -32,7 +35,7 @@ declare module "next-auth" { first_name: string; last_name: string; username: string; - email?: string | null; + email: string; role: string[]; } @@ -50,14 +53,15 @@ declare module "next-auth" { declare module "next-auth/jwt" { interface JWT { - sub?: string; - accessToken: string; + accessToken?: string; refreshToken?: string; - accessTokenExpires: number; - first_name: string; - last_name: string; - username: string; - role: string[]; + accessTokenExpires?: number; + first_name?: string; + last_name?: string; + username?: string; + role?: string[]; + rocketChatToken?: string; + rocketChatUserId?: string; error?: string; } }