diff --git a/package.json b/package.json
index 8918efb2..7323b73b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@fossorial/pangolin",
- "version": "0.1.0",
+ "version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
@@ -60,6 +60,7 @@
"node-fetch": "3.3.2",
"nodemailer": "6.9.15",
"oslo": "1.2.1",
+ "qrcode.react": "4.2.0",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"react-hook-form": "7.53.0",
@@ -74,7 +75,6 @@
"zod-validation-error": "3.4.0"
},
"devDependencies": {
- "react-email": "3.0.2",
"@dotenvx/dotenvx": "1.14.2",
"@esbuild-plugins/tsconfig-paths": "0.1.2",
"@types/better-sqlite3": "7.6.11",
@@ -92,6 +92,7 @@
"esbuild": "0.20.1",
"esbuild-node-externals": "1.13.0",
"postcss": "^8",
+ "react-email": "3.0.2",
"tailwindcss": "^3.4.1",
"tsc-alias": "1.8.10",
"tsx": "4.19.1",
diff --git a/server/auth/2fa.ts b/server/auth/2fa.ts
index cb215ddd..2bf62c33 100644
--- a/server/auth/2fa.ts
+++ b/server/auth/2fa.ts
@@ -4,19 +4,22 @@ import { twoFactorBackupCodes } from "@server/db/schema";
import { eq } from "drizzle-orm";
import { decodeHex } from "oslo/encoding";
import { TOTPController } from "oslo/otp";
+import { verifyPassword } from "./password";
export async function verifyTotpCode(
code: string,
secret: string,
- userId: string,
+ userId: string
): Promise {
- if (code.length !== 6) {
+ // if code is digits only, it's totp
+ const isTotp = /^\d+$/.test(code);
+ if (!isTotp) {
const validBackupCode = await verifyBackUpCode(code, userId);
return validBackupCode;
} else {
const validOTP = await new TOTPController().verify(
code,
- decodeHex(secret),
+ decodeHex(secret)
);
return validOTP;
@@ -25,7 +28,7 @@ export async function verifyTotpCode(
export async function verifyBackUpCode(
code: string,
- userId: string,
+ userId: string
): Promise {
const allHashed = await db
.select()
@@ -38,12 +41,7 @@ export async function verifyBackUpCode(
let validId;
for (const hashedCode of allHashed) {
- const validCode = await verify(hashedCode.codeHash, code, {
- memoryCost: 19456,
- timeCost: 2,
- outputLen: 32,
- parallelism: 1,
- });
+ const validCode = await verifyPassword(code, hashedCode.codeHash);
if (validCode) {
validId = hashedCode.codeId;
}
diff --git a/server/auth/resourceOtp.ts b/server/auth/resourceOtp.ts
index 523b4011..a9de7499 100644
--- a/server/auth/resourceOtp.ts
+++ b/server/auth/resourceOtp.ts
@@ -8,6 +8,7 @@ import { sendEmail } from "@server/emails";
import ResourceOTPCode from "@server/emails/templates/ResourceOTPCode";
import config from "@server/config";
import { hash, verify } from "@node-rs/argon2";
+import { hashPassword } from "./password";
export async function sendResourceOtpEmail(
email: string,
@@ -47,12 +48,7 @@ export async function generateResourceOtpCode(
const otp = generateRandomString(8, alphabet("0-9", "A-Z", "a-z"));
- const otpHash = await hash(otp, {
- memoryCost: 19456,
- timeCost: 2,
- outputLen: 32,
- parallelism: 1,
- });
+ const otpHash = await hashPassword(otp);
await db.insert(resourceOtp).values({
resourceId,
@@ -84,12 +80,7 @@ export async function isValidOtp(
return false;
}
- const validCode = await verify(record[0].otpHash, otp, {
- memoryCost: 19456,
- timeCost: 2,
- outputLen: 32,
- parallelism: 1
- });
+ const validCode = await verifyPassword(otp, record[0].otpHash);
if (!validCode) {
return false;
}
diff --git a/server/config.ts b/server/config.ts
index 98f69928..8d78cbc0 100644
--- a/server/config.ts
+++ b/server/config.ts
@@ -132,6 +132,17 @@ if (!parsedConfig.success) {
throw new Error(`Invalid configuration file: ${errors}`);
}
+const packageJsonPath = path.join(__DIRNAME, "..", "package.json");
+let packageJson: any;
+if (fs.existsSync && fs.existsSync(packageJsonPath)) {
+ const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
+ packageJson = JSON.parse(packageJsonContent);
+
+ if (packageJson.version) {
+ process.env.APP_VERSION = packageJson.version;
+ }
+}
+
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
process.env.SERVER_EXTERNAL_PORT =
parsedConfig.data.server.external_port.toString();
diff --git a/server/db/schema.ts b/server/db/schema.ts
index 5ab72289..36b384b7 100644
--- a/server/db/schema.ts
+++ b/server/db/schema.ts
@@ -150,6 +150,7 @@ export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
export const passwordResetTokens = sqliteTable("passwordResetTokens", {
tokenId: integer("id").primaryKey({ autoIncrement: true }),
+ email: text("email").notNull(),
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
diff --git a/server/emails/templates/NotifyResetPassword.tsx b/server/emails/templates/NotifyResetPassword.tsx
new file mode 100644
index 00000000..05ff1f50
--- /dev/null
+++ b/server/emails/templates/NotifyResetPassword.tsx
@@ -0,0 +1,70 @@
+import {
+ Body,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Preview,
+ Section,
+ Text,
+ Tailwind
+} from "@react-email/components";
+import * as React from "react";
+
+interface Props {
+ email: string;
+}
+
+export const ConfirmPasswordReset = ({ email }: Props) => {
+ const previewText = `Your password has been reset`;
+
+ return (
+
+
+ {previewText}
+
+
+
+
+ Your password has been successfully reset
+
+
+ Hi {email || "there"},
+
+
+ This email confirms that your password has just been
+ reset. If you made this change, no further action is
+ required.
+
+
+
+ If you did not request this change, please
+ contact our support team immediately.
+
+
+
+ Thank you for keeping your account secure.
+
+
+ Best regards,
+
+ Fossorial
+
+
+
+
+
+ );
+};
+
+export default ConfirmPasswordReset;
diff --git a/server/emails/templates/ResetPasswordCode.tsx b/server/emails/templates/ResetPasswordCode.tsx
new file mode 100644
index 00000000..eb2ec7fb
--- /dev/null
+++ b/server/emails/templates/ResetPasswordCode.tsx
@@ -0,0 +1,75 @@
+import {
+ Body,
+ Container,
+ Head,
+ Heading,
+ Html,
+ Preview,
+ Section,
+ Text,
+ Tailwind
+} from "@react-email/components";
+import * as React from "react";
+
+interface Props {
+ email: string;
+ code: string;
+ link: string;
+}
+
+export const ResetPasswordCode = ({ email, code, link }: Props) => {
+ const previewText = `Reset your password, ${email}`;
+
+ return (
+
+
+ {previewText}
+
+
+
+
+ You've requested to reset your password
+
+
+ Hi {email || "there"},
+
+
+ You’ve requested to reset your password. Please{" "}
+
+ click here
+ {" "}
+ and follow the instructions to reset your password,
+ or manually enter the following code:
+
+
+
+ If you didn’t request this, you can safely ignore
+ this email.
+
+
+ Best regards,
+
+ Fossorial
+
+
+
+
+
+ );
+};
+
+export default ResetPasswordCode;
diff --git a/server/emails/templates/ResourceOTPCode.tsx b/server/emails/templates/ResourceOTPCode.tsx
index 20356160..7744400c 100644
--- a/server/emails/templates/ResourceOTPCode.tsx
+++ b/server/emails/templates/ResourceOTPCode.tsx
@@ -61,6 +61,12 @@ export const ResourceOTPCode = ({
{otp}
+
+
+ Best regards,
+
+ Fossorial
+
diff --git a/server/emails/templates/SendInviteLink.tsx b/server/emails/templates/SendInviteLink.tsx
index 9612f5d9..d7a4228d 100644
--- a/server/emails/templates/SendInviteLink.tsx
+++ b/server/emails/templates/SendInviteLink.tsx
@@ -8,7 +8,7 @@ import {
Section,
Text,
Tailwind,
- Button,
+ Button
} from "@react-email/components";
import * as React from "react";
@@ -25,7 +25,7 @@ export const SendInviteLink = ({
inviteLink,
orgName,
inviterName,
- expiresInDays,
+ expiresInDays
}: SendInviteLinkProps) => {
const previewText = `${inviterName} invited to join ${orgName}`;
@@ -33,15 +33,17 @@ export const SendInviteLink = ({