Skip to content

Commit

Permalink
Merge pull request #15 from ZeroCatDev/SunWuyuan/add-magiclink-login
Browse files Browse the repository at this point in the history
Add magiclink login functionality
  • Loading branch information
SunWuyuan authored Nov 16, 2024
2 parents d193209 + 7a8d057 commit 589e6a0
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 24 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
DATABASE_URL="mysql://[username]:[password]@[host]:[port]/[database]"
DATABASE_URL="mysql://[username]:[password]@[host]:[port]/[database]"

MAGICLINK_SECRET="your-magiclink-secret"
MAGICLINK_EXPIRATION="3600" # 1 hour
MAGICLINK_BASE_URL="http://localhost:3000"
9 changes: 8 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ model ow_users {
updatedAt DateTime? @default(dbgenerated("'2000-03-31 16:00:00'")) @db.Timestamp(0)
label String? @db.VarChar(255)
@@id([id, username])
@@id([id, username, email])
}

/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
Expand Down Expand Up @@ -359,6 +359,13 @@ model ow_users_totp {
updated_at DateTime? @default(now()) @db.Timestamp(0)
}

model magiclinktoken {
id Int @id @default(autoincrement())
userId Int
token String @unique(map: "token") @db.VarChar(255)
expiresAt DateTime @db.DateTime(0)
}

/// The underlying view does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client.
/// This view or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
view ow_projects_view {
Expand Down
7 changes: 5 additions & 2 deletions server/configManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ class ConfigManager {
// Configuration information
global.configinfo = configs;

console.log(global.configinfo); // Log the updated config info
//console.log(global.configinfo); // Log the updated config info
}

async getConfig(key) {
// Check if the value is already cached
if (global.config && global.config[key]) {
if (global.config && global.config[key]!=null) {
return global.config[key];
}
var config = await this.prisma.ow_config.findFirst({ where: { key: key } });
console.log(config);
return config.value;
// If not cached, fetch from the database
await this.loadConfigsFromDB();
// If not cached, fetch from the database
Expand Down
89 changes: 89 additions & 0 deletions server/router_account.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,93 @@ router.post("/totp/protected-route", validateTotpToken, (req, res) => {
});
});

router.post("/magiclink/generate", async (req, res) => {
try {
const { email } = req.body;
if (!email || !I.emailTest(email)) {
return res.status(200).json({ status: "error",message: "无效的邮箱地址" });
}

const user = await I.prisma.ow_users.findFirst({ where: { email } });
if (!user) {
//用户不存在
return res.status(404).json({ status: "error",message: "无效的邮箱地址" });
}

const token = jwt.sign(
{ id: user.id },
await configManager.getConfig("security.jwttoken"),
{ expiresIn: 60 * 10 }
);

await I.prisma.magiclinktoken.create({
data: {
userId: user.id,
token,
expiresAt: new Date(Date.now() + 60 * 1000),
},
});

const magicLink = `${await configManager.getConfig("urls.frontend")}/account/magiclink/validate?token=${token}`;
await sendEmail(
email,
"Magic Link 登录",
`点击以下链接登录:<a href="${magicLink}">${magicLink}</a>`
);

res.status(200).json({status: "success", message: "Magic Link 已发送到您的邮箱" });
} catch (error) {
console.error("生成 Magic Link 时出错:", error);
res.status(200).json({ status: "error", message: "生成 Magic Link 失败" });
}
});

router.get("/magiclink/validate", async (req, res) => {
try {
const { token } = req.query;
if (!token) {
return res.status(200).json({status: "error", message: "无效的 Magic Link" });
}

const decoded = jwt.verify(token, await configManager.getConfig("security.jwttoken"));
const magicLinkToken = await I.prisma.magiclinktoken.findUnique({
where: { token },
});

if (!magicLinkToken || magicLinkToken.expiresAt < new Date()) {
return res.status(200).json({ status: "error",message: "Magic Link 已过期" });
}

const user = await I.prisma.ow_users.findUnique({
where: { id: magicLinkToken.userId },
});

if (!user) {
return res.status(404).json({ status: "error",message: "用户不存在" });
}

const jwtToken = await I.GenerateJwt({
userid: user.id,
username: user.username,
email: user.email,
display_name: user.display_name,
avatar: user.images,
});

res.status(200).json({
status: "success",
message: "登录成功",
userid: user.id,
email: user.email,
username: user.username,
display_name: user.display_name,
avatar: user.images,
token: jwtToken,
});
} catch (error) {
console.error("验证 Magic Link 时出错:", error);
res.status(200).json({ status: "error",message: "验证 Magic Link 失败" });
}
});

module.exports = router;
45 changes: 25 additions & 20 deletions server/services/emailService.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
const configManager = require("../configManager");

const nodemailer = require('nodemailer');
let service, user, pass
configManager.getConfig('mail.service').then((res) => {
service = res
});
configManager.getConfig('mail.user').then((res) => {
user = res
});
configManager.getConfig('mail.pass').then((res) => {
pass = res
});
const transporter = nodemailer.createTransport({
service: service,
secure: true,
auth: {
user: user,
pass: pass,
},
const nodemailer = require("nodemailer");
let service, user, pass, transporter;
configManager.getConfig("mail.service").then((res) => {
service = res;

configManager.getConfig("mail.user").then((res) => {
user = res;

configManager.getConfig("mail.pass").then((res) => {
pass = res;
//console.log(service, user, pass);
transporter = nodemailer.createTransport({
service: service,
secure: true,
auth: {
user: user,
pass: pass,
},
});
});
});
});

const sendEmail = async (to, subject, html) => {
try {
await transporter.sendMail({
from: `${await configManager.getConfig('site.name')} <${await configManager.getConfig('mail.from')}>`,
from: `${await configManager.getConfig(
"site.name"
)} <${await configManager.getConfig("mail.from")}>`,
to,
subject,
html,
});
} catch (error) {
console.error('Error sending email:', error);
console.error("Error sending email:", error);
}
};

Expand Down

0 comments on commit 589e6a0

Please sign in to comment.