feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)
New features: - HLS video encoding/streaming (hls routes, hls-encoder) and a media transcripts pipeline (transcripts route + media-transcript queries) - AI assistant chat vs. run model: agent-course-generation worker/queue, run-monitor pane, chat-changes card, content ask-ai bar - Per-theme course-page previews across org landing-page themes (bold, classic, corporate, editorial, etc.) with a shared course token/component set - View-as-student mode and login-link / link-invite invite flows Refactor: - Extract packages/core consolidating config (env, storage, dashboard-url), services and utils previously duplicated under apps/api/src/config - Remove legacy agent service modules (chat-tools, chat-context, context-window, document, fetch-url, lesson-content) - Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding - Add format script to tenant-router so turbo run format covers it
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
export * from './organization';
|
||||
export * from './quiz';
|
||||
export * from './invite';
|
||||
export * from './link-invite';
|
||||
export * from './automation-key';
|
||||
export * from './automation-usage';
|
||||
|
||||
@@ -257,10 +257,12 @@ export async function getLatestOrgInvitesByEmails(
|
||||
|
||||
const latestByEmail = new Map<string, TLatestOrgInviteByEmail>();
|
||||
for (const row of rows) {
|
||||
if (!row.email) continue;
|
||||
|
||||
const key = row.email.toLowerCase();
|
||||
const existing = latestByEmail.get(key);
|
||||
if (!existing || new Date(row.createdAt) > new Date(existing.createdAt)) {
|
||||
latestByEmail.set(key, row);
|
||||
latestByEmail.set(key, { ...row, email: row.email });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import * as schema from '@db/schema';
|
||||
|
||||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import { db, type DbOrTxClient } from '@db/drizzle';
|
||||
import type { TOrganizationInvite } from '@db/types';
|
||||
|
||||
export type TLinkInviteRow = TOrganizationInvite & {
|
||||
organization: { id: string; name: string; siteName: string };
|
||||
};
|
||||
|
||||
export async function getOrgLinkInvite(organizationId: string): Promise<TOrganizationInvite | null> {
|
||||
try {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(schema.organizationInvite)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.organizationInvite.organizationId, organizationId),
|
||||
eq(schema.organizationInvite.type, 'LINK'),
|
||||
eq(schema.organizationInvite.isRevoked, false)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return row ?? null;
|
||||
} catch (error) {
|
||||
console.error('getOrgLinkInvite error:', error);
|
||||
throw new Error(`Failed to get org link invite: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createLinkInvite(data: {
|
||||
organizationId: string;
|
||||
roleId: number;
|
||||
tokenHash: string;
|
||||
createdByProfileId: string;
|
||||
expiresAt: string;
|
||||
metadata: Record<string, unknown>;
|
||||
}): Promise<TOrganizationInvite> {
|
||||
try {
|
||||
const [created] = await db
|
||||
.insert(schema.organizationInvite)
|
||||
.values({
|
||||
...data,
|
||||
type: 'LINK',
|
||||
email: null,
|
||||
isRevoked: false
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!created) {
|
||||
throw new Error('Failed to create link invite');
|
||||
}
|
||||
|
||||
return created;
|
||||
} catch (error) {
|
||||
console.error('createLinkInvite error:', error);
|
||||
throw new Error(`Failed to create link invite: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setLinkInviteRevoked(
|
||||
organizationId: string,
|
||||
isRevoked: boolean,
|
||||
profileId: string
|
||||
): Promise<TOrganizationInvite | null> {
|
||||
try {
|
||||
const [updated] = await db
|
||||
.update(schema.organizationInvite)
|
||||
.set({
|
||||
isRevoked,
|
||||
revokedByProfileId: isRevoked ? profileId : null,
|
||||
revokedAt: isRevoked ? new Date().toISOString() : null,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
.where(
|
||||
and(eq(schema.organizationInvite.organizationId, organizationId), eq(schema.organizationInvite.type, 'LINK'))
|
||||
)
|
||||
.returning();
|
||||
|
||||
return updated ?? null;
|
||||
} catch (error) {
|
||||
console.error('setLinkInviteRevoked error:', error);
|
||||
throw new Error(`Failed to update link invite: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrgLinkInviteWithOrg(
|
||||
dbClient: DbOrTxClient,
|
||||
tokenHash: string
|
||||
): Promise<{ invite: TOrganizationInvite; organization: { id: string; name: string; siteName: string } } | null> {
|
||||
try {
|
||||
const [row] = await dbClient
|
||||
.select({
|
||||
invite: schema.organizationInvite,
|
||||
organization: {
|
||||
id: schema.organization.id,
|
||||
name: schema.organization.name,
|
||||
siteName: schema.organization.siteName
|
||||
}
|
||||
})
|
||||
.from(schema.organizationInvite)
|
||||
.innerJoin(schema.organization, eq(schema.organizationInvite.organizationId, schema.organization.id))
|
||||
.where(and(eq(schema.organizationInvite.tokenHash, tokenHash), eq(schema.organizationInvite.type, 'LINK')))
|
||||
.limit(1);
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
...row,
|
||||
organization: { ...row.organization, siteName: row.organization.siteName ?? '' }
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('getOrgLinkInviteWithOrg error:', error);
|
||||
throw new Error(`Failed to load link invite: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
@@ -913,9 +913,7 @@ export const getOrganizationPlanBySubscriptionId = async (
|
||||
return plan ?? null;
|
||||
} catch (error) {
|
||||
console.error('getOrganizationPlanBySubscriptionId error:', error);
|
||||
throw new Error(
|
||||
`Failed to fetch organization plan: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
throw new Error(`Failed to fetch organization plan: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user