carnet api

This commit is contained in:
alma 2025-04-20 12:43:17 +02:00
parent 2765730b47
commit ae6de407d7
5 changed files with 94 additions and 129 deletions

View File

@ -35,6 +35,7 @@ declare module "next-auth" {
role: string[]; role: string[];
}; };
accessToken: string; accessToken: string;
nextcloudToken: string;
} }
interface JWT { interface JWT {
@ -92,6 +93,29 @@ async function refreshAccessToken(token: JWT) {
} }
} }
// Add NextCloud token exchange logic
async function exchangeKeycloakTokenForNextCloud(token: string): Promise<string> {
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 = { export const authOptions: NextAuthOptions = {
providers: [ providers: [
KeycloakProvider({ KeycloakProvider({
@ -139,107 +163,31 @@ export const authOptions: NextAuthOptions = {
maxAge: 30 * 24 * 60 * 60, // 30 days maxAge: 30 * 24 * 60 * 60, // 30 days
}, },
callbacks: { callbacks: {
async jwt({ token, account, profile }) { async jwt({ token, account }) {
console.log('JWT callback start:', { if (account?.access_token) {
hasAccount: !!account, token.accessToken = account.access_token;
hasProfile: !!profile, // Only set refresh token if it exists
token if (account.refresh_token) {
}); token.refreshToken = account.refresh_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<DecodedToken>(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) { // Set expiry if it exists, otherwise set a default
console.error('Error decoding token:', error); token.accessTokenExpires = account.expires_at ? account.expires_at * 1000 : Date.now() + 3600 * 1000;
} }
}
if (Date.now() < (token.accessTokenExpires as number) * 1000) {
return token; return token;
}
return refreshAccessToken(token);
}, },
async session({ session, token }) { async session({ session, token }) {
if (token.error) { if (token) {
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; session.accessToken = token.accessToken;
// We'll handle Nextcloud authentication separately using app passwords
console.log('Session callback final session:', { session.user = {
userRoles: session.user.role, ...session.user,
session id: token.sub || '',
}); role: token.role || [],
username: token.username || '',
first_name: token.first_name || '',
last_name: token.last_name || '',
};
}
return session; return session;
} }
}, },

View File

@ -100,6 +100,7 @@ export default function CarnetPage() {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
title: note.title,
content: note.content, content: note.content,
date: note.lastEdited.toISOString(), date: note.lastEdited.toISOString(),
category: 'Notes' category: 'Notes'

View File

@ -9,7 +9,7 @@ interface EditorProps {
title: string; title: string;
content: string; content: string;
lastEdited: Date; lastEdited: Date;
}; } | null;
onSave?: (note: { id: string; title: string; content: string; lastEdited: Date }) => void; onSave?: (note: { id: string; title: string; content: string; lastEdited: Date }) => void;
} }
@ -33,9 +33,9 @@ export const Editor: React.FC<EditorProps> = ({ note, onSave }) => {
}; };
const handleSave = () => { const handleSave = () => {
if (note?.id) { if (onSave) {
onSave?.({ onSave({
id: note.id, id: note?.id || Date.now().toString(),
title, title,
content, content,
lastEdited: new Date() lastEdited: new Date()

View File

@ -5,32 +5,52 @@ export class NextCloudService {
private webdav: WebDAV; private webdav: WebDAV;
private basePath: string = '/Personal/Carnet'; private basePath: string = '/Personal/Carnet';
private subFolders: string[] = ['Journal', 'Santé', 'Notes']; private subFolders: string[] = ['Journal', 'Santé', 'Notes'];
private token: string;
private appPassword?: string;
private baseUrl: string;
constructor(token: string) { constructor(token: string, appPassword?: string) {
const nextcloudUrl = process.env.NEXTCLOUD_URL; this.token = token;
this.appPassword = appPassword;
this.baseUrl = process.env.NEXTCLOUD_URL || '';
console.log('NextCloud Configuration:', { console.log('NextCloud Configuration:', {
baseUrl: nextcloudUrl, baseUrl: this.baseUrl,
tokenLength: token?.length || 0, tokenLength: token?.length || 0,
hasToken: !!token hasToken: !!token
}); });
if (!nextcloudUrl) { if (!this.baseUrl) {
throw new Error('NEXTCLOUD_URL environment variable is not set'); 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); console.log('WebDAV endpoint:', webdavUrl);
this.webdav = new WebDAV( this.webdav = new WebDAV(
webdavUrl, webdavUrl,
{ {
headers: { headers: this.getHeaders(),
Authorization: `Bearer ${token}`,
},
} }
); );
} }
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() { private async testConnection() {
try { try {
console.log('Testing WebDAV connection...'); console.log('Testing WebDAV connection...');

38
types/next-auth.d.ts vendored
View File

@ -7,26 +7,23 @@ declare module "next-auth" {
first_name: string; first_name: string;
last_name: string; last_name: string;
username: string; username: string;
email: string; email?: string | null;
role: string[]; role: string[];
} & DefaultSession["user"]; } & DefaultSession["user"];
accessToken?: string; accessToken: string;
refreshToken?: string; refreshToken?: string;
rocketChatToken?: string | null;
rocketChatUserId?: string | null;
error?: string; error?: string;
} }
interface JWT { interface JWT {
accessToken?: string; sub?: string;
accessToken: string;
refreshToken?: string; refreshToken?: string;
accessTokenExpires?: number; accessTokenExpires: number;
first_name?: string; first_name: string;
last_name?: string; last_name: string;
username?: string; username: string;
role?: string[]; role: string[];
rocketChatToken?: string | null;
rocketChatUserId?: string | null;
error?: string; error?: string;
} }
@ -35,7 +32,7 @@ declare module "next-auth" {
first_name: string; first_name: string;
last_name: string; last_name: string;
username: string; username: string;
email: string; email?: string | null;
role: string[]; role: string[];
} }
@ -53,15 +50,14 @@ declare module "next-auth" {
declare module "next-auth/jwt" { declare module "next-auth/jwt" {
interface JWT { interface JWT {
accessToken?: string; sub?: string;
accessToken: string;
refreshToken?: string; refreshToken?: string;
accessTokenExpires?: number; accessTokenExpires: number;
first_name?: string; first_name: string;
last_name?: string; last_name: string;
username?: string; username: string;
role?: string[]; role: string[];
rocketChatToken?: string;
rocketChatUserId?: string;
error?: string; error?: string;
} }
} }