Initial release v1.0.0
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
import { Router } from "express";
|
||||
import { requireAuthOrToken } from "../middleware/auth";
|
||||
import { prisma } from "../utils/db";
|
||||
import crypto from "crypto";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Generate a random 6-character alphanumeric code
|
||||
function generateLinkCode(): string {
|
||||
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // Exclude similar looking chars
|
||||
let code = "";
|
||||
for (let i = 0; i < 6; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
// Generate API key
|
||||
function generateApiKey(): string {
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
}
|
||||
|
||||
// POST /device-link/generate - Generate a new device link code (requires auth)
|
||||
router.post("/generate", requireAuthOrToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user!.id;
|
||||
|
||||
// Delete any existing unused codes for this user
|
||||
await prisma.deviceLinkCode.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
usedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Generate a unique code
|
||||
let code: string;
|
||||
let attempts = 0;
|
||||
do {
|
||||
code = generateLinkCode();
|
||||
attempts++;
|
||||
if (attempts > 10) {
|
||||
return res.status(500).json({ error: "Failed to generate unique code" });
|
||||
}
|
||||
} while (
|
||||
await prisma.deviceLinkCode.findUnique({
|
||||
where: { code },
|
||||
})
|
||||
);
|
||||
|
||||
// Create the code with 5-minute expiry
|
||||
const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
|
||||
const linkCode = await prisma.deviceLinkCode.create({
|
||||
data: {
|
||||
code,
|
||||
userId,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: linkCode.code,
|
||||
expiresAt: linkCode.expiresAt,
|
||||
expiresIn: 300, // 5 minutes in seconds
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Generate device link code error:", error);
|
||||
res.status(500).json({ error: "Failed to generate device link code" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /device-link/verify - Verify a code and get API key (no auth required)
|
||||
router.post("/verify", async (req, res) => {
|
||||
try {
|
||||
const { code, deviceName } = req.body;
|
||||
|
||||
if (!code || typeof code !== "string") {
|
||||
return res.status(400).json({ error: "Code is required" });
|
||||
}
|
||||
|
||||
// Find the code
|
||||
const linkCode = await prisma.deviceLinkCode.findUnique({
|
||||
where: { code: code.toUpperCase() },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (!linkCode) {
|
||||
return res.status(404).json({ error: "Invalid code" });
|
||||
}
|
||||
|
||||
if (linkCode.usedAt) {
|
||||
return res.status(400).json({ error: "Code already used" });
|
||||
}
|
||||
|
||||
if (new Date() > linkCode.expiresAt) {
|
||||
return res.status(400).json({ error: "Code expired" });
|
||||
}
|
||||
|
||||
// Generate API key for this device
|
||||
const apiKey = generateApiKey();
|
||||
const createdApiKey = await prisma.apiKey.create({
|
||||
data: {
|
||||
userId: linkCode.userId,
|
||||
key: apiKey,
|
||||
name: deviceName || "Mobile Device",
|
||||
},
|
||||
});
|
||||
|
||||
// Mark the link code as used
|
||||
await prisma.deviceLinkCode.update({
|
||||
where: { id: linkCode.id },
|
||||
data: {
|
||||
usedAt: new Date(),
|
||||
deviceName: deviceName || "Mobile Device",
|
||||
apiKeyId: createdApiKey.id,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
apiKey,
|
||||
userId: linkCode.userId,
|
||||
username: linkCode.user.username,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Verify device link code error:", error);
|
||||
res.status(500).json({ error: "Failed to verify device link code" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /device-link/status/:code - Poll for code usage status (no auth required)
|
||||
router.get("/status/:code", async (req, res) => {
|
||||
try {
|
||||
const { code } = req.params;
|
||||
|
||||
const linkCode = await prisma.deviceLinkCode.findUnique({
|
||||
where: { code: code.toUpperCase() },
|
||||
});
|
||||
|
||||
if (!linkCode) {
|
||||
return res.status(404).json({ error: "Invalid code" });
|
||||
}
|
||||
|
||||
if (new Date() > linkCode.expiresAt && !linkCode.usedAt) {
|
||||
return res.json({
|
||||
status: "expired",
|
||||
expiresAt: linkCode.expiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
if (linkCode.usedAt) {
|
||||
return res.json({
|
||||
status: "used",
|
||||
usedAt: linkCode.usedAt,
|
||||
deviceName: linkCode.deviceName,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: "pending",
|
||||
expiresAt: linkCode.expiresAt,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Check device link status error:", error);
|
||||
res.status(500).json({ error: "Failed to check status" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /device-link/devices - List linked devices (requires auth)
|
||||
router.get("/devices", requireAuthOrToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user!.id;
|
||||
|
||||
const apiKeys = await prisma.apiKey.findMany({
|
||||
where: { userId },
|
||||
orderBy: { lastUsed: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
lastUsed: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
res.json(apiKeys);
|
||||
} catch (error) {
|
||||
console.error("Get devices error:", error);
|
||||
res.status(500).json({ error: "Failed to get devices" });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /device-link/devices/:id - Revoke a device (requires auth)
|
||||
router.delete("/devices/:id", requireAuthOrToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user!.id;
|
||||
const { id } = req.params;
|
||||
|
||||
const apiKey = await prisma.apiKey.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(404).json({ error: "Device not found" });
|
||||
}
|
||||
|
||||
await prisma.apiKey.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Revoke device error:", error);
|
||||
res.status(500).json({ error: "Failed to revoke device" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user