-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add user signup email * login emails * slack invite, and email team
- Loading branch information
1 parent
ae1229e
commit c67bfb0
Showing
7 changed files
with
295 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ | |
"jsonwebtoken": "^9.0.2", | ||
"moment": "^2.30.1", | ||
"node-statsd": "^0.1.1", | ||
"postmark": "^4.0.5", | ||
"request-ip": "^3.3.0", | ||
"response-time": "^2.3.3", | ||
"ts-node": "^10.9.2", | ||
|
@@ -577,6 +578,8 @@ | |
|
||
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="], | ||
|
||
"postmark": ["[email protected]", "", { "dependencies": { "axios": "^1.7.4" } }, "sha512-nerZdd3TwOH4CgGboZnlUM/q7oZk0EqpZgJL+Y3Nup8kHeaukxouQ6JcFF3EJEijc4QbuNv1TefGhboAKtf/SQ=="], | ||
|
||
"prisma": ["[email protected]", "", { "dependencies": { "@prisma/engines": "6.4.1", "esbuild": ">=0.12 <1", "esbuild-register": "3.6.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-q2uJkgXnua/jj66mk6P9bX/zgYJFI/jn4Yp0aS6SPRrjH/n6VyOV7RDe1vHD0DX8Aanx4MvgmUPPoYnR6MJnPg=="], | ||
|
||
"proxy-addr": ["[email protected]", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
prisma/migrations/20250306234824_enhanced_login_and_security_details/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
-- CreateTable | ||
CREATE TABLE "LoginAttempt" ( | ||
"id" SERIAL NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
"userId" INTEGER NOT NULL, | ||
"ipAddress" TEXT NOT NULL, | ||
"userAgent" TEXT, | ||
"successful" BOOLEAN NOT NULL DEFAULT true, | ||
"ipInfo" TEXT, | ||
"country" TEXT, | ||
"city" TEXT, | ||
"region" TEXT, | ||
"isp" TEXT, | ||
"organization" TEXT, | ||
"deviceType" TEXT, | ||
"browser" TEXT, | ||
"loginMethod" TEXT DEFAULT 'password', | ||
|
||
CONSTRAINT "LoginAttempt_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "LoginAttempt_userId_idx" ON "LoginAttempt"("userId"); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "LoginAttempt_ipAddress_idx" ON "LoginAttempt"("ipAddress"); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "LoginAttempt_createdAt_idx" ON "LoginAttempt"("createdAt"); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "LoginAttempt" ADD CONSTRAINT "LoginAttempt_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
export async function inviteToSlack(email: string) { | ||
const xoxc = process.env.SLACK_BROWSER_TOKEN!; | ||
const xoxd = process.env.SLACK_COOKIE!; | ||
|
||
const payload = `------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="token" | ||
${xoxc} | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="invites" | ||
[{"email":"${email}","type":"regular","mode":"manual"}] | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="team_id" | ||
T07J9QT2P8R | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="restricted" | ||
false | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="ultra_restricted" | ||
false | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="campaign" | ||
team_menu | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="_x_reason" | ||
submit-invite-to-workspace-invites | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="_x_mode" | ||
online | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="_x_sonic" | ||
true | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A | ||
Content-Disposition: form-data; name="_x_app_name" | ||
client | ||
------WebKitFormBoundaryIk2wxHLsyrGnUA6A--`; | ||
|
||
let result; | ||
|
||
try { | ||
const data = await fetch( | ||
"https://phishdirectory.slack.com/api/users.admin.inviteBulk", | ||
{ | ||
headers: { | ||
"content-type": | ||
"multipart/form-data; boundary=----WebKitFormBoundaryIk2wxHLsyrGnUA6A", | ||
}, | ||
referrerPolicy: "no-referrer", | ||
body: payload, | ||
method: "POST", | ||
mode: "cors", | ||
credentials: "include", | ||
} | ||
); | ||
|
||
result = data.ok; | ||
} catch (e) { | ||
console.error(e); | ||
result = false; | ||
} | ||
|
||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,14 @@ | ||
import bcrypt from "bcrypt"; | ||
import express from "express"; | ||
|
||
import { inviteToSlack } from "../func/slackInvite"; | ||
import { logRequest } from "../middleware/logRequest"; | ||
import { | ||
authenticateToken, | ||
generateAccessToken, | ||
getUserInfo, | ||
} from "../utils/jwt"; | ||
import postmark from "../utils/postmark"; | ||
import { prisma } from "../utils/prisma"; | ||
import { userNeedsExtendedData } from "../utils/userNeedsExtendedData"; | ||
|
||
|
@@ -39,9 +41,7 @@ let saltRounds = 10; | |
*/ | ||
router.post("/signup", async (req, res) => { | ||
// metrics.increment("endpoint.user.signup"); | ||
|
||
const body = req.body; | ||
|
||
const { name, email, password } = body; | ||
|
||
if (!name || !email || !password) { | ||
|
@@ -66,19 +66,52 @@ router.post("/signup", async (req, res) => { | |
const salt = bcrypt.genSaltSync(saltRounds); | ||
let passHash = await bcrypt.hash(password, salt); | ||
|
||
// Create the user | ||
const newUser = await prisma.user.create({ | ||
data: { | ||
name: name, | ||
email: email, | ||
password: passHash, | ||
}, | ||
}); | ||
try { | ||
// Create the user | ||
const newUser = await prisma.user.create({ | ||
data: { | ||
name: name, | ||
email: email, | ||
password: passHash, | ||
}, | ||
}); | ||
|
||
res.status(200).json({ | ||
message: "User created successfully, please login.", | ||
uuid: newUser.uuid, | ||
}); | ||
// Send welcome email after user is created | ||
await postmark.sendEmailWithTemplate({ | ||
From: "[email protected]", | ||
To: newUser.email, | ||
TemplateAlias: "welcome", | ||
TemplateModel: { | ||
product_url: "https://api.phish.directory", | ||
product_name: "Phish Directory API", | ||
name: newUser.name, | ||
email: newUser.email, | ||
company_name: "Phish Directory", | ||
company_address: "36 Old Quarry Rd, Fayston, VT 05673", | ||
}, | ||
}); | ||
|
||
postmark.sendEmail({ | ||
From: "[email protected]", | ||
To: "[email protected]", | ||
Subject: "New User Signup", | ||
// email the team and provide name, and email of the new user, | ||
HtmlBody: `<html><body><h1>New User Signup</h1><p>Name: ${newUser.name}</p><p>Email: ${newUser.email}</p></body></html>`, | ||
}); | ||
|
||
await inviteToSlack(newUser.email); | ||
|
||
// Send success response with the user's uuid | ||
res.status(200).json({ | ||
message: "User created successfully, please login.", | ||
uuid: newUser.uuid, | ||
}); | ||
} catch (error: any) { | ||
res.status(500).json({ | ||
message: "Error creating user", | ||
error: error.message, | ||
}); | ||
} | ||
}); | ||
|
||
/** | ||
|
@@ -114,6 +147,61 @@ router.post("/login", async (req, res) => { | |
return; | ||
} | ||
|
||
// Get IP address from the request | ||
const ipAddress = | ||
req.headers["x-forwarded-for"] || | ||
req.connection.remoteAddress || | ||
req.socket.remoteAddress || | ||
req.ip || | ||
"0.0.0.0"; | ||
|
||
// Get detailed IP information from ipgeolocation.io | ||
let ipInfo = { | ||
ip: ipAddress, | ||
timestamp: new Date().toISOString(), | ||
userAgent: req.headers["user-agent"] || "Unknown", | ||
country: null, | ||
city: null, | ||
region: null, | ||
location: null, | ||
isp: null, | ||
organization: null, | ||
}; | ||
|
||
try { | ||
const API_KEY = process.env.IPGEOLOCATION_API_KEY; // Store your API key in environment variables | ||
const geoResponse = await fetch( | ||
`https://api.ipgeolocation.io/ipgeo?apiKey=${API_KEY}&ip=${ipAddress}` | ||
); | ||
|
||
if (geoResponse.ok) { | ||
const geoData = await geoResponse.json(); | ||
|
||
// Update ipInfo with data from the API | ||
ipInfo = { | ||
...ipInfo, | ||
country: geoData.country_name, | ||
countryCode: geoData.country_code2, | ||
city: geoData.city, | ||
region: geoData.state_prov, | ||
regionCode: geoData.state_code, | ||
zipcode: geoData.zipcode, | ||
// @ts-expect-error | ||
location: { | ||
lat: geoData.latitude, | ||
lng: geoData.longitude, | ||
}, | ||
isp: geoData.isp, | ||
organization: geoData.organization, | ||
timezone: geoData.time_zone?.name, | ||
currency: geoData.currency?.code, | ||
}; | ||
} | ||
} catch (error) { | ||
console.error("Failed to fetch IP geolocation data:", error); | ||
// Continue with basic IP info if geolocation lookup fails | ||
} | ||
|
||
const user = await prisma.user.findUnique({ | ||
where: { | ||
email: email, | ||
|
@@ -138,10 +226,56 @@ router.post("/login", async (req, res) => { | |
); | ||
} | ||
|
||
// Store login attempt with IP information | ||
try { | ||
await prisma.loginAttempt.create({ | ||
data: { | ||
userId: user.id, | ||
ipAddress: ipAddress as string, | ||
userAgent: ipInfo.userAgent, | ||
successful: true, | ||
ipInfo: JSON.stringify(ipInfo), | ||
}, | ||
}); | ||
} catch (error) { | ||
console.error("Failed to log login attempt:", error); | ||
// Continue with login process even if logging fails | ||
} | ||
|
||
let token = await generateAccessToken(user); | ||
|
||
let jsonresponsebody = {}; | ||
|
||
// Update the email to include comprehensive IP address information | ||
await postmark.sendEmail({ | ||
From: "[email protected]", | ||
To: user.email, | ||
Subject: "Phish Directory API Login", | ||
HtmlBody: `<html><body> | ||
<h1>Hello ${user.name}</h1> | ||
<p>You have successfully logged in to the Phish Directory API.</p> | ||
<p>Login details:</p> | ||
<ul> | ||
<li>Time: ${ipInfo.timestamp}</li> | ||
<li>IP Address: ${ipInfo.ip}</li> | ||
<li>Device: ${ipInfo.userAgent}</li> | ||
${ | ||
ipInfo.city | ||
? // @ts-expect-error | ||
`<li>Location: ${ipInfo.city}, ${ipInfo.region} ${ipInfo.countryCode}</li>` | ||
: "" | ||
} | ||
${ | ||
ipInfo.organization | ||
? `<li>Organization: ${ipInfo.organization}</li>` | ||
: "" | ||
} | ||
${ipInfo.isp ? `<li>Internet Provider: ${ipInfo.isp}</li>` : ""} | ||
</ul> | ||
<p>If this wasn't you, please contact <a href="mailto:[email protected]">[email protected]</a> immediately AND change your password.</p> | ||
</body></html>`, | ||
}); | ||
|
||
if (useExteded) { | ||
jsonresponsebody = { | ||
token: token, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import postmark from "postmark"; | ||
|
||
const client = new postmark.ServerClient(process.env.POSTMARK_API_KEY!); | ||
|
||
export default client as postmark.ServerClient; |