tetrons 2.2.5 → 2.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/api/register/route.d.ts +1 -0
- package/dist/app/api/register/route.js +23 -17
- package/dist/app/api/validate/route.d.ts +3 -0
- package/dist/app/api/validate/route.js +52 -12
- package/dist/app/page.jsx +28 -39
- package/dist/components/tetrons/EditorContent.jsx +29 -13
- package/dist/components/tetrons/toolbar/TetronsToolbar.d.ts +2 -1
- package/dist/components/tetrons/toolbar/TetronsToolbar.jsx +13 -11
- package/dist/index.d.ts +5 -1
- package/dist/index.js +26 -2
- package/dist/utils/apiKeyUtils.d.ts +9 -9
- package/dist/utils/apiKeyUtils.js +27 -11
- package/package.json +1 -1
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { connectDB } from "../../../lib/db";
|
|
3
3
|
import { ApiKey } from "../../../models/ApiKey";
|
|
4
|
-
import {
|
|
4
|
+
import { generateUserApiKey, getFreeApiKey } from "../../../utils/apiKeyUtils";
|
|
5
5
|
export async function POST(req) {
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const body = await req.json();
|
|
7
|
+
const { email, organization, version } = body;
|
|
8
|
+
if (!email || !organization || !version) {
|
|
8
9
|
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
|
10
|
+
}
|
|
9
11
|
await connectDB();
|
|
12
|
+
const lowerEmail = email.trim().toLowerCase();
|
|
13
|
+
const lowerOrg = organization.trim().toLowerCase();
|
|
14
|
+
await ApiKey.deleteMany({ email: lowerEmail, version });
|
|
15
|
+
let apiKey;
|
|
16
|
+
let expiresAt = null;
|
|
10
17
|
if (version === "free") {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
organization,
|
|
17
|
-
version,
|
|
18
|
-
apiKey,
|
|
19
|
-
expiresAt,
|
|
20
|
-
});
|
|
21
|
-
return NextResponse.json({ apiKey, expiresAt });
|
|
18
|
+
apiKey = getFreeApiKey();
|
|
19
|
+
expiresAt = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
apiKey = generateUserApiKey(lowerEmail, lowerOrg, version);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
await ApiKey.create({
|
|
25
|
+
email: lowerEmail,
|
|
26
|
+
organization: lowerOrg,
|
|
27
|
+
version,
|
|
28
|
+
apiKey,
|
|
29
|
+
expiresAt,
|
|
30
|
+
});
|
|
31
|
+
return NextResponse.json({ apiKey, expiresAt });
|
|
26
32
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
export declare function OPTIONS(): Promise<Response>;
|
|
2
3
|
export declare function POST(req: NextRequest): Promise<NextResponse<{
|
|
3
4
|
error: string;
|
|
4
5
|
}> | NextResponse<{
|
|
5
6
|
valid: boolean;
|
|
6
7
|
version: any;
|
|
8
|
+
email: any;
|
|
9
|
+
organization: any;
|
|
7
10
|
}>>;
|
|
@@ -1,18 +1,58 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { connectDB } from "../../../lib/db";
|
|
3
3
|
import { ApiKey } from "../../../models/ApiKey";
|
|
4
|
+
const corsHeaders = {
|
|
5
|
+
"Access-Control-Allow-Origin": "*",
|
|
6
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
7
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
8
|
+
};
|
|
9
|
+
export async function OPTIONS() {
|
|
10
|
+
return new Response(null, {
|
|
11
|
+
status: 204,
|
|
12
|
+
headers: corsHeaders,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
4
15
|
export async function POST(req) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
try {
|
|
17
|
+
const body = (await req.json());
|
|
18
|
+
const apiKey = body.apiKey;
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
return NextResponse.json({ error: "API key required" }, {
|
|
21
|
+
status: 400,
|
|
22
|
+
headers: corsHeaders,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
await connectDB();
|
|
26
|
+
const keyEntry = await ApiKey.findOne({ apiKey });
|
|
27
|
+
if (!keyEntry) {
|
|
28
|
+
return NextResponse.json({ error: "Invalid API key" }, {
|
|
29
|
+
status: 401,
|
|
30
|
+
headers: corsHeaders,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (keyEntry.version === "free" &&
|
|
34
|
+
keyEntry.expiresAt &&
|
|
35
|
+
new Date() > new Date(keyEntry.expiresAt)) {
|
|
36
|
+
return NextResponse.json({ error: "Free trial expired" }, {
|
|
37
|
+
status: 403,
|
|
38
|
+
headers: corsHeaders,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return NextResponse.json({
|
|
42
|
+
valid: true,
|
|
43
|
+
version: keyEntry.version,
|
|
44
|
+
email: keyEntry.email,
|
|
45
|
+
organization: keyEntry.organization,
|
|
46
|
+
}, {
|
|
47
|
+
status: 200,
|
|
48
|
+
headers: corsHeaders,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error("Validation Error:", err);
|
|
53
|
+
return NextResponse.json({ error: "Server error" }, {
|
|
54
|
+
status: 500,
|
|
55
|
+
headers: corsHeaders,
|
|
56
|
+
});
|
|
16
57
|
}
|
|
17
|
-
return NextResponse.json({ valid: true, version: keyEntry.version });
|
|
18
58
|
}
|
package/dist/app/page.jsx
CHANGED
|
@@ -1,58 +1,47 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
import { useSearchParams } from "next/navigation";
|
|
4
3
|
import EditorContent from "../components/tetrons/EditorContent";
|
|
5
4
|
import "../styles/tetrons.css";
|
|
6
5
|
export default function Home() {
|
|
7
|
-
const searchParams = useSearchParams();
|
|
8
|
-
const urlKey = searchParams.get("apiKey");
|
|
9
6
|
const [apiKey, setApiKey] = useState(null);
|
|
10
7
|
const [loading, setLoading] = useState(true);
|
|
11
8
|
useEffect(() => {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
const fetchOrGenerateApiKey = async () => {
|
|
10
|
+
let key = localStorage.getItem("tetrons-key");
|
|
11
|
+
if (!key) {
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch("/api/register", {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
email: "developer@finapsys.co.in",
|
|
18
|
+
organization: "FCSPL",
|
|
19
|
+
version: "free",
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
const data = await res.json();
|
|
23
|
+
if (!res.ok || !data.apiKey) {
|
|
24
|
+
throw new Error(data.error || "Failed to register for API key");
|
|
25
|
+
}
|
|
26
|
+
key = data.apiKey;
|
|
27
|
+
if (key) {
|
|
28
|
+
localStorage.setItem("tetrons-key", key);
|
|
29
|
+
}
|
|
19
30
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
setApiKey(cachedKey);
|
|
23
|
-
setLoading(false);
|
|
24
|
-
return;
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error("❌ Failed to fetch or register API key:", err);
|
|
25
33
|
}
|
|
26
|
-
const res = await fetch("/api/register", {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: { "Content-Type": "application/json" },
|
|
29
|
-
body: JSON.stringify({
|
|
30
|
-
email: "mr.swastikjha@gmail.com",
|
|
31
|
-
organization: "FCSPL",
|
|
32
|
-
version: "free",
|
|
33
|
-
}),
|
|
34
|
-
});
|
|
35
|
-
const data = await res.json();
|
|
36
|
-
if (!res.ok || !data.apiKey) {
|
|
37
|
-
throw new Error(data.error || "Failed to fetch API key");
|
|
38
|
-
}
|
|
39
|
-
localStorage.setItem("my-api-key", data.apiKey);
|
|
40
|
-
setApiKey(data.apiKey);
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
console.error("Error fetching API key:", error);
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
setLoading(false);
|
|
47
34
|
}
|
|
35
|
+
setApiKey(key);
|
|
36
|
+
setLoading(false);
|
|
48
37
|
};
|
|
49
|
-
|
|
50
|
-
}, [
|
|
38
|
+
fetchOrGenerateApiKey();
|
|
39
|
+
}, []);
|
|
51
40
|
if (loading) {
|
|
52
41
|
return <div className="text-center p-4">⏳ Loading Editor...</div>;
|
|
53
42
|
}
|
|
54
43
|
if (!apiKey) {
|
|
55
|
-
return
|
|
44
|
+
return <div className="text-red-600 text-center">❌ API key not found</div>;
|
|
56
45
|
}
|
|
57
46
|
return (<main className="flex flex-col h-screen overflow-hidden">
|
|
58
47
|
<div className="flex-1 overflow-auto flex flex-col">
|
|
@@ -49,11 +49,25 @@ export default function EditorContent({ apiKey }) {
|
|
|
49
49
|
const [isValid, setIsValid] = React.useState(null);
|
|
50
50
|
const [error, setError] = React.useState(null);
|
|
51
51
|
const [versions, setVersions] = React.useState([]);
|
|
52
|
+
const [userVersion, setUserVersion] = React.useState(null);
|
|
52
53
|
const [currentVersionIndex, setCurrentVersionIndex] = React.useState(null);
|
|
54
|
+
function getApiBaseUrl() {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
if (typeof import.meta !== "undefined" &&
|
|
57
|
+
((_a = import.meta.env) === null || _a === void 0 ? void 0 : _a.VITE_TETRONS_API_URL)) {
|
|
58
|
+
return import.meta.env.VITE_TETRONS_API_URL;
|
|
59
|
+
}
|
|
60
|
+
if (typeof process !== "undefined" &&
|
|
61
|
+
((_b = process.env) === null || _b === void 0 ? void 0 : _b.NEXT_PUBLIC_TETRONS_API_URL)) {
|
|
62
|
+
return process.env.NEXT_PUBLIC_TETRONS_API_URL;
|
|
63
|
+
}
|
|
64
|
+
return "http://localhost:3000";
|
|
65
|
+
}
|
|
66
|
+
const API_BASE_URL = getApiBaseUrl();
|
|
53
67
|
useEffect(() => {
|
|
54
68
|
const validateKey = async () => {
|
|
55
69
|
try {
|
|
56
|
-
const res = await fetch(
|
|
70
|
+
const res = await fetch(`${API_BASE_URL}/api/validate`, {
|
|
57
71
|
method: "POST",
|
|
58
72
|
headers: {
|
|
59
73
|
"Content-Type": "application/json",
|
|
@@ -64,6 +78,7 @@ export default function EditorContent({ apiKey }) {
|
|
|
64
78
|
if (!res.ok)
|
|
65
79
|
throw new Error(data.error || "Invalid API Key");
|
|
66
80
|
setIsValid(true);
|
|
81
|
+
setUserVersion(data.version);
|
|
67
82
|
}
|
|
68
83
|
catch (err) {
|
|
69
84
|
if (err instanceof Error) {
|
|
@@ -169,20 +184,21 @@ export default function EditorContent({ apiKey }) {
|
|
|
169
184
|
return <div className="editor-loading">🔍 Validating license...</div>;
|
|
170
185
|
}
|
|
171
186
|
return (<div className="editor-container">
|
|
172
|
-
<div className="editor-toolbar">
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
187
|
+
{userVersion !== "free" && (<div className="editor-toolbar">
|
|
188
|
+
<button type="button" onClick={saveVersion} disabled={!editor} className="editor-save-btn">
|
|
189
|
+
Save Version
|
|
190
|
+
</button>
|
|
176
191
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
192
|
+
<div className="editor-versions-wrapper">
|
|
193
|
+
{versions.length === 0 && (<span className="editor-no-versions">No saved versions</span>)}
|
|
194
|
+
{versions.map((_, idx) => (<button type="button" key={idx} onClick={() => restoreVersion(idx)} className={`editor-version-btn ${idx === currentVersionIndex ? "active" : ""}`} title={`Restore Version ${idx + 1}`}>
|
|
195
|
+
{`V${idx + 1}`}
|
|
196
|
+
</button>))}
|
|
197
|
+
</div>
|
|
198
|
+
</div>)}
|
|
199
|
+
|
|
200
|
+
{editor && userVersion && (<TetronsToolbar editor={editor} version={userVersion}/>)}
|
|
184
201
|
|
|
185
|
-
{editor && <TetronsToolbar editor={editor}/>}
|
|
186
202
|
<div ref={wrapperRef} className="editor-content-wrapper" onClick={handleEditorClick}>
|
|
187
203
|
{editor ? (<>
|
|
188
204
|
<TiptapEditorContent editor={editor}/>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Editor } from "@tiptap/react";
|
|
3
|
-
export default function TetronsToolbar({ editor }: {
|
|
3
|
+
export default function TetronsToolbar({ editor, version, }: {
|
|
4
4
|
editor: Editor;
|
|
5
|
+
version: "free" | "pro" | "premium" | "platinum";
|
|
5
6
|
}): React.JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { useEffect } from "react";
|
|
2
|
+
import React, { useEffect, useState } from "react";
|
|
3
3
|
import ActionGroup from "./ActionGroup";
|
|
4
4
|
import ClipboardGroup from "./ClipboardGroup";
|
|
5
5
|
import FontStyleGroup from "./FontStyleGroup";
|
|
@@ -7,8 +7,8 @@ import InsertGroup from "./InsertGroup";
|
|
|
7
7
|
import ListAlignGroup from "./ListAlignGroup";
|
|
8
8
|
import MiscGroup from "./MiscGroup";
|
|
9
9
|
import FileGroup from "./FileGroup";
|
|
10
|
-
export default function TetronsToolbar({ editor }) {
|
|
11
|
-
const [autoSave, setAutoSave] =
|
|
10
|
+
export default function TetronsToolbar({ editor, version, }) {
|
|
11
|
+
const [autoSave, setAutoSave] = useState(false);
|
|
12
12
|
useEffect(() => {
|
|
13
13
|
if (!editor)
|
|
14
14
|
return;
|
|
@@ -28,17 +28,19 @@ export default function TetronsToolbar({ editor }) {
|
|
|
28
28
|
};
|
|
29
29
|
}, [autoSave, editor]);
|
|
30
30
|
return (<div className="tetrons-toolbar">
|
|
31
|
-
<div className="group">
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
{version !== "free" && (<div className="group">
|
|
32
|
+
<input type="checkbox" id="autoSave" checked={autoSave} onChange={(e) => setAutoSave(e.target.checked)}/>
|
|
33
|
+
<label htmlFor="autoSave">Auto Save</label>
|
|
34
|
+
</div>)}
|
|
35
35
|
|
|
36
|
-
<FileGroup editor={editor}/>
|
|
36
|
+
{["pro", "premium", "platinum"].includes(version) && (<FileGroup editor={editor}/>)}
|
|
37
37
|
<ClipboardGroup editor={editor}/>
|
|
38
38
|
<FontStyleGroup editor={editor}/>
|
|
39
39
|
<ListAlignGroup editor={editor}/>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
{["premium", "platinum"].includes(version) && (<>
|
|
41
|
+
<InsertGroup editor={editor}/>
|
|
42
|
+
<ActionGroup editor={editor}/>
|
|
43
|
+
</>)}
|
|
44
|
+
{version === "platinum" && <MiscGroup editor={editor}/>}
|
|
43
45
|
</div>);
|
|
44
46
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
export { default as EditorContent } from "./components/tetrons/EditorContent";
|
|
2
1
|
import EditorContent from "./components/tetrons/EditorContent";
|
|
2
|
+
import "./styles/tetrons.css";
|
|
3
|
+
export declare function initializeTetrons(apiKey: string): Promise<void>;
|
|
4
|
+
export declare function getTetronsVersion(): "" | "free" | "pro" | "premium" | "platinum";
|
|
5
|
+
export declare function isApiKeyValid(): boolean;
|
|
6
|
+
export { EditorContent };
|
|
3
7
|
export default EditorContent;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
export { default as EditorContent } from "./components/tetrons/EditorContent";
|
|
2
|
-
// import "./styles/tetrons.css";
|
|
3
1
|
import EditorContent from "./components/tetrons/EditorContent";
|
|
2
|
+
import "./styles/tetrons.css";
|
|
3
|
+
let API_VALID = false;
|
|
4
|
+
let API_VERSION = "";
|
|
5
|
+
export async function initializeTetrons(apiKey) {
|
|
6
|
+
const res = await fetch("http://localhost:3000/api/validate", {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers: {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
},
|
|
11
|
+
body: JSON.stringify({ apiKey }),
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
const error = await res.json();
|
|
15
|
+
throw new Error(`API Key validation failed: ${error.error}`);
|
|
16
|
+
}
|
|
17
|
+
const data = await res.json();
|
|
18
|
+
API_VALID = data.valid;
|
|
19
|
+
API_VERSION = data.version;
|
|
20
|
+
}
|
|
21
|
+
export function getTetronsVersion() {
|
|
22
|
+
return API_VERSION;
|
|
23
|
+
}
|
|
24
|
+
export function isApiKeyValid() {
|
|
25
|
+
return API_VALID;
|
|
26
|
+
}
|
|
27
|
+
export { EditorContent };
|
|
4
28
|
export default EditorContent;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export declare function getFreeApiKey(): string;
|
|
2
|
+
declare const VERSION_PREFIXES: {
|
|
3
|
+
readonly pro: "PRO";
|
|
4
|
+
readonly premium: "PREMIUM";
|
|
5
|
+
readonly platinum: "PLATINUM";
|
|
6
|
+
};
|
|
7
|
+
export type VersionType = keyof typeof VERSION_PREFIXES;
|
|
8
|
+
export declare function generateUserApiKey(email: string, organization: string, version: VersionType): string;
|
|
6
9
|
export declare function generateApiKey(length?: number): string;
|
|
7
|
-
/**
|
|
8
|
-
* Generates a unique key with optional prefix (e.g., for versioning or branding)
|
|
9
|
-
* @param prefix Optional prefix like 'PRO-', 'PREMIUM-', etc.
|
|
10
|
-
*/
|
|
11
10
|
export declare function generateVersionedKey(prefix?: string): string;
|
|
11
|
+
export {};
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
const FREE_API_KEY = process.env.TETRONS_FREE_KEY || "TETRONS_FREE_KEY";
|
|
3
|
+
const SECRET_KEY = process.env.API_KEY_SECRET || "default-secret-key";
|
|
4
|
+
export function getFreeApiKey() {
|
|
5
|
+
return FREE_API_KEY;
|
|
6
|
+
}
|
|
7
|
+
const VERSION_PREFIXES = {
|
|
8
|
+
pro: "PRO",
|
|
9
|
+
premium: "PREMIUM",
|
|
10
|
+
platinum: "PLATINUM",
|
|
11
|
+
};
|
|
12
|
+
export function generateUserApiKey(email, organization, version) {
|
|
13
|
+
const input = `${email.trim().toLowerCase()}:${organization
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase()}`;
|
|
16
|
+
const hash = crypto
|
|
17
|
+
.createHmac("sha256", SECRET_KEY)
|
|
18
|
+
.update(input)
|
|
19
|
+
.digest("hex")
|
|
20
|
+
.toUpperCase();
|
|
21
|
+
const prefix = VERSION_PREFIXES[version];
|
|
22
|
+
return `${prefix}${hash.slice(0, 48 - prefix.length)}`;
|
|
23
|
+
}
|
|
7
24
|
export function generateApiKey(length = 48) {
|
|
8
|
-
return crypto
|
|
25
|
+
return crypto
|
|
26
|
+
.randomBytes(length / 2)
|
|
27
|
+
.toString("hex")
|
|
28
|
+
.toUpperCase();
|
|
9
29
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Generates a unique key with optional prefix (e.g., for versioning or branding)
|
|
12
|
-
* @param prefix Optional prefix like 'PRO-', 'PREMIUM-', etc.
|
|
13
|
-
*/
|
|
14
30
|
export function generateVersionedKey(prefix = "") {
|
|
15
|
-
const key = generateApiKey(48);
|
|
31
|
+
const key = generateApiKey(48 - prefix.length);
|
|
16
32
|
return `${prefix}${key}`;
|
|
17
33
|
}
|