upd calender impotz
This commit is contained in:
parent
039582b0c8
commit
aada965ff4
|
|
@ -12,6 +12,8 @@ AZURE_TENANT_ID=<azure-ad-tenant-id>
|
||||||
# App Registration → Certificates & secrets → New client secret
|
# App Registration → Certificates & secrets → New client secret
|
||||||
AZURE_GRAPH_CLIENT_ID=<graph-app-registration-client-id>
|
AZURE_GRAPH_CLIENT_ID=<graph-app-registration-client-id>
|
||||||
AZURE_GRAPH_CLIENT_SECRET=<graph-client-secret-value>
|
AZURE_GRAPH_CLIENT_SECRET=<graph-client-secret-value>
|
||||||
|
# Fallback email when auth provides no email (e.g. API key login)
|
||||||
|
CALENDAR_USER_EMAIL=<your-email@domain.com>
|
||||||
|
|
||||||
# ── CLIENT (Vite — copy relevant lines to client/.env) ───────────────────────
|
# ── CLIENT (Vite — copy relevant lines to client/.env) ───────────────────────
|
||||||
# VITE_AZURE_CLIENT_ID=<frontend-app-registration-client-id>
|
# VITE_AZURE_CLIENT_ID=<frontend-app-registration-client-id>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
1.1.81
|
1.1.84
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
import { useUnsavedGuard } from '$lib/utils/unsavedGuard.svelte';
|
import { useUnsavedGuard } from '$lib/utils/unsavedGuard.svelte';
|
||||||
import EventCard from './EventCard.svelte';
|
import EventCard from './EventCard.svelte';
|
||||||
import { eventsForDate } from '$lib/stores/agenda';
|
import { eventsForDate } from '$lib/stores/agenda';
|
||||||
import { createEvent, updateEventNotes } from '$lib/db/repositories';
|
import { createEvent, updateEventNotes, upsertContext } from '$lib/db/repositories';
|
||||||
import { fetchCalendarEvents, type CalendarEvent } from '$lib/utils/calendarApi';
|
import { fetchCalendarEvents, type CalendarEvent } from '$lib/utils/calendarApi';
|
||||||
import { extractMentionName, quoteMention } from '$lib/actions/mentionCore';
|
import { extractMentionName, quoteMention } from '$lib/actions/mentionCore';
|
||||||
import type { PersonMeta } from '@ka-note/shared';
|
import type { PersonMeta } from '@ka-note/shared';
|
||||||
|
|
@ -222,6 +222,11 @@
|
||||||
let calendarError = $state<string | null>(null);
|
let calendarError = $state<string | null>(null);
|
||||||
let calendarEvents = $state<CalendarEvent[]>([]);
|
let calendarEvents = $state<CalendarEvent[]>([]);
|
||||||
|
|
||||||
|
// Unknown-persons dialog (shown after calendar import)
|
||||||
|
interface UnknownAttendee { name: string; email: string; selected: boolean; }
|
||||||
|
let unknownDialogOpen = $state(false);
|
||||||
|
let unknownAttendees = $state<UnknownAttendee[]>([]);
|
||||||
|
|
||||||
async function openCalendarPicker() {
|
async function openCalendarPicker() {
|
||||||
if (!showNewEventForm) openNewEventForm();
|
if (!showNewEventForm) openNewEventForm();
|
||||||
calendarLoading = true;
|
calendarLoading = true;
|
||||||
|
|
@ -241,23 +246,48 @@
|
||||||
newEventTitle = ev.subject;
|
newEventTitle = ev.subject;
|
||||||
newEventTime = ev.start;
|
newEventTime = ev.start;
|
||||||
pendingBodyPreview = ev.bodyPreview;
|
pendingBodyPreview = ev.bodyPreview;
|
||||||
|
calendarPickerOpen = false;
|
||||||
|
|
||||||
const persons = await db.contexts
|
const persons = await db.contexts
|
||||||
.filter(c => !c.deletedAt && c.type === 'person')
|
.filter(c => !c.deletedAt && c.type === 'person')
|
||||||
.toArray();
|
.toArray();
|
||||||
const mentions = ev.attendees.map(att => {
|
|
||||||
|
const mentions: string[] = [];
|
||||||
|
const unknowns: UnknownAttendee[] = [];
|
||||||
|
|
||||||
|
for (const att of ev.attendees) {
|
||||||
const match = persons.find(
|
const match = persons.find(
|
||||||
p => (p.meta as PersonMeta | null)?.email?.toLowerCase() === att.email.toLowerCase()
|
p => (p.meta as PersonMeta | null)?.email?.toLowerCase() === att.email.toLowerCase()
|
||||||
);
|
);
|
||||||
if (match) {
|
if (match) {
|
||||||
return quoteMention('@', extractMentionName(match));
|
mentions.push(quoteMention('@', extractMentionName(match)));
|
||||||
|
} else {
|
||||||
|
const cleaned = att.name.replace(/\s*\(.*?\)\s*$/, '').trim();
|
||||||
|
mentions.push(quoteMention('@', cleaned));
|
||||||
|
if (att.email) unknowns.push({ name: cleaned, email: att.email, selected: true });
|
||||||
}
|
}
|
||||||
// Strip company suffix e.g. "Lars Leifer (KRAH)" → "Lars Leifer"
|
}
|
||||||
const cleaned = att.name.replace(/\s*\(.*?\)\s*$/, '').trim();
|
|
||||||
return quoteMention('@', cleaned);
|
|
||||||
});
|
|
||||||
newEventParticipants = mentions.join(' ');
|
newEventParticipants = mentions.join(' ');
|
||||||
calendarPickerOpen = false;
|
|
||||||
|
if (unknowns.length > 0) {
|
||||||
|
unknownAttendees = unknowns;
|
||||||
|
unknownDialogOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmUnknownPersons() {
|
||||||
|
for (const u of unknownAttendees.filter(u => u.selected)) {
|
||||||
|
const slug = u.name.toLowerCase().replace(/\s+/g, '-');
|
||||||
|
await upsertContext({
|
||||||
|
id: `u-${slug}`,
|
||||||
|
name: `Person ${u.name}`,
|
||||||
|
type: 'person',
|
||||||
|
sortOrder: 99,
|
||||||
|
meta: { fullName: u.name, email: u.email, phone: '', duSince: '' } satisfies PersonMeta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
unknownDialogOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateEvents = $derived(eventsForDate(selectedDate));
|
const dateEvents = $derived(eventsForDate(selectedDate));
|
||||||
|
|
@ -468,6 +498,32 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if unknownDialogOpen}
|
||||||
|
<div class="mb-3 rounded border border-[#555] bg-[#1e1e1e] p-3 shadow-lg">
|
||||||
|
<p class="mb-2 text-sm text-[#ccc]">Unbekannte Teilnehmer anlegen?</p>
|
||||||
|
<div class="mb-3 flex flex-col gap-1.5">
|
||||||
|
{#each unknownAttendees as u}
|
||||||
|
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
|
<input type="checkbox" bind:checked={u.selected} class="accent-accent" />
|
||||||
|
<span class="text-white">{u.name}</span>
|
||||||
|
<span class="text-xs text-muted">{u.email}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="rounded bg-accent px-3 py-1 text-sm font-bold text-white hover:brightness-110"
|
||||||
|
onclick={confirmUnknownPersons}
|
||||||
|
>Anlegen</button>
|
||||||
|
<button
|
||||||
|
class="rounded bg-[#444] px-3 py-1 text-sm text-white hover:bg-[#555]"
|
||||||
|
onclick={() => (unknownDialogOpen = false)}
|
||||||
|
>Überspringen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#each $dateEvents ?? [] as event (event.id)}
|
{#each $dateEvents ?? [] as event (event.id)}
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<EventCard {event} />
|
<EventCard {event} />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE `api_keys` ADD `email` text NOT NULL DEFAULT '';
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE `api_keys` ADD `email` text DEFAULT '' NOT NULL;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -99,6 +99,13 @@
|
||||||
"when": 1772100000000,
|
"when": 1772100000000,
|
||||||
"tag": "0013_fts_search",
|
"tag": "0013_fts_search",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 14,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1772212044510,
|
||||||
|
"tag": "0014_classy_power_pack",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -144,6 +144,7 @@ export const apiKeys = sqliteTable('api_keys', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
userId: text('user_id').notNull(),
|
userId: text('user_id').notNull(),
|
||||||
label: text('label').notNull(),
|
label: text('label').notNull(),
|
||||||
|
email: text('email').notNull().default(''),
|
||||||
keyHash: text('key_hash').notNull().unique(),
|
keyHash: text('key_hash').notNull().unique(),
|
||||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||||
lastUsedAt: integer('last_used_at', { mode: 'timestamp' }),
|
lastUsedAt: integer('last_used_at', { mode: 'timestamp' }),
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export const authMiddleware = createMiddleware<AuthEnv>(async (c, next) => {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return c.json({ error: 'Invalid or revoked API key' }, 401);
|
return c.json({ error: 'Invalid or revoked API key' }, 401);
|
||||||
}
|
}
|
||||||
c.set('auth', { userId: key.userId, name: key.label, email: '' });
|
c.set('auth', { userId: key.userId, name: key.label, email: key.email ?? '' });
|
||||||
// fire-and-forget lastUsedAt update
|
// fire-and-forget lastUsedAt update
|
||||||
db.update(apiKeys).set({ lastUsedAt: new Date() }).where(eq(apiKeys.id, key.id)).catch(() => {});
|
db.update(apiKeys).set({ lastUsedAt: new Date() }).where(eq(apiKeys.id, key.id)).catch(() => {});
|
||||||
await next();
|
await next();
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ router.get('/', handle('api-keys/list', async (c) => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
router.post('/', handle('api-keys/create', async (c) => {
|
router.post('/', handle('api-keys/create', async (c) => {
|
||||||
const { userId } = c.get('auth');
|
const { userId, email } = c.get('auth');
|
||||||
const body = await c.req.json<{ label: string }>();
|
const body = await c.req.json<{ label: string }>();
|
||||||
if (!body.label?.trim()) {
|
if (!body.label?.trim()) {
|
||||||
return c.json({ error: 'label required' }, 400);
|
return c.json({ error: 'label required' }, 400);
|
||||||
|
|
@ -37,6 +37,7 @@ router.post('/', handle('api-keys/create', async (c) => {
|
||||||
await db.insert(apiKeys).values({
|
await db.insert(apiKeys).values({
|
||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
|
email,
|
||||||
label: body.label.trim(),
|
label: body.label.trim(),
|
||||||
keyHash,
|
keyHash,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ calendar.get('/events', async (c) => {
|
||||||
const auth = c.get('auth');
|
const auth = c.get('auth');
|
||||||
|
|
||||||
if (!auth.email) {
|
if (!auth.email) {
|
||||||
return c.json({ error: 'graph_unavailable', detail: 'No email in auth context (API key or dev-bypass)' }, 502);
|
return c.json({ error: 'graph_unavailable', detail: 'No email in auth context' }, 502);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue