1
+ import { ExpressAuth , getSession } from "@auth/express" ;
2
+ import Credentials from "@auth/express/providers/credentials" ;
1
3
import Database from "better-sqlite3" ;
2
- import express , { urlencoded } from "express" ;
4
+ import crypto from "crypto" ;
5
+ import "dotenv/config" ;
6
+ import express , { json , urlencoded } from "express" ; // Import json middleware
3
7
import expressLayouts from "express-ejs-layouts" ;
4
8
import expressUploads from "express-fileupload" ;
5
9
import expressMethodOverride from "method-override" ;
@@ -10,48 +14,139 @@ const __dirname = import.meta.dirname;
10
14
const app = express ( ) ;
11
15
const port = 3000 ;
12
16
17
+ const secret = process . env . AUTH_SECRET ;
18
+
19
+ if ( ! secret ) {
20
+ console . error ( "AUTH_SECRET is not set! Authentication may not work correctly." ) ;
21
+ process . exit ( 1 ) ; // Exit if AUTH_SECRET is missing in production
22
+ }
23
+
24
+ // Database setup
25
+ const db = new Database ( "./data.db" ) ;
26
+
27
+ const schema = `
28
+ CREATE TABLE IF NOT EXISTS "templates" (
29
+ "id" TEXT PRIMARY KEY,
30
+ "name" TEXT NOT NULL,
31
+ "to" TEXT NOT NULL,
32
+ "cc" TEXT,
33
+ "bcc" TEXT,
34
+ "subject" TEXT NOT NULL,
35
+ "body" TEXT NOT NULL,
36
+ "fields" TEXT
37
+ );
38
+
39
+ CREATE TABLE IF NOT EXISTS "users" (
40
+ "id" TEXT PRIMARY KEY,
41
+ "email" TEXT NOT NULL UNIQUE,
42
+ "password" TEXT NOT NULL
43
+ );
44
+ ` ;
45
+
46
+ db . exec ( schema ) ;
47
+
13
48
// Middleware
14
49
app . use ( expressLayouts ) ;
15
50
app . use ( expressUploads ( ) ) ;
16
51
app . use ( expressMethodOverride ( "_method" ) ) ;
17
52
app . use ( urlencoded ( { extended : true } ) ) ;
53
+ app . use ( json ( ) ) ; // Add json middleware
18
54
app . use ( express . static ( "public" ) ) ;
19
55
20
- app . use ( ( req , res , next ) => {
21
- res . removeHeader ( "Content-Security-Policy" ) ;
56
+ /**
57
+ * @type {import("@auth/express").ExpressAuthConfig }
58
+ */
59
+ const authConfig = {
60
+ secret,
61
+ trustHost : true ,
62
+ pages : {
63
+ signIn : "/login" , // Custom sign-in page
64
+ } ,
65
+ providers : [
66
+ Credentials ( {
67
+ credentials : {
68
+ email : { label : "Email" , type : "text" } ,
69
+ password : { label : "Password" , type : "password" } ,
70
+ } ,
71
+ async authorize ( credentials , req ) {
72
+ if ( ! credentials ?. email || ! credentials ?. password ) {
73
+ return null ;
74
+ }
75
+ const user = getUser ( credentials . email ) ;
76
+
77
+ if ( ! user ) {
78
+ return null ;
79
+ }
80
+
81
+ const [ storedHash , salt ] = user . password . split ( ":" ) ;
82
+
83
+ if ( ! storedHash || ! salt ) {
84
+ console . error ( "Invalid password format in database." ) ;
85
+ return null ;
86
+ }
87
+
88
+ const isValid = verifyPassword ( credentials . password , salt , storedHash ) ;
89
+
90
+ if ( isValid ) {
91
+ // **IMPORTANT**: Return a user object that MUST contain `id` and `email`
92
+ return { id : user . id , email : user . email } ;
93
+ } else {
94
+ return null ;
95
+ }
96
+ } ,
97
+ } ) ,
98
+ ] ,
99
+ debug : true , // Enable debug logs for troubleshooting
100
+ } ;
101
+
102
+ const authSession = async ( req , res , next ) => {
103
+ res . locals . session = await getSession ( req , authConfig ) ;
22
104
next ( ) ;
23
- } ) ;
105
+ } ;
106
+
107
+ app . use ( "/auth/*" , ExpressAuth ( authConfig ) ) ;
108
+
109
+ app . use ( authSession ) ;
24
110
25
111
// Express Settings
26
112
app . set ( "view engine" , "ejs" ) ;
27
113
app . set ( "views" , join ( __dirname , "views" ) ) ;
28
114
app . set ( "layout" , "layouts/layout" ) ;
115
+ app . set ( "trust proxy" , true ) ;
29
116
30
- // Database setup
31
- const db = new Database ( "./templates.db" ) ;
117
+ // Helper functions
118
+ const saltAndHashPassword = ( password ) => {
119
+ const salt = crypto . randomBytes ( 16 ) . toString ( "hex" ) ;
120
+ const hash = crypto . pbkdf2Sync ( password , salt , 1000 , 64 , "sha512" ) . toString ( "hex" ) ;
121
+ return { salt, hash } ;
122
+ } ;
32
123
33
- const schema = `
34
- CREATE TABLE IF NOT EXISTS "templates" (
35
- "id" TEXT PRIMARY KEY,
36
- "name" TEXT NOT NULL,
37
- "to" TEXT NOT NULL,
38
- "cc" TEXT,
39
- "bcc" TEXT,
40
- "subject" TEXT NOT NULL,
41
- "body" TEXT NOT NULL,
42
- "fields" TEXT
43
- );
44
-
45
- CREATE TABLE IF NOT EXISTS users (
46
- id TEXT PRIMARY KEY,
47
- username TEXT NOT NULL,
48
- password TEXT NOT NULL
49
- );
50
- ` ;
124
+ const verifyPassword = ( password , salt , storedHash ) => {
125
+ const hash = crypto . pbkdf2Sync ( password , salt , 1000 , 64 , "sha512" ) . toString ( "hex" ) ;
126
+ return storedHash === hash ;
127
+ } ;
51
128
52
- db . exec ( schema ) ;
129
+ const nanoId = ( length = 5 ) => {
130
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ;
131
+ let str = "" ;
132
+ for ( let i = 0 ; i < length ; i ++ ) {
133
+ const randomIndex = Math . floor ( Math . random ( ) * chars . length ) ;
134
+ str += chars [ randomIndex ] ;
135
+ }
136
+ return str ;
137
+ } ;
53
138
54
139
// Database helper methods
140
+ const getUser = ( email ) => {
141
+ return db . prepare ( 'SELECT * FROM "users" WHERE "email" = ?' ) . get ( email ) ;
142
+ } ;
143
+
144
+ const addUser = ( email , password ) => {
145
+ const { salt, hash } = saltAndHashPassword ( password ) ;
146
+ const id = nanoId ( ) ;
147
+ db . prepare ( 'INSERT INTO "users" ("id", "email", "password") VALUES (?, ?, ?)' ) . run ( id , email , `${ hash } :${ salt } ` ) ;
148
+ } ;
149
+
55
150
const getTemplates = ( ) => {
56
151
const templates = db . prepare ( 'SELECT * FROM "templates"' ) . all ( ) ;
57
152
return templates . map ( ( template ) => ( {
@@ -88,7 +183,50 @@ const deleteTemplate = (id) => {
88
183
db . prepare ( 'DELETE FROM "templates" WHERE "id" = ?' ) . run ( id ) ;
89
184
} ;
90
185
186
+ // Authentication middleware
187
+ const authenticatedUser = async ( req , res , next ) => {
188
+ const session = res . locals . session ?? ( await getSession ( req , authConfig ) ) ;
189
+ if ( ! session ?. user ) {
190
+ res . redirect ( "/login" ) ;
191
+ } else {
192
+ next ( ) ;
193
+ }
194
+ } ;
195
+
91
196
// Routes
197
+ app . get ( "/login" , ( req , res ) => {
198
+ if ( res . locals . session ?. user ) {
199
+ return res . redirect ( "/" ) ;
200
+ }
201
+ res . render ( "login" ) ;
202
+ } ) ;
203
+
204
+ app . get ( "/signup" , ( req , res ) => {
205
+ if ( res . locals . session ?. user ) {
206
+ return res . redirect ( "/" ) ;
207
+ }
208
+ res . render ( "signup" ) ;
209
+ } ) ;
210
+
211
+ app . post ( "/signup" , ( req , res ) => {
212
+ const { email, password } = req . body ;
213
+
214
+ try {
215
+ addUser ( email , password ) ;
216
+ res . redirect ( "/login" ) ;
217
+ } catch ( error ) {
218
+ console . log ( { error } ) ;
219
+ res . status ( 400 ) . render ( "signup" , { error : "Error registering user. Email may already exist." } ) ;
220
+ }
221
+ } ) ;
222
+
223
+ app . get ( "/logout" , ( req , res ) => {
224
+ res . redirect ( "/auth/signout" ) ;
225
+ } ) ;
226
+
227
+ // Protected routes
228
+ app . use ( authenticatedUser ) ;
229
+
92
230
app . get ( "/" , async ( _ , res ) => {
93
231
const templates = getTemplates ( ) ;
94
232
res . render ( "home" , { templates, query : "" } ) ;
@@ -167,7 +305,6 @@ app.post("/generate", async (req, res) => {
167
305
const { to, cc, bcc, ...fields } = req . body ;
168
306
169
307
const template = getTemplate ( templateId ) ;
170
-
171
308
if ( ! template ) {
172
309
return res . status ( 404 ) . send ( "Template not found" ) ;
173
310
}
@@ -187,7 +324,7 @@ app.post("/generate", async (req, res) => {
187
324
res . redirect ( mailtoLink ) ;
188
325
} ) ;
189
326
190
- // Helper functions
327
+ // Additional helper functions
191
328
const extractDynamicFields = ( content ) => {
192
329
content = content . trim ( ) ;
193
330
const regex = / \{ @ ( .* ?) : ( .* ?) \} / gm;
@@ -200,7 +337,6 @@ const extractDynamicFields = (content) => {
200
337
const [ label , value = "" ] = body . split ( "|" ) ;
201
338
fields . push ( { id : match , type : type . replace ( "@" , "" ) , label : label . trim ( ) , value : value . trim ( ) } ) ;
202
339
}
203
-
204
340
return fields ;
205
341
} ;
206
342
@@ -224,16 +360,6 @@ const sanitizeJSON = (unsanitized) => {
224
360
. replace ( / \& / g, "\\&" ) ;
225
361
} ;
226
362
227
- const nanoId = ( length = 5 ) => {
228
- const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ;
229
- let str = "" ;
230
- for ( let i = 0 ; i < length ; i ++ ) {
231
- const randomIndex = Math . floor ( Math . random ( ) * chars . length ) ;
232
- str += chars [ randomIndex ] ;
233
- }
234
- return str ;
235
- } ;
236
-
237
363
app . listen ( port , ( ) => {
238
364
console . log ( `App listening at http://localhost:${ port } ` ) ;
239
365
} ) ;
0 commit comments