@appconda/nextjs 1.0.87 → 1.0.89
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/crypto.d.ts +23 -0
- package/dist/lib/crypto.js +76 -0
- package/dist/lib/env.js +11 -3
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/jwt.d.ts +12 -0
- package/dist/lib/jwt.js +102 -0
- package/package.json +2 -1
- package/src/index.ts +1 -0
- package/src/lib/crypto.ts +103 -0
- package/src/lib/env.ts +11 -3
- package/src/lib/index.ts +2 -0
- package/src/lib/jwt.ts +113 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
/**
|
2
|
+
*
|
3
|
+
* @param text Value to be encrypted
|
4
|
+
* @param key Key used to encrypt value must be 32 bytes for AES256 encryption algorithm
|
5
|
+
*
|
6
|
+
* @returns Encrypted value using key
|
7
|
+
*/
|
8
|
+
export declare const symmetricEncrypt: (text: string, key: string) => string;
|
9
|
+
/**
|
10
|
+
*
|
11
|
+
* @param text Value to decrypt
|
12
|
+
* @param key Key used to decrypt value must be 32 bytes for AES256 encryption algorithm
|
13
|
+
*/
|
14
|
+
export declare const symmetricDecrypt: (text: string, key: string) => string;
|
15
|
+
export declare const getHash: (key: string) => string;
|
16
|
+
export declare const encryptAES128: (encryptionKey: string, data: string) => string;
|
17
|
+
export declare const decryptAES128: (encryptionKey: string, data: string) => string;
|
18
|
+
export declare const generateLocalSignedUrl: (fileName: string, environmentId: string, fileType: string) => {
|
19
|
+
signature: string;
|
20
|
+
uuid: string;
|
21
|
+
timestamp: number;
|
22
|
+
};
|
23
|
+
export declare const validateLocalSignedUrl: (uuid: string, fileName: string, environmentId: string, fileType: string, timestamp: number, signature: string, secret: string) => boolean;
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import crypto from "crypto";
|
2
|
+
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from "crypto";
|
3
|
+
import { getEnv } from "./env";
|
4
|
+
const ALGORITHM = "aes256";
|
5
|
+
const INPUT_ENCODING = "utf8";
|
6
|
+
const OUTPUT_ENCODING = "hex";
|
7
|
+
const BUFFER_ENCODING = getEnv().ENCRYPTION_KEY.length === 32 ? "latin1" : "hex";
|
8
|
+
const IV_LENGTH = 16; // AES blocksize
|
9
|
+
/**
|
10
|
+
*
|
11
|
+
* @param text Value to be encrypted
|
12
|
+
* @param key Key used to encrypt value must be 32 bytes for AES256 encryption algorithm
|
13
|
+
*
|
14
|
+
* @returns Encrypted value using key
|
15
|
+
*/
|
16
|
+
export const symmetricEncrypt = (text, key) => {
|
17
|
+
const _key = Buffer.from(key, BUFFER_ENCODING);
|
18
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
19
|
+
// @ts-ignore -- the package needs to be built
|
20
|
+
const cipher = crypto.createCipheriv(ALGORITHM, _key, iv);
|
21
|
+
let ciphered = cipher.update(text, INPUT_ENCODING, OUTPUT_ENCODING);
|
22
|
+
ciphered += cipher.final(OUTPUT_ENCODING);
|
23
|
+
const ciphertext = iv.toString(OUTPUT_ENCODING) + ":" + ciphered;
|
24
|
+
return ciphertext;
|
25
|
+
};
|
26
|
+
/**
|
27
|
+
*
|
28
|
+
* @param text Value to decrypt
|
29
|
+
* @param key Key used to decrypt value must be 32 bytes for AES256 encryption algorithm
|
30
|
+
*/
|
31
|
+
export const symmetricDecrypt = (text, key) => {
|
32
|
+
const _key = Buffer.from(key, BUFFER_ENCODING);
|
33
|
+
const components = text.split(":");
|
34
|
+
const iv_from_ciphertext = Buffer.from(components.shift() || "", OUTPUT_ENCODING);
|
35
|
+
// @ts-ignore -- the package needs to be built
|
36
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, _key, iv_from_ciphertext);
|
37
|
+
let deciphered = decipher.update(components.join(":"), OUTPUT_ENCODING, INPUT_ENCODING);
|
38
|
+
deciphered += decipher.final(INPUT_ENCODING);
|
39
|
+
return deciphered;
|
40
|
+
};
|
41
|
+
export const getHash = (key) => createHash("sha256").update(key).digest("hex");
|
42
|
+
// create an aes128 encryption function
|
43
|
+
export const encryptAES128 = (encryptionKey, data) => {
|
44
|
+
// @ts-ignore -- the package needs to be built
|
45
|
+
const cipher = createCipheriv("aes-128-ecb", Buffer.from(encryptionKey, "base64"), "");
|
46
|
+
let encrypted = cipher.update(data, "utf-8", "hex");
|
47
|
+
encrypted += cipher.final("hex");
|
48
|
+
return encrypted;
|
49
|
+
};
|
50
|
+
// create an aes128 decryption function
|
51
|
+
export const decryptAES128 = (encryptionKey, data) => {
|
52
|
+
// @ts-ignore -- the package needs to be built
|
53
|
+
const cipher = createDecipheriv("aes-128-ecb", Buffer.from(encryptionKey, "base64"), "");
|
54
|
+
let decrypted = cipher.update(data, "hex", "utf-8");
|
55
|
+
decrypted += cipher.final("utf-8");
|
56
|
+
return decrypted;
|
57
|
+
};
|
58
|
+
export const generateLocalSignedUrl = (fileName, environmentId, fileType) => {
|
59
|
+
const uuid = randomBytes(16).toString("hex");
|
60
|
+
const timestamp = Date.now();
|
61
|
+
const data = `${uuid}:${fileName}:${environmentId}:${fileType}:${timestamp}`;
|
62
|
+
const signature = createHmac("sha256", getEnv().ENCRYPTION_KEY).update(data).digest("hex");
|
63
|
+
return { signature, uuid, timestamp };
|
64
|
+
};
|
65
|
+
export const validateLocalSignedUrl = (uuid, fileName, environmentId, fileType, timestamp, signature, secret) => {
|
66
|
+
const data = `${uuid}:${fileName}:${environmentId}:${fileType}:${timestamp}`;
|
67
|
+
const expectedSignature = createHmac("sha256", secret).update(data).digest("hex");
|
68
|
+
if (expectedSignature !== signature) {
|
69
|
+
return false;
|
70
|
+
}
|
71
|
+
// valid for 5 minutes
|
72
|
+
if (Date.now() - timestamp > 1000 * 60 * 5) {
|
73
|
+
return false;
|
74
|
+
}
|
75
|
+
return true;
|
76
|
+
};
|
package/dist/lib/env.js
CHANGED
@@ -14,6 +14,10 @@ export const getEnv = (() => {
|
|
14
14
|
APPCONDA_CLIENT_ENDPOINT: z.string(),
|
15
15
|
_SERVICE_TOKEN: z.string(),
|
16
16
|
ENTERPRISE_LICENSE_KEY: z.string(),
|
17
|
+
NEXTAUTH_SECRET: z.string().min(1),
|
18
|
+
ENCRYPTION_KEY: z.string().length(64).or(z.string().length(32)),
|
19
|
+
CRON_SECRET: z.string().min(10),
|
20
|
+
_ADMIN_DOMAIN: z.string().optional(),
|
17
21
|
/* AI_AZURE_LLM_API_KEY: z.string().optional(),
|
18
22
|
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: z.string().optional(),
|
19
23
|
AI_AZURE_LLM_DEPLOYMENT_ID: z.string().optional(),
|
@@ -33,7 +37,7 @@ export const getEnv = (() => {
|
|
33
37
|
E2E_TESTING: z.enum(["1", "0"]).optional(),
|
34
38
|
EMAIL_AUTH_DISABLED: z.enum(["1", "0"]).optional(),
|
35
39
|
EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
|
36
|
-
|
40
|
+
|
37
41
|
ENTERPRISE_LICENSE_KEY: z.string().optional(),
|
38
42
|
FORMBRICKS_ENCRYPTION_KEY: z.string().length(24).or(z.string().length(0)).optional(),
|
39
43
|
GITHUB_ID: z.string().optional(),
|
@@ -55,7 +59,7 @@ export const getEnv = (() => {
|
|
55
59
|
INTERCOM_SECRET_KEY: z.string().optional(),
|
56
60
|
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
57
61
|
MAIL_FROM: z.string().email().optional(),
|
58
|
-
|
62
|
+
|
59
63
|
NOTION_OAUTH_CLIENT_ID: z.string().optional(),
|
60
64
|
NOTION_OAUTH_CLIENT_SECRET: z.string().optional(),
|
61
65
|
OIDC_CLIENT_ID: z.string().optional(),
|
@@ -101,7 +105,7 @@ export const getEnv = (() => {
|
|
101
105
|
TURNSTILE_SECRET_KEY: z.string().optional(),
|
102
106
|
UPLOADS_DIR: z.string().min(1).optional(),
|
103
107
|
VERCEL_URL: z.string().optional(),
|
104
|
-
|
108
|
+
|
105
109
|
ADMIN_URL: z.string().url().optional(),
|
106
110
|
UNSPLASH_ACCESS_KEY: z.string().optional(),
|
107
111
|
LANGFUSE_SECRET_KEY: z.string().optional(),
|
@@ -119,6 +123,10 @@ export const getEnv = (() => {
|
|
119
123
|
APPCONDA_CLIENT_ENDPOINT: process.env.APPCONDA_CLIENT_ENDPOINT,
|
120
124
|
_SERVICE_TOKEN: process.env._SERVICE_TOKEN,
|
121
125
|
ENTERPRISE_LICENSE_KEY: process.env.ENTERPRISE_LICENSE_KEY,
|
126
|
+
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
127
|
+
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
128
|
+
CRON_SECRET: process.env.CRON_SECRET,
|
129
|
+
_ADMIN_DOMAIN: process.env._ADMIN_DOMAIN,
|
122
130
|
},
|
123
131
|
});
|
124
132
|
console.log(env);
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { JwtPayload } from "jsonwebtoken";
|
2
|
+
export declare const createToken: (userId: string, userEmail: string, options?: {}) => string;
|
3
|
+
export declare const createTokenForLinkSurvey: (surveyId: string, userEmail: string) => string;
|
4
|
+
export declare const createEmailToken: (email: string) => string;
|
5
|
+
export declare const getEmailFromEmailToken: (token: string) => string;
|
6
|
+
export declare const createInviteToken: (inviteId: string, email: string, options?: {}) => string;
|
7
|
+
export declare const verifyTokenForLinkSurvey: (token: string, surveyId: string) => string | null;
|
8
|
+
export declare const verifyToken: (token: string) => Promise<JwtPayload>;
|
9
|
+
export declare const verifyInviteToken: (token: string) => {
|
10
|
+
inviteId: string;
|
11
|
+
email: string;
|
12
|
+
};
|
package/dist/lib/jwt.js
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
import jwt from "jsonwebtoken";
|
2
|
+
import { symmetricDecrypt, symmetricEncrypt } from "./crypto";
|
3
|
+
import { getEnv } from "./env";
|
4
|
+
export const createToken = (userId, userEmail, options = {}) => {
|
5
|
+
const encryptedUserId = symmetricEncrypt(userId, getEnv().ENCRYPTION_KEY);
|
6
|
+
return jwt.sign({ id: encryptedUserId }, getEnv().NEXTAUTH_SECRET + userEmail, options);
|
7
|
+
};
|
8
|
+
export const createTokenForLinkSurvey = (surveyId, userEmail) => {
|
9
|
+
const encryptedEmail = symmetricEncrypt(userEmail, getEnv().ENCRYPTION_KEY);
|
10
|
+
return jwt.sign({ email: encryptedEmail }, getEnv().NEXTAUTH_SECRET + surveyId);
|
11
|
+
};
|
12
|
+
export const createEmailToken = (email) => {
|
13
|
+
const encryptedEmail = symmetricEncrypt(email, getEnv().ENCRYPTION_KEY);
|
14
|
+
return jwt.sign({ email: encryptedEmail }, getEnv().NEXTAUTH_SECRET);
|
15
|
+
};
|
16
|
+
export const getEmailFromEmailToken = (token) => {
|
17
|
+
const payload = jwt.verify(token, getEnv().NEXTAUTH_SECRET);
|
18
|
+
try {
|
19
|
+
// Try to decrypt first (for newer tokens)
|
20
|
+
const decryptedEmail = symmetricDecrypt(payload.email, getEnv().ENCRYPTION_KEY);
|
21
|
+
return decryptedEmail;
|
22
|
+
}
|
23
|
+
catch {
|
24
|
+
// If decryption fails, return the original email (for older tokens)
|
25
|
+
return payload.email;
|
26
|
+
}
|
27
|
+
};
|
28
|
+
export const createInviteToken = (inviteId, email, options = {}) => {
|
29
|
+
const encryptedInviteId = symmetricEncrypt(inviteId, getEnv().ENCRYPTION_KEY);
|
30
|
+
const encryptedEmail = symmetricEncrypt(email, getEnv().ENCRYPTION_KEY);
|
31
|
+
return jwt.sign({ inviteId: encryptedInviteId, email: encryptedEmail }, getEnv().NEXTAUTH_SECRET, options);
|
32
|
+
};
|
33
|
+
export const verifyTokenForLinkSurvey = (token, surveyId) => {
|
34
|
+
try {
|
35
|
+
const { email } = jwt.verify(token, getEnv().NEXTAUTH_SECRET + surveyId);
|
36
|
+
try {
|
37
|
+
// Try to decrypt first (for newer tokens)
|
38
|
+
const decryptedEmail = symmetricDecrypt(email, getEnv().ENCRYPTION_KEY);
|
39
|
+
return decryptedEmail;
|
40
|
+
}
|
41
|
+
catch {
|
42
|
+
// If decryption fails, return the original email (for older tokens)
|
43
|
+
return email;
|
44
|
+
}
|
45
|
+
}
|
46
|
+
catch (err) {
|
47
|
+
return null;
|
48
|
+
}
|
49
|
+
};
|
50
|
+
export const verifyToken = async (token) => {
|
51
|
+
// First decode to get the ID
|
52
|
+
const decoded = jwt.decode(token);
|
53
|
+
const payload = decoded;
|
54
|
+
const { id } = payload;
|
55
|
+
if (!id) {
|
56
|
+
throw new Error("Token missing required field: id");
|
57
|
+
}
|
58
|
+
// Try to decrypt the ID (for newer tokens), if it fails use the ID as-is (for older tokens)
|
59
|
+
let decryptedId;
|
60
|
+
try {
|
61
|
+
decryptedId = symmetricDecrypt(id, getEnv().ENCRYPTION_KEY);
|
62
|
+
}
|
63
|
+
catch {
|
64
|
+
decryptedId = id;
|
65
|
+
}
|
66
|
+
// If no email provided, look up the user
|
67
|
+
const foundUser = null; /* await prisma.user.findUnique({
|
68
|
+
where: { id: decryptedId },
|
69
|
+
}); */
|
70
|
+
if (!foundUser) {
|
71
|
+
throw new Error("User not found");
|
72
|
+
}
|
73
|
+
const userEmail = foundUser.email;
|
74
|
+
return { id: decryptedId, email: userEmail };
|
75
|
+
};
|
76
|
+
export const verifyInviteToken = (token) => {
|
77
|
+
try {
|
78
|
+
const decoded = jwt.decode(token);
|
79
|
+
const payload = decoded;
|
80
|
+
const { inviteId, email } = payload;
|
81
|
+
let decryptedInviteId;
|
82
|
+
let decryptedEmail;
|
83
|
+
try {
|
84
|
+
// Try to decrypt first (for newer tokens)
|
85
|
+
decryptedInviteId = symmetricDecrypt(inviteId, getEnv().ENCRYPTION_KEY);
|
86
|
+
decryptedEmail = symmetricDecrypt(email, getEnv().ENCRYPTION_KEY);
|
87
|
+
}
|
88
|
+
catch {
|
89
|
+
// If decryption fails, use original values (for older tokens)
|
90
|
+
decryptedInviteId = inviteId;
|
91
|
+
decryptedEmail = email;
|
92
|
+
}
|
93
|
+
return {
|
94
|
+
inviteId: decryptedInviteId,
|
95
|
+
email: decryptedEmail,
|
96
|
+
};
|
97
|
+
}
|
98
|
+
catch (error) {
|
99
|
+
console.error(`Error verifying invite token: ${error}`);
|
100
|
+
throw new Error("Invalid or expired invite token");
|
101
|
+
}
|
102
|
+
};
|
package/package.json
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
"name": "@appconda/nextjs",
|
3
3
|
"homepage": "https://appconda.io/support",
|
4
4
|
"description": "Appconda is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API",
|
5
|
-
"version": "1.0.
|
5
|
+
"version": "1.0.89",
|
6
6
|
"license": "BSD-3-Clause",
|
7
7
|
"main": "dist/index.js",
|
8
8
|
"types": "dist/index.d.ts",
|
@@ -24,6 +24,7 @@
|
|
24
24
|
"jsdelivr": "dist/iife/sdk.js",
|
25
25
|
"unpkg": "dist/iife/sdk.js",
|
26
26
|
"dependencies": {
|
27
|
+
"jsonwebtoken": "^9.0.2",
|
27
28
|
"next": "^15.2.3",
|
28
29
|
"next-auth": "^4.24.11",
|
29
30
|
"next-safe-action": "^7.10.4",
|
package/src/index.ts
CHANGED
@@ -0,0 +1,103 @@
|
|
1
|
+
import crypto from "crypto";
|
2
|
+
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from "crypto";
|
3
|
+
import { getEnv } from "./env";
|
4
|
+
|
5
|
+
|
6
|
+
const ALGORITHM = "aes256";
|
7
|
+
const INPUT_ENCODING = "utf8";
|
8
|
+
const OUTPUT_ENCODING = "hex";
|
9
|
+
const BUFFER_ENCODING = getEnv().ENCRYPTION_KEY!.length === 32 ? "latin1" : "hex";
|
10
|
+
const IV_LENGTH = 16; // AES blocksize
|
11
|
+
|
12
|
+
/**
|
13
|
+
*
|
14
|
+
* @param text Value to be encrypted
|
15
|
+
* @param key Key used to encrypt value must be 32 bytes for AES256 encryption algorithm
|
16
|
+
*
|
17
|
+
* @returns Encrypted value using key
|
18
|
+
*/
|
19
|
+
export const symmetricEncrypt = (text: string, key: string) => {
|
20
|
+
const _key = Buffer.from(key, BUFFER_ENCODING);
|
21
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
22
|
+
|
23
|
+
// @ts-ignore -- the package needs to be built
|
24
|
+
const cipher = crypto.createCipheriv(ALGORITHM, _key, iv);
|
25
|
+
let ciphered = cipher.update(text, INPUT_ENCODING, OUTPUT_ENCODING);
|
26
|
+
ciphered += cipher.final(OUTPUT_ENCODING);
|
27
|
+
const ciphertext = iv.toString(OUTPUT_ENCODING) + ":" + ciphered;
|
28
|
+
|
29
|
+
return ciphertext;
|
30
|
+
};
|
31
|
+
|
32
|
+
/**
|
33
|
+
*
|
34
|
+
* @param text Value to decrypt
|
35
|
+
* @param key Key used to decrypt value must be 32 bytes for AES256 encryption algorithm
|
36
|
+
*/
|
37
|
+
export const symmetricDecrypt = (text: string, key: string) => {
|
38
|
+
const _key = Buffer.from(key, BUFFER_ENCODING);
|
39
|
+
|
40
|
+
const components = text.split(":");
|
41
|
+
const iv_from_ciphertext = Buffer.from(components.shift() || "", OUTPUT_ENCODING);
|
42
|
+
// @ts-ignore -- the package needs to be built
|
43
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, _key, iv_from_ciphertext);
|
44
|
+
let deciphered = decipher.update(components.join(":"), OUTPUT_ENCODING, INPUT_ENCODING);
|
45
|
+
deciphered += decipher.final(INPUT_ENCODING);
|
46
|
+
|
47
|
+
return deciphered;
|
48
|
+
};
|
49
|
+
|
50
|
+
export const getHash = (key: string): string => createHash("sha256").update(key).digest("hex");
|
51
|
+
|
52
|
+
// create an aes128 encryption function
|
53
|
+
export const encryptAES128 = (encryptionKey: string, data: string): string => {
|
54
|
+
// @ts-ignore -- the package needs to be built
|
55
|
+
const cipher = createCipheriv("aes-128-ecb", Buffer.from(encryptionKey, "base64"), "");
|
56
|
+
let encrypted = cipher.update(data, "utf-8", "hex");
|
57
|
+
encrypted += cipher.final("hex");
|
58
|
+
return encrypted;
|
59
|
+
};
|
60
|
+
// create an aes128 decryption function
|
61
|
+
export const decryptAES128 = (encryptionKey: string, data: string): string => {
|
62
|
+
// @ts-ignore -- the package needs to be built
|
63
|
+
const cipher = createDecipheriv("aes-128-ecb", Buffer.from(encryptionKey, "base64"), "");
|
64
|
+
let decrypted = cipher.update(data, "hex", "utf-8");
|
65
|
+
decrypted += cipher.final("utf-8");
|
66
|
+
return decrypted;
|
67
|
+
};
|
68
|
+
|
69
|
+
export const generateLocalSignedUrl = (
|
70
|
+
fileName: string,
|
71
|
+
environmentId: string,
|
72
|
+
fileType: string
|
73
|
+
): { signature: string; uuid: string; timestamp: number } => {
|
74
|
+
const uuid = randomBytes(16).toString("hex");
|
75
|
+
const timestamp = Date.now();
|
76
|
+
const data = `${uuid}:${fileName}:${environmentId}:${fileType}:${timestamp}`;
|
77
|
+
const signature = createHmac("sha256", getEnv().ENCRYPTION_KEY!).update(data).digest("hex");
|
78
|
+
return { signature, uuid, timestamp };
|
79
|
+
};
|
80
|
+
|
81
|
+
export const validateLocalSignedUrl = (
|
82
|
+
uuid: string,
|
83
|
+
fileName: string,
|
84
|
+
environmentId: string,
|
85
|
+
fileType: string,
|
86
|
+
timestamp: number,
|
87
|
+
signature: string,
|
88
|
+
secret: string
|
89
|
+
): boolean => {
|
90
|
+
const data = `${uuid}:${fileName}:${environmentId}:${fileType}:${timestamp}`;
|
91
|
+
const expectedSignature = createHmac("sha256", secret).update(data).digest("hex");
|
92
|
+
|
93
|
+
if (expectedSignature !== signature) {
|
94
|
+
return false;
|
95
|
+
}
|
96
|
+
|
97
|
+
// valid for 5 minutes
|
98
|
+
if (Date.now() - timestamp > 1000 * 60 * 5) {
|
99
|
+
return false;
|
100
|
+
}
|
101
|
+
|
102
|
+
return true;
|
103
|
+
};
|
package/src/lib/env.ts
CHANGED
@@ -16,6 +16,10 @@ export const getEnv = (() => {
|
|
16
16
|
APPCONDA_CLIENT_ENDPOINT: z.string(),
|
17
17
|
_SERVICE_TOKEN: z.string(),
|
18
18
|
ENTERPRISE_LICENSE_KEY: z.string(),
|
19
|
+
NEXTAUTH_SECRET: z.string().min(1),
|
20
|
+
ENCRYPTION_KEY: z.string().length(64).or(z.string().length(32)),
|
21
|
+
CRON_SECRET: z.string().min(10),
|
22
|
+
_ADMIN_DOMAIN: z.string().optional(),
|
19
23
|
/* AI_AZURE_LLM_API_KEY: z.string().optional(),
|
20
24
|
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: z.string().optional(),
|
21
25
|
AI_AZURE_LLM_DEPLOYMENT_ID: z.string().optional(),
|
@@ -35,7 +39,7 @@ export const getEnv = (() => {
|
|
35
39
|
E2E_TESTING: z.enum(["1", "0"]).optional(),
|
36
40
|
EMAIL_AUTH_DISABLED: z.enum(["1", "0"]).optional(),
|
37
41
|
EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
|
38
|
-
|
42
|
+
|
39
43
|
ENTERPRISE_LICENSE_KEY: z.string().optional(),
|
40
44
|
FORMBRICKS_ENCRYPTION_KEY: z.string().length(24).or(z.string().length(0)).optional(),
|
41
45
|
GITHUB_ID: z.string().optional(),
|
@@ -57,7 +61,7 @@ export const getEnv = (() => {
|
|
57
61
|
INTERCOM_SECRET_KEY: z.string().optional(),
|
58
62
|
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
59
63
|
MAIL_FROM: z.string().email().optional(),
|
60
|
-
|
64
|
+
|
61
65
|
NOTION_OAUTH_CLIENT_ID: z.string().optional(),
|
62
66
|
NOTION_OAUTH_CLIENT_SECRET: z.string().optional(),
|
63
67
|
OIDC_CLIENT_ID: z.string().optional(),
|
@@ -103,7 +107,7 @@ export const getEnv = (() => {
|
|
103
107
|
TURNSTILE_SECRET_KEY: z.string().optional(),
|
104
108
|
UPLOADS_DIR: z.string().min(1).optional(),
|
105
109
|
VERCEL_URL: z.string().optional(),
|
106
|
-
|
110
|
+
|
107
111
|
ADMIN_URL: z.string().url().optional(),
|
108
112
|
UNSPLASH_ACCESS_KEY: z.string().optional(),
|
109
113
|
LANGFUSE_SECRET_KEY: z.string().optional(),
|
@@ -122,6 +126,10 @@ export const getEnv = (() => {
|
|
122
126
|
APPCONDA_CLIENT_ENDPOINT: process.env.APPCONDA_CLIENT_ENDPOINT,
|
123
127
|
_SERVICE_TOKEN: process.env._SERVICE_TOKEN,
|
124
128
|
ENTERPRISE_LICENSE_KEY: process.env.ENTERPRISE_LICENSE_KEY,
|
129
|
+
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
130
|
+
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
131
|
+
CRON_SECRET: process.env.CRON_SECRET,
|
132
|
+
_ADMIN_DOMAIN: process.env._ADMIN_DOMAIN,
|
125
133
|
},
|
126
134
|
});
|
127
135
|
console.log(env);
|
package/src/lib/index.ts
ADDED
package/src/lib/jwt.ts
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
import jwt, { JwtPayload } from "jsonwebtoken";
|
2
|
+
import { symmetricDecrypt, symmetricEncrypt } from "./crypto";
|
3
|
+
import { getEnv } from "./env";
|
4
|
+
|
5
|
+
export const createToken = (userId: string, userEmail: string, options = {}): string => {
|
6
|
+
const encryptedUserId = symmetricEncrypt(userId, getEnv().ENCRYPTION_KEY);
|
7
|
+
return jwt.sign({ id: encryptedUserId }, getEnv().NEXTAUTH_SECRET + userEmail, options);
|
8
|
+
};
|
9
|
+
export const createTokenForLinkSurvey = (surveyId: string, userEmail: string): string => {
|
10
|
+
const encryptedEmail = symmetricEncrypt(userEmail, getEnv().ENCRYPTION_KEY);
|
11
|
+
return jwt.sign({ email: encryptedEmail }, getEnv().NEXTAUTH_SECRET + surveyId);
|
12
|
+
};
|
13
|
+
|
14
|
+
export const createEmailToken = (email: string): string => {
|
15
|
+
const encryptedEmail = symmetricEncrypt(email, getEnv().ENCRYPTION_KEY);
|
16
|
+
return jwt.sign({ email: encryptedEmail }, getEnv().NEXTAUTH_SECRET);
|
17
|
+
};
|
18
|
+
|
19
|
+
export const getEmailFromEmailToken = (token: string): string => {
|
20
|
+
const payload = jwt.verify(token, getEnv().NEXTAUTH_SECRET) as JwtPayload;
|
21
|
+
try {
|
22
|
+
// Try to decrypt first (for newer tokens)
|
23
|
+
const decryptedEmail = symmetricDecrypt(payload.email, getEnv().ENCRYPTION_KEY);
|
24
|
+
return decryptedEmail;
|
25
|
+
} catch {
|
26
|
+
// If decryption fails, return the original email (for older tokens)
|
27
|
+
return payload.email;
|
28
|
+
}
|
29
|
+
};
|
30
|
+
|
31
|
+
export const createInviteToken = (inviteId: string, email: string, options = {}): string => {
|
32
|
+
const encryptedInviteId = symmetricEncrypt(inviteId, getEnv().ENCRYPTION_KEY);
|
33
|
+
const encryptedEmail = symmetricEncrypt(email, getEnv().ENCRYPTION_KEY);
|
34
|
+
return jwt.sign({ inviteId: encryptedInviteId, email: encryptedEmail }, getEnv().NEXTAUTH_SECRET, options);
|
35
|
+
};
|
36
|
+
|
37
|
+
export const verifyTokenForLinkSurvey = (token: string, surveyId: string): string | null => {
|
38
|
+
try {
|
39
|
+
const { email } = jwt.verify(token, getEnv().NEXTAUTH_SECRET + surveyId) as JwtPayload;
|
40
|
+
try {
|
41
|
+
// Try to decrypt first (for newer tokens)
|
42
|
+
const decryptedEmail = symmetricDecrypt(email, getEnv().ENCRYPTION_KEY);
|
43
|
+
return decryptedEmail;
|
44
|
+
} catch {
|
45
|
+
// If decryption fails, return the original email (for older tokens)
|
46
|
+
return email;
|
47
|
+
}
|
48
|
+
} catch (err) {
|
49
|
+
return null;
|
50
|
+
}
|
51
|
+
};
|
52
|
+
|
53
|
+
export const verifyToken = async (token: string): Promise<JwtPayload> => {
|
54
|
+
// First decode to get the ID
|
55
|
+
const decoded = jwt.decode(token);
|
56
|
+
const payload: JwtPayload = decoded as JwtPayload;
|
57
|
+
|
58
|
+
const { id } = payload;
|
59
|
+
if (!id) {
|
60
|
+
throw new Error("Token missing required field: id");
|
61
|
+
}
|
62
|
+
|
63
|
+
// Try to decrypt the ID (for newer tokens), if it fails use the ID as-is (for older tokens)
|
64
|
+
let decryptedId: string;
|
65
|
+
try {
|
66
|
+
decryptedId = symmetricDecrypt(id, getEnv().ENCRYPTION_KEY);
|
67
|
+
} catch {
|
68
|
+
decryptedId = id;
|
69
|
+
}
|
70
|
+
|
71
|
+
// If no email provided, look up the user
|
72
|
+
const foundUser = null;/* await prisma.user.findUnique({
|
73
|
+
where: { id: decryptedId },
|
74
|
+
}); */
|
75
|
+
|
76
|
+
if (!foundUser) {
|
77
|
+
throw new Error("User not found");
|
78
|
+
}
|
79
|
+
|
80
|
+
const userEmail = foundUser.email;
|
81
|
+
|
82
|
+
return { id: decryptedId, email: userEmail };
|
83
|
+
};
|
84
|
+
|
85
|
+
export const verifyInviteToken = (token: string): { inviteId: string; email: string } => {
|
86
|
+
try {
|
87
|
+
const decoded = jwt.decode(token);
|
88
|
+
const payload: JwtPayload = decoded as JwtPayload;
|
89
|
+
|
90
|
+
const { inviteId, email } = payload;
|
91
|
+
|
92
|
+
let decryptedInviteId: string;
|
93
|
+
let decryptedEmail: string;
|
94
|
+
|
95
|
+
try {
|
96
|
+
// Try to decrypt first (for newer tokens)
|
97
|
+
decryptedInviteId = symmetricDecrypt(inviteId, getEnv().ENCRYPTION_KEY);
|
98
|
+
decryptedEmail = symmetricDecrypt(email, getEnv().ENCRYPTION_KEY);
|
99
|
+
} catch {
|
100
|
+
// If decryption fails, use original values (for older tokens)
|
101
|
+
decryptedInviteId = inviteId;
|
102
|
+
decryptedEmail = email;
|
103
|
+
}
|
104
|
+
|
105
|
+
return {
|
106
|
+
inviteId: decryptedInviteId,
|
107
|
+
email: decryptedEmail,
|
108
|
+
};
|
109
|
+
} catch (error) {
|
110
|
+
console.error(`Error verifying invite token: ${error}`);
|
111
|
+
throw new Error("Invalid or expired invite token");
|
112
|
+
}
|
113
|
+
};
|