mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-21 05:58:12 +02:00
feat(minimax): add Group ID support to fix rate limit quota
Pass GroupId query param to Minimax T2A v2 API — required for paid plan quota allocation. Without it, Minimax defaults to (0/0 used). - shared/schema.ts: groupId field on minimaxConfig table - server/migrate.ts: ADD COLUMN IF NOT EXISTS group_id - server/routes/reels.ts: fetch and pass groupId alongside apiKey - server/services/ffmpeg.ts: minimax_group_id in request interface/body - ffmpeg-service/main.py: GroupId in URL, threaded through all call sites - client/src/pages/settings.tsx: Group ID input in Minimax settings card
This commit is contained in:
1 parent
cf20b0a264
commit
0209c3eacf
6 files changed
+61
-9
No files matched your search
@@ -31,6 +31,7 @@ export default function Settings() {
|
|||||||
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
|
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
|
||||||
const [externalApiKey, setExternalApiKey] = useState("");
|
const [externalApiKey, setExternalApiKey] = useState("");
|
||||||
const [minimaxApiKey, setMinimaxApiKey] = useState("");
|
const [minimaxApiKey, setMinimaxApiKey] = useState("");
|
||||||
|
const [minimaxGroupId, setMinimaxGroupId] = useState("");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
||||||
@@ -264,6 +265,7 @@ export default function Settings() {
|
|||||||
|
|
||||||
const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured;
|
const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured;
|
||||||
const hasExistingMinimaxConfig = !!(minimaxConfig as any)?.apiKey;
|
const hasExistingMinimaxConfig = !!(minimaxConfig as any)?.apiKey;
|
||||||
|
const hasExistingMinimaxGroupId = !!(minimaxConfig as any)?.groupId;
|
||||||
|
|
||||||
const saveMinimaxMutation = useMutation({
|
const saveMinimaxMutation = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
@@ -271,11 +273,15 @@ export default function Settings() {
|
|||||||
if (minimaxApiKey && minimaxApiKey.trim() !== "") {
|
if (minimaxApiKey && minimaxApiKey.trim() !== "") {
|
||||||
payload.apiKey = minimaxApiKey;
|
payload.apiKey = minimaxApiKey;
|
||||||
}
|
}
|
||||||
|
if (minimaxGroupId && minimaxGroupId.trim() !== "") {
|
||||||
|
payload.groupId = minimaxGroupId;
|
||||||
|
}
|
||||||
return apiRequest('POST', '/api/minimax/config', payload);
|
return apiRequest('POST', '/api/minimax/config', payload);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/minimax/config'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/minimax/config'] });
|
||||||
setMinimaxApiKey("");
|
setMinimaxApiKey("");
|
||||||
|
setMinimaxGroupId("");
|
||||||
toast({
|
toast({
|
||||||
title: "Configuration sauvegardée",
|
title: "Configuration sauvegardée",
|
||||||
description: "Votre clé API Minimax a été enregistrée",
|
description: "Votre clé API Minimax a été enregistrée",
|
||||||
@@ -680,6 +686,24 @@ export default function Settings() {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="minimaxGroupId">Group ID Minimax</Label>
|
||||||
|
<Input
|
||||||
|
id="minimaxGroupId"
|
||||||
|
type="text"
|
||||||
|
value={minimaxGroupId}
|
||||||
|
onChange={(e) => setMinimaxGroupId(e.target.value)}
|
||||||
|
placeholder={hasExistingMinimaxGroupId ? "••••••••••••••••" : "1234567890"}
|
||||||
|
data-testid="input-minimax-group-id"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{hasExistingMinimaxGroupId ? (
|
||||||
|
<span className="text-green-600 dark:text-green-400">✓ Group ID configuré - Laissez vide pour conserver</span>
|
||||||
|
) : (
|
||||||
|
"Requis pour les quotas corrects — trouvez-le sur platform.minimaxi.com dans votre profil"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="rounded-lg bg-muted/50 p-3 space-y-1">
|
<div className="rounded-lg bg-muted/50 p-3 space-y-1">
|
||||||
<p className="text-xs font-medium">Voix françaises disponibles :</p>
|
<p className="text-xs font-medium">Voix françaises disponibles :</p>
|
||||||
<div className="grid grid-cols-2 gap-1 text-xs text-muted-foreground">
|
<div className="grid grid-cols-2 gap-1 text-xs text-muted-foreground">
|
||||||
@@ -693,7 +717,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => saveMinimaxMutation.mutate()}
|
onClick={() => saveMinimaxMutation.mutate()}
|
||||||
disabled={saveMinimaxMutation.isPending || (!minimaxApiKey.trim() && !hasExistingMinimaxConfig)}
|
disabled={saveMinimaxMutation.isPending || (!minimaxApiKey.trim() && !minimaxGroupId.trim() && !hasExistingMinimaxConfig)}
|
||||||
data-testid="button-save-minimax"
|
data-testid="button-save-minimax"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
|
|||||||
+11
-5
@@ -161,6 +161,7 @@ class ReelRequest(BaseModel):
|
|||||||
tts_voice: str = "fr-FR-VivienneMultilingualNeural"
|
tts_voice: str = "fr-FR-VivienneMultilingualNeural"
|
||||||
tts_provider: str = "edge_tts" # "edge_tts" or "minimax"
|
tts_provider: str = "edge_tts" # "edge_tts" or "minimax"
|
||||||
minimax_api_key: Optional[str] = None
|
minimax_api_key: Optional[str] = None
|
||||||
|
minimax_group_id: Optional[str] = None
|
||||||
draw_text: bool = True
|
draw_text: bool = True
|
||||||
stabilize: bool = False # Stabilisation vidéo via vidstab
|
stabilize: bool = False # Stabilisation vidéo via vidstab
|
||||||
enable_ending_effect: bool = True
|
enable_ending_effect: bool = True
|
||||||
@@ -194,9 +195,9 @@ def clean_text_for_tts(text: str) -> str:
|
|||||||
return " ".join(text.split())
|
return " ".join(text.split())
|
||||||
|
|
||||||
|
|
||||||
def generate_tts_minimax(text: str, voice_id: str, api_key: str, audio_path: Path) -> None:
|
def generate_tts_minimax(text: str, voice_id: str, api_key: str, audio_path: Path, group_id: Optional[str] = None) -> None:
|
||||||
"""Generate TTS audio using Minimax Speech API (T2A v2)."""
|
"""Generate TTS audio using Minimax Speech API (T2A v2)."""
|
||||||
url = "https://api.minimax.io/v1/t2a_v2"
|
url = f"https://api.minimax.io/v1/t2a_v2?GroupId={group_id}" if group_id else "https://api.minimax.io/v1/t2a_v2"
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {api_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -254,6 +255,7 @@ async def generate_tts_with_subs(
|
|||||||
delay: float = 0.0,
|
delay: float = 0.0,
|
||||||
tts_provider: str = "edge_tts",
|
tts_provider: str = "edge_tts",
|
||||||
minimax_api_key: Optional[str] = None,
|
minimax_api_key: Optional[str] = None,
|
||||||
|
minimax_group_id: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Generate TTS audio with word-level synchronized subtitles.
|
"""Generate TTS audio with word-level synchronized subtitles.
|
||||||
|
|
||||||
@@ -267,8 +269,8 @@ async def generate_tts_with_subs(
|
|||||||
if not minimax_api_key:
|
if not minimax_api_key:
|
||||||
raise Exception("Minimax API key required but not provided")
|
raise Exception("Minimax API key required but not provided")
|
||||||
text_to_display = display_text if display_text else text
|
text_to_display = display_text if display_text else text
|
||||||
print(f"🔊 Minimax TTS with voice: {voice}")
|
print(f"🔊 Minimax TTS with voice: {voice}, group_id={bool(minimax_group_id)}")
|
||||||
generate_tts_minimax(text, voice, minimax_api_key, audio_path)
|
generate_tts_minimax(text, voice, minimax_api_key, audio_path, group_id=minimax_group_id)
|
||||||
|
|
||||||
audio_duration = None
|
audio_duration = None
|
||||||
try:
|
try:
|
||||||
@@ -879,7 +881,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
# Clean text for TTS (remove hashtags/emojis)
|
# Clean text for TTS (remove hashtags/emojis)
|
||||||
tts_clean_text = clean_text_for_tts(request.text)
|
tts_clean_text = clean_text_for_tts(request.text)
|
||||||
print(f"🔊 TTS enabled. Provider: {request.tts_provider}, Voice: {request.tts_voice}")
|
print(f"🔊 TTS enabled. Provider: {request.tts_provider}, Voice: {request.tts_voice}")
|
||||||
print(f"🔊 TTS has minimax_api_key: {bool(request.minimax_api_key)}")
|
print(f"🔊 TTS has minimax_api_key: {bool(request.minimax_api_key)}, group_id: {bool(request.minimax_group_id)}")
|
||||||
print(f"🔊 TTS cleaned: '{tts_clean_text[:80]}...'")
|
print(f"🔊 TTS cleaned: '{tts_clean_text[:80]}...'")
|
||||||
|
|
||||||
# Check for male/female voice map (edge_tts only)
|
# Check for male/female voice map (edge_tts only)
|
||||||
@@ -906,6 +908,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
delay=2.0,
|
delay=2.0,
|
||||||
tts_provider=request.tts_provider,
|
tts_provider=request.tts_provider,
|
||||||
minimax_api_key=request.minimax_api_key,
|
minimax_api_key=request.minimax_api_key,
|
||||||
|
minimax_group_id=request.minimax_group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify files were created
|
# Verify files were created
|
||||||
@@ -1297,6 +1300,9 @@ async def preview_tts(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
tts_audio_path,
|
tts_audio_path,
|
||||||
tts_srt_path,
|
tts_srt_path,
|
||||||
display_text=clean_text_for_display(request.text),
|
display_text=clean_text_for_display(request.text),
|
||||||
|
tts_provider=request.tts_provider,
|
||||||
|
minimax_api_key=request.minimax_api_key,
|
||||||
|
minimax_group_id=request.minimax_group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not tts_audio_path.exists():
|
if not tts_audio_path.exists():
|
||||||
|
|||||||
@@ -164,6 +164,12 @@ export async function migrate() {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// minimax_config.group_id
|
||||||
|
await client.query(`
|
||||||
|
ALTER TABLE "minimax_config"
|
||||||
|
ADD COLUMN IF NOT EXISTS "group_id" text;
|
||||||
|
`);
|
||||||
|
|
||||||
console.log("[Migration] Safe migration completed.");
|
console.log("[Migration] Safe migration completed.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Migration] Error during migration:", error);
|
console.error("[Migration] Error during migration:", error);
|
||||||
|
|||||||
+10
-2
@@ -354,12 +354,14 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
|||||||
const finalWordDuration = wordDuration;
|
const finalWordDuration = wordDuration;
|
||||||
|
|
||||||
let minimaxApiKey: string | undefined;
|
let minimaxApiKey: string | undefined;
|
||||||
|
let minimaxGroupId: string | undefined;
|
||||||
if (ttsEnabled && ttsProvider === 'minimax') {
|
if (ttsEnabled && ttsProvider === 'minimax') {
|
||||||
const minimaxCfg = await storage.getMinimaxConfig(user.id);
|
const minimaxCfg = await storage.getMinimaxConfig(user.id);
|
||||||
if (!minimaxCfg?.apiKey) {
|
if (!minimaxCfg?.apiKey) {
|
||||||
return res.status(400).json({ error: 'Clé API Minimax non configurée. Allez dans les Paramètres.' });
|
return res.status(400).json({ error: 'Clé API Minimax non configurée. Allez dans les Paramètres.' });
|
||||||
}
|
}
|
||||||
minimaxApiKey = minimaxCfg.apiKey;
|
minimaxApiKey = minimaxCfg.apiKey;
|
||||||
|
minimaxGroupId = minimaxCfg.groupId ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traiter la vidéo via FFmpeg
|
// Traiter la vidéo via FFmpeg
|
||||||
@@ -370,6 +372,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
|||||||
ttsVoice,
|
ttsVoice,
|
||||||
ttsProvider,
|
ttsProvider,
|
||||||
minimaxApiKey,
|
minimaxApiKey,
|
||||||
|
minimaxGroupId,
|
||||||
wordDuration: finalWordDuration,
|
wordDuration: finalWordDuration,
|
||||||
fontSize,
|
fontSize,
|
||||||
musicVolume,
|
musicVolume,
|
||||||
@@ -409,15 +412,17 @@ reelsRouter.post('/reels/tts-preview', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let minimaxApiKey: string | undefined;
|
let minimaxApiKey: string | undefined;
|
||||||
|
let minimaxGroupId: string | undefined;
|
||||||
if (ttsProvider === 'minimax') {
|
if (ttsProvider === 'minimax') {
|
||||||
const minimaxCfg = await storage.getMinimaxConfig(user.id);
|
const minimaxCfg = await storage.getMinimaxConfig(user.id);
|
||||||
if (!minimaxCfg?.apiKey) {
|
if (!minimaxCfg?.apiKey) {
|
||||||
return res.status(400).json({ error: 'Clé API Minimax non configurée. Allez dans les Paramètres.' });
|
return res.status(400).json({ error: 'Clé API Minimax non configurée. Allez dans les Paramètres.' });
|
||||||
}
|
}
|
||||||
minimaxApiKey = minimaxCfg.apiKey;
|
minimaxApiKey = minimaxCfg.apiKey;
|
||||||
|
minimaxGroupId = minimaxCfg.groupId ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await ffmpegService.previewTTS(text, voice, ttsProvider, minimaxApiKey);
|
const result = await ffmpegService.previewTTS(text, voice, ttsProvider, minimaxApiKey, minimaxGroupId);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
|
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
|
||||||
@@ -610,16 +615,18 @@ async function processReelBackground(
|
|||||||
const finalWordDuration = wordDuration ?? 0.6;
|
const finalWordDuration = wordDuration ?? 0.6;
|
||||||
|
|
||||||
let minimaxApiKeyBg: string | undefined;
|
let minimaxApiKeyBg: string | undefined;
|
||||||
|
let minimaxGroupIdBg: string | undefined;
|
||||||
if (ttsEnabled && ttsProvider === 'minimax') {
|
if (ttsEnabled && ttsProvider === 'minimax') {
|
||||||
const minimaxCfg = await storage.getMinimaxConfig(userId);
|
const minimaxCfg = await storage.getMinimaxConfig(userId);
|
||||||
if (minimaxCfg?.apiKey) {
|
if (minimaxCfg?.apiKey) {
|
||||||
minimaxApiKeyBg = minimaxCfg.apiKey;
|
minimaxApiKeyBg = minimaxCfg.apiKey;
|
||||||
|
minimaxGroupIdBg = minimaxCfg.groupId ?? undefined;
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ [Background] Minimax API key not found in DB for user:', userId);
|
console.error('❌ [Background] Minimax API key not found in DB for user:', userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔊 [Background] TTS config:', { ttsEnabled, ttsProvider, ttsVoice, hasMinimaxKey: !!minimaxApiKeyBg });
|
console.log('🔊 [Background] TTS config:', { ttsEnabled, ttsProvider, ttsVoice, hasMinimaxKey: !!minimaxApiKeyBg, hasGroupId: !!minimaxGroupIdBg });
|
||||||
|
|
||||||
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||||
text: overlayText,
|
text: overlayText,
|
||||||
@@ -628,6 +635,7 @@ async function processReelBackground(
|
|||||||
ttsVoice,
|
ttsVoice,
|
||||||
ttsProvider,
|
ttsProvider,
|
||||||
minimaxApiKey: minimaxApiKeyBg,
|
minimaxApiKey: minimaxApiKeyBg,
|
||||||
|
minimaxGroupId: minimaxGroupIdBg,
|
||||||
wordDuration: finalWordDuration,
|
wordDuration: finalWordDuration,
|
||||||
fontSize,
|
fontSize,
|
||||||
musicVolume,
|
musicVolume,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface FFmpegReelRequest {
|
|||||||
tts_voice?: string; // Voix TTS (ex: fr-FR-VivienneNeural)
|
tts_voice?: string; // Voix TTS (ex: fr-FR-VivienneNeural)
|
||||||
tts_provider?: string; // Provider TTS: "edge_tts" (défaut) ou "minimax"
|
tts_provider?: string; // Provider TTS: "edge_tts" (défaut) ou "minimax"
|
||||||
minimax_api_key?: string; // Clé API Minimax (requis si tts_provider=minimax)
|
minimax_api_key?: string; // Clé API Minimax (requis si tts_provider=minimax)
|
||||||
|
minimax_group_id?: string; // Group ID Minimax (requis pour quotas corrects)
|
||||||
word_duration?: number; // Durée par mot (default: 0.6s)
|
word_duration?: number; // Durée par mot (default: 0.6s)
|
||||||
font_size?: number; // Taille police (default: 24)
|
font_size?: number; // Taille police (default: 24)
|
||||||
music_volume?: number; // Volume musique (default: 0.25)
|
music_volume?: number; // Volume musique (default: 0.25)
|
||||||
@@ -76,6 +77,7 @@ export class FFmpegService {
|
|||||||
ttsVoice?: string;
|
ttsVoice?: string;
|
||||||
ttsProvider?: string;
|
ttsProvider?: string;
|
||||||
minimaxApiKey?: string;
|
minimaxApiKey?: string;
|
||||||
|
minimaxGroupId?: string;
|
||||||
wordDuration?: number;
|
wordDuration?: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
musicVolume?: number;
|
musicVolume?: number;
|
||||||
@@ -97,6 +99,7 @@ export class FFmpegService {
|
|||||||
tts_voice: options.ttsVoice,
|
tts_voice: options.ttsVoice,
|
||||||
tts_provider: options.ttsProvider,
|
tts_provider: options.ttsProvider,
|
||||||
minimax_api_key: options.minimaxApiKey,
|
minimax_api_key: options.minimaxApiKey,
|
||||||
|
minimax_group_id: options.minimaxGroupId,
|
||||||
word_duration: options.wordDuration ?? 0.6,
|
word_duration: options.wordDuration ?? 0.6,
|
||||||
font_size: options.fontSize ?? 64,
|
font_size: options.fontSize ?? 64,
|
||||||
music_volume: options.musicVolume ?? 0.25,
|
music_volume: options.musicVolume ?? 0.25,
|
||||||
@@ -181,6 +184,7 @@ export class FFmpegService {
|
|||||||
ttsVoice?: string;
|
ttsVoice?: string;
|
||||||
ttsProvider?: string;
|
ttsProvider?: string;
|
||||||
minimaxApiKey?: string;
|
minimaxApiKey?: string;
|
||||||
|
minimaxGroupId?: string;
|
||||||
wordDuration?: number;
|
wordDuration?: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
musicVolume?: number;
|
musicVolume?: number;
|
||||||
@@ -202,6 +206,7 @@ export class FFmpegService {
|
|||||||
tts_voice: options.ttsVoice,
|
tts_voice: options.ttsVoice,
|
||||||
tts_provider: options.ttsProvider,
|
tts_provider: options.ttsProvider,
|
||||||
minimax_api_key: options.minimaxApiKey,
|
minimax_api_key: options.minimaxApiKey,
|
||||||
|
minimax_group_id: options.minimaxGroupId,
|
||||||
word_duration: options.wordDuration ?? 0.6,
|
word_duration: options.wordDuration ?? 0.6,
|
||||||
font_size: options.fontSize ?? 64,
|
font_size: options.fontSize ?? 64,
|
||||||
music_volume: options.musicVolume ?? 0.25,
|
music_volume: options.musicVolume ?? 0.25,
|
||||||
@@ -304,7 +309,8 @@ export class FFmpegService {
|
|||||||
text: string,
|
text: string,
|
||||||
voice: string,
|
voice: string,
|
||||||
ttsProvider?: string,
|
ttsProvider?: string,
|
||||||
minimaxApiKey?: string
|
minimaxApiKey?: string,
|
||||||
|
minimaxGroupId?: string
|
||||||
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
|
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
|
||||||
const config = this.ensureConfigured();
|
const config = this.ensureConfigured();
|
||||||
|
|
||||||
@@ -320,6 +326,7 @@ export class FFmpegService {
|
|||||||
tts_voice: voice,
|
tts_voice: voice,
|
||||||
tts_provider: ttsProvider,
|
tts_provider: ttsProvider,
|
||||||
minimax_api_key: minimaxApiKey,
|
minimax_api_key: minimaxApiKey,
|
||||||
|
minimax_group_id: minimaxGroupId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export const minimaxConfig = pgTable("minimax_config", {
|
|||||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
||||||
apiKey: text("api_key").notNull(),
|
apiKey: text("api_key").notNull(),
|
||||||
|
groupId: text("group_id"),
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user