mirror of
https://github.com/d0zingcat/alert-message-center.git
synced 2026-05-13 15:09:19 +00:00
164 lines
4.1 KiB
TypeScript
164 lines
4.1 KiB
TypeScript
import { eq } from "drizzle-orm";
|
|
import { Hono } from "hono";
|
|
import { getCookie, setCookie } from "hono/cookie";
|
|
import { db } from "./db";
|
|
import { users } from "./db/schema";
|
|
import { feishuClient } from "./feishu";
|
|
import { logger } from "./lib/logger";
|
|
|
|
const auth = new Hono();
|
|
|
|
const ADMIN_EMAILS = (process.env.ADMIN_EMAILS || "")
|
|
.split(",")
|
|
.map((email) => email.trim())
|
|
.filter(Boolean);
|
|
|
|
// Get the login URL for frontend to redirect
|
|
auth.get("/login-url", (c) => {
|
|
const appId = process.env.FEISHU_APP_ID;
|
|
const redirectUri = encodeURIComponent(
|
|
process.env.REDIRECT_URI || "http://localhost:5173/auth/callback",
|
|
);
|
|
const state = crypto.randomUUID();
|
|
|
|
// Store state in cookie for CSRF protection
|
|
setCookie(c, "oauth_state", state, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
maxAge: 600, // 10 minutes
|
|
sameSite: "Lax",
|
|
});
|
|
|
|
const loginUrl = `https://open.feishu.cn/open-apis/authen/v1/index?app_id=${appId}&redirect_uri=${redirectUri}&state=${state}`;
|
|
|
|
return c.json({ loginUrl });
|
|
});
|
|
|
|
// Handle OAuth callback
|
|
auth.get("/callback", async (c) => {
|
|
const code = c.req.query("code");
|
|
const state = c.req.query("state");
|
|
const storedState = getCookie(c, "oauth_state");
|
|
|
|
// Verify state for CSRF protection
|
|
if (!state || state !== storedState) {
|
|
return c.json({ error: "Invalid state parameter" }, 400);
|
|
}
|
|
|
|
if (!code) {
|
|
return c.json({ error: "No code provided" }, 400);
|
|
}
|
|
|
|
try {
|
|
// Exchange code for user access token and user info
|
|
const userData = await feishuClient.getUserAccessToken(code);
|
|
|
|
if (!userData) {
|
|
logger.error("[Auth] Failed to get user data from code");
|
|
return c.json({ error: "Failed to get user info from Feishu" }, 500);
|
|
}
|
|
|
|
// Check if user exists, otherwise create
|
|
let user = await db.query.users.findFirst({
|
|
where: eq(users.feishuUserId, userData.open_id),
|
|
});
|
|
|
|
const isAdmin = ADMIN_EMAILS.includes(userData.email || "");
|
|
|
|
if (!user) {
|
|
// Create new user
|
|
const result = await db
|
|
.insert(users)
|
|
.values({
|
|
name: userData.name,
|
|
feishuUserId: userData.open_id,
|
|
email: userData.email || null,
|
|
isAdmin,
|
|
})
|
|
.returning();
|
|
user = result[0];
|
|
} else {
|
|
// Update user info (don't overwrite admin/trusted status from feishu logic unless it's a new admin)
|
|
const result = await db
|
|
.update(users)
|
|
.set({
|
|
name: userData.name,
|
|
email: userData.email || user.email,
|
|
isAdmin: user.isAdmin || isAdmin, // Keep admin if already admin or in ADMIN_EMAILS
|
|
})
|
|
.where(eq(users.id, user.id))
|
|
.returning();
|
|
user = result[0];
|
|
}
|
|
|
|
// Set session cookie
|
|
setCookie(
|
|
c,
|
|
"session",
|
|
JSON.stringify({
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email,
|
|
isAdmin: user.isAdmin,
|
|
isTrusted: user.isTrusted,
|
|
personalToken: user.personalToken,
|
|
}),
|
|
{
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
sameSite: "Lax",
|
|
},
|
|
);
|
|
|
|
return c.json({
|
|
success: true,
|
|
user: {
|
|
id: user.id,
|
|
name: user.name,
|
|
email: user.email,
|
|
isAdmin: user.isAdmin,
|
|
isTrusted: user.isTrusted,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error({ err: error }, "OAuth callback error");
|
|
return c.json({ error: "Authentication failed" }, 500);
|
|
}
|
|
});
|
|
|
|
// Get current user from session
|
|
auth.get("/me", (c) => {
|
|
const sessionCookie = getCookie(c, "session");
|
|
|
|
if (!sessionCookie) {
|
|
return c.json({ error: "Not authenticated" }, 401);
|
|
}
|
|
|
|
try {
|
|
const session = sessionCookie ? JSON.parse(sessionCookie) : null;
|
|
if (!session) {
|
|
return c.json({ error: "Not authenticated" }, 401);
|
|
}
|
|
// Normalize user object to ensure id is present (handle legacy session with userId)
|
|
const user = {
|
|
...session,
|
|
id: session.id || session.userId,
|
|
};
|
|
return c.json({ user });
|
|
} catch (error) {
|
|
logger.error({ err: error }, "[Auth] Failed to parse session cookie");
|
|
return c.json({ error: "Invalid session" }, 401);
|
|
}
|
|
});
|
|
|
|
// Logout
|
|
auth.post("/logout", (c) => {
|
|
setCookie(c, "session", "", {
|
|
maxAge: 0,
|
|
});
|
|
return c.json({ success: true });
|
|
});
|
|
|
|
export default auth;
|