From cb51c46f639b0aaa02ad4962a2a76cf6d31b9b5c Mon Sep 17 00:00:00 2001 From: Ryahn Date: Mon, 27 Jan 2025 15:59:36 -0500 Subject: [PATCH] Lots of stuff again --- app.js | 35 +++++++ package-lock.json | 21 +++- package.json | 4 +- src/db/models/Base.js | 21 +++- src/db/models/Domain.js | 4 - src/db/models/TempEmail.js | 9 ++ src/db/seeds/admin_user.js | 2 +- src/email_server/services/MessageService.js | 11 +- src/routes/admin.js | 105 ++++++++++++++++++++ src/routes/email.js | 72 ++++++++++++++ src/routes/messages.js | 23 +++++ src/routes/test.js | 11 ++ test.js | 30 ++++++ 13 files changed, 337 insertions(+), 11 deletions(-) create mode 100644 src/routes/admin.js create mode 100644 src/routes/email.js create mode 100644 src/routes/messages.js create mode 100644 src/routes/test.js create mode 100644 test.js diff --git a/app.js b/app.js index e69de29..f1cc46c 100644 --- a/app.js +++ b/app.js @@ -0,0 +1,35 @@ +const express = require('express'); +const { Model } = require('objection'); +const Knex = require('knex'); +const cron = require('node-cron'); +const knexConfig = require('./src/config/database'); +const MessageService = require('./src/email_server/services/MessageService'); + +// Initialize Knex +const knex = Knex(knexConfig.development); +Model.knex(knex); + +const app = express(); +app.use(express.json()); + +// Routes +app.use('/auth', require('./src/routes/auth')); +app.use('/admin', require('./src/routes/admin')); +app.use('/messages', require('./src/routes/messages')); +app.use('/email', require('./src/routes/email')); +app.use('/test', require('./src/routes/test')); + +// Schedule cleanup job +cron.schedule('0 0 * * *', async () => { + try { + await MessageService.cleanup(); + console.log('Daily cleanup completed'); + } catch (error) { + console.error('Cleanup failed:', error); + } +}); + +const PORT = 3000; +app.listen(PORT, () => { + console.log(`API server running on port ${PORT}`); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cec76a0..9c47017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,11 @@ "haraka": "^0.0.33", "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", + "moniker": "^0.1.2", "mysql2": "^3.12.0", "node-cron": "^3.0.3", - "objection": "^3.1.5" + "objection": "^3.1.5", + "unique-names-generator": "^4.7.1" } }, "node_modules/@mapbox/node-pre-gyp": { @@ -1296,6 +1298,14 @@ "node": ">=10" } }, + "node_modules/moniker": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/moniker/-/moniker-0.1.2.tgz", + "integrity": "sha512-Uj9iV0QYr6281G+o0TvqhKwHHWB2Q/qUTT4LPQ3qDGc0r8cbMuqQjRXPZuVZ+gcL7APx+iQgE8lcfWPrj1LsLA==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1963,6 +1973,15 @@ "node": ">= 0.6" } }, + "node_modules/unique-names-generator": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz", + "integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index f456470..2d713e4 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,10 @@ "haraka": "^0.0.33", "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", + "moniker": "^0.1.2", "mysql2": "^3.12.0", "node-cron": "^3.0.3", - "objection": "^3.1.5" + "objection": "^3.1.5", + "unique-names-generator": "^4.7.1" } } diff --git a/src/db/models/Base.js b/src/db/models/Base.js index b7a060f..474c8bc 100644 --- a/src/db/models/Base.js +++ b/src/db/models/Base.js @@ -6,12 +6,27 @@ class BaseModel extends Model { } $beforeInsert() { - this.created_at = new Date().toISOString(); - this.updated_at = new Date().toISOString(); + this.created_at = this.formatDateForMySQL(new Date()); + this.updated_at = this.formatDateForMySQL(new Date()); + + if (this.expires_at) { + this.expires_at = this.formatDateForMySQL(new Date(this.expires_at)); + } } $beforeUpdate() { - this.updated_at = new Date().toISOString(); + this.updated_at = this.formatDateForMySQL(new Date()); + } + + $afterFind() { + // Format expires_at after fetching from database + if (this.expires_at) { + this.expires_at = this.formatDateForMySQL(new Date(this.expires_at)); + } + } + + formatDateForMySQL(date) { + return date.toISOString().slice(0, 19).replace('T', ' '); } } diff --git a/src/db/models/Domain.js b/src/db/models/Domain.js index d4fbaba..dab4a02 100644 --- a/src/db/models/Domain.js +++ b/src/db/models/Domain.js @@ -4,10 +4,6 @@ class Domain extends BaseModel { static get tableName() { return 'domains'; } - - static get relationMappings() { - return {}; - } } module.exports = Domain; \ No newline at end of file diff --git a/src/db/models/TempEmail.js b/src/db/models/TempEmail.js index 269c522..8472fa5 100644 --- a/src/db/models/TempEmail.js +++ b/src/db/models/TempEmail.js @@ -7,6 +7,7 @@ class TempEmail extends BaseModel { static get relationMappings() { const Message = require('./Message'); + const User = require('./User'); return { messages: { relation: BaseModel.HasManyRelation, @@ -15,6 +16,14 @@ class TempEmail extends BaseModel { from: 'temp_emails.id', to: 'messages.temp_email_id' } + }, + user: { + relation: BaseModel.BelongsToOneRelation, + modelClass: User, + join: { + from: 'temp_emails.user_id', + to: 'users.id' + } } }; } diff --git a/src/db/seeds/admin_user.js b/src/db/seeds/admin_user.js index 092167c..37a81e8 100644 --- a/src/db/seeds/admin_user.js +++ b/src/db/seeds/admin_user.js @@ -25,7 +25,7 @@ exports.seed = async function(knex) { const token = jwt.sign( { id: user.id, email: user.email, is_admin: user.is_admin }, config.auth.jwtSecret, - { expiresIn: config.auth.tokenExpiry } + { expiresIn: '20y' } ); await knex('users').where('id', user.id).update({ api_key: token }); diff --git a/src/email_server/services/MessageService.js b/src/email_server/services/MessageService.js index 4e52835..4d44a74 100644 --- a/src/email_server/services/MessageService.js +++ b/src/email_server/services/MessageService.js @@ -48,7 +48,9 @@ class MessageService { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - config.storage.retention); - return Message.query().where("created_at", "<", cutoffDate).delete(); + const deletedMessages = await Message.query().where("created_at", "<", cutoffDate).delete(); + const deletedTempEmails = await TempEmail.query().where("created_at", "<", cutoffDate).delete(); + return { deletedMessages, deletedTempEmails }; } static async search(params) { @@ -59,12 +61,19 @@ class MessageService { if (params.temp_email) { query = query.where("temp_email.email", params.temp_email); } + + if (params.temp_email_id) { + query = query.where("temp_email.id", params.temp_email_id); + } + if (params.from) { query = query.where("from", "like", `%${params.from}%`); } + if (params.to) { query = query.where("to", "like", `%${params.to}%`); } + if (params.subject) { query = query.where("subject", "like", `%${params.subject}%`); } diff --git a/src/routes/admin.js b/src/routes/admin.js new file mode 100644 index 0000000..db59e76 --- /dev/null +++ b/src/routes/admin.js @@ -0,0 +1,105 @@ +const express = require('express'); +const router = express.Router(); +const { authenticateToken, requireAdmin } = require('../middleware/auth'); +const { Domain } = require('../db/models/Domain'); +const { User } = require('../db/models/User'); +const { TempEmail } = require('../db/models/TempEmail'); +const MessageService = require('../email_server/services/MessageService'); + +router.use(authenticateToken, requireAdmin); + +// Domain management +router.get('/domains', async (req, res) => { + try { + const domains = await Domain.query(); + res.json(domains); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.post('/domains', async (req, res) => { + try { + const domain = await Domain.query().insert(req.body); + res.json(domain); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.delete('/domains/:id', async (req, res) => { + try { + const deletedDomain = await Domain.query().deleteById(req.params.id); + res.json(deletedDomain); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.delete('/users/:id', async (req, res) => { + try { + const deletedUser = await User.query().deleteById(req.params.id); + res.json(deletedUser); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// User management +router.get('/users', async (req, res) => { + try { + const users = await User.query().select('id', 'email', 'is_admin', 'created_at'); + res.json(users); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.post('/users', async (req, res) => { + try { + const user = await User.query().insert(req.body); + res.json(user); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.get('/temp-emails', async (req, res) => { + try { + const tempEmails = await TempEmail.query().withGraphFetched('user'); + res.json(tempEmails); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.post('/temp-emails', async (req, res) => { + try { + const tempEmail = await TempEmail.query().insert(req.body); + res.json(tempEmail); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.delete('/temp-emails/:id', async (req, res) => { + try { + const deletedTempEmail = await TempEmail.query().deleteById(req.params.id); + const deletedMessages = await MessageService.query().where('temp_email_id', req.params.id).delete(); + res.json({ deletedTempEmail, deletedMessages }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Manual cleanup trigger +router.post('/cleanup', async (req, res) => { + try { + const deletedCount = await MessageService.cleanup(); + res.json({ deletedCount }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/email.js b/src/routes/email.js new file mode 100644 index 0000000..651780d --- /dev/null +++ b/src/routes/email.js @@ -0,0 +1,72 @@ +const express = require('express'); +const router = express.Router(); +const { authenticateToken } = require('../middleware/auth'); +const TempEmail = require('../db/models/TempEmail'); +router.use(authenticateToken); +const Domain = require('../db/models/Domain'); +const { uniqueNamesGenerator, adjectives, animals, colors, names, languages, starWars, countries } = require('unique-names-generator'); + +function generateUniqueName() { + const dictionaries = [adjectives, animals, colors, names, languages, starWars, countries]; + + const randomIndex1 = Math.floor(Math.random() * dictionaries.length); + let randomIndex2 = Math.floor(Math.random() * dictionaries.length); + + while (randomIndex2 === randomIndex1) { + randomIndex2 = Math.floor(Math.random() * dictionaries.length); + } + + const randomName = uniqueNamesGenerator({ + dictionaries: [dictionaries[randomIndex1], dictionaries[randomIndex2]], + separator: '-', + style: 'lowerCase' + }); + + const formattedName = randomName + .replace(/[^a-zA-Z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-+|-+$/g, ''); + + const randomNumber = Math.floor(Math.random() * 999999) + 1; + + return `${formattedName}${randomNumber}`; +} + +router.post('/', async (req, res) => { + try { + const randomDomain = await Domain.query() + .where('active', true) + .orderByRaw('RAND()') + .first(); + + const emailName = req.body.name || generateUniqueName(); + const randomDomainName = `${emailName}@${randomDomain.name}`; + + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() + 14); + + const tempEmail = await TempEmail.query().insert({ + email: randomDomainName, + user_id: req.user.id, + expires_at: cutoffDate + }); + + res.json(tempEmail); + } catch (error) { + console.error('Error creating temp email:', error); + res.status(500).json({ error: error.message }); + } +}); + +router.get('/', async (req, res) => { + const tempEmails = await TempEmail.query().where('user_id', req.user.id); + res.json(tempEmails); +}); + +router.delete('/:id', async (req, res) => { + const tempEmail = await TempEmail.query().where('id', req.params.id).andWhere('user_id', req.user.id).delete(); + res.json(tempEmail); +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/messages.js b/src/routes/messages.js new file mode 100644 index 0000000..143d4e6 --- /dev/null +++ b/src/routes/messages.js @@ -0,0 +1,23 @@ +const express = require('express'); +const router = express.Router(); +const { authenticateToken } = require('../middleware/auth'); +const MessageService = require('../email_server/services/MessageService'); +const { Message } = require('../db/models/Message'); + +router.use(authenticateToken); + +router.get('/search', async (req, res) => { + try { + const messages = await MessageService.search(req.query); + res.json(messages); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.get('/:id', async (req, res) => { + const message = await Message.query().where('id', req.params.id).withGraphFetched('temp_email').first(); + res.json(message); +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/test.js b/src/routes/test.js new file mode 100644 index 0000000..b6f7129 --- /dev/null +++ b/src/routes/test.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const { authenticateToken } = require('../middleware/auth'); + +router.use(authenticateToken); + +router.get('/', (req, res) => { + res.json({ user: req.user }); +}); + +module.exports = router; \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..c62c0d5 --- /dev/null +++ b/test.js @@ -0,0 +1,30 @@ +const { uniqueNamesGenerator, adjectives, animals, colors, names, languages, starWars, countries } = require('unique-names-generator'); + +function generateUniqueName() { + const dictionaries = [adjectives, animals, colors, names, languages, starWars, countries]; + + const randomIndex1 = Math.floor(Math.random() * dictionaries.length); + let randomIndex2 = Math.floor(Math.random() * dictionaries.length); + + while (randomIndex2 === randomIndex1) { + randomIndex2 = Math.floor(Math.random() * dictionaries.length); + } + + const randomName = uniqueNamesGenerator({ + dictionaries: [dictionaries[randomIndex1], dictionaries[randomIndex2]], + separator: '-', + style: 'lowerCase' + }); + + const formattedName = randomName + .replace(/[^a-zA-Z0-9\s-]/g, '') // Remove special characters except spaces and hyphens + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen + .replace(/^-+|-+$/g, ''); // Remove hyphens from start and end + + const randomNumber = Math.floor(Math.random() * 999999) + 1; + + return `${formattedName}${randomNumber}`; +} + +console.log(generateUniqueName()); \ No newline at end of file