Skip to content

Commit d91bcee

Browse files
authored
Merge pull request #27 from nuthx/dev-0.0.6
Dev 0.0.6
2 parents 40d47bc + 7a4a4f7 commit d91bcee

File tree

15 files changed

+1192
-1281
lines changed

15 files changed

+1192
-1281
lines changed

app/api/auth/login/route.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import crypto from "crypto";
1+
import jwt from "jsonwebtoken";
22
import { UAParser } from "ua-parser-js";
33
import { cookies } from "next/headers";
44
import { prisma } from "@/lib/db";
@@ -38,10 +38,15 @@ export async function POST(request) {
3838
});
3939
}
4040

41-
// Create user token
42-
const token = crypto.createHash("sha256")
43-
.update(user.password + user.username + Date.now().toString())
44-
.digest("hex");
41+
// Create JWT
42+
const token = jwt.sign(
43+
{
44+
userId: user.id,
45+
username: user.username
46+
},
47+
process.env.JWT_SECRET,
48+
{ expiresIn: "30d" }
49+
);
4550

4651
// Get user agent
4752
const ua = UAParser(request.headers).withClientHints();
@@ -53,11 +58,12 @@ export async function POST(request) {
5358
token,
5459
browser: `${ua.browser.name || ""} ${ua.browser.version || ""}`,
5560
os: `${ua.os.name || ""} ${ua.os.version || ""}`,
56-
ip: request.headers.get("x-forwarded-for") || ""
61+
ip: request.headers.get("x-forwarded-for") || "",
62+
expiredAt: new Date(Date.now() + 30 * 86400 * 1000) // 30 days
5763
}
5864
});
5965
} else {
60-
logger.warn(`Non-browser login, token will not be stored in database, user agent: ${ua.ua}`, { model: "POST /api/auth/login" });
66+
logger.warn(`Non-browser login, ua: ${ua.ua}`, { model: "POST /api/auth/login" });
6167
}
6268

6369
// Set cookie
@@ -66,7 +72,7 @@ export async function POST(request) {
6672
name: "auth_token",
6773
value: token,
6874
sameSite: "strict",
69-
expires: new Date(Date.now() + 365 * 86400 * 1000) // 1 year
75+
expires: new Date(Date.now() + 30 * 86400 * 1000) // 30 days
7076
});
7177

7278
return sendResponse(request, {

app/api/auth/verify/route.js

Lines changed: 0 additions & 44 deletions
This file was deleted.

app/api/feeds/route.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import parser from "cron-parser";
1+
import schedule from "node-schedule";
22
import RSSParser from "rss-parser";
33
import { prisma } from "@/lib/db";
44
import { sendResponse } from "@/lib/http/response";
@@ -67,9 +67,11 @@ export async function POST(request) {
6767
}
6868

6969
// Check cron validity
70-
// This will throw an error if the cron is invalid
70+
// Create a new scheduler instance and cancel it immediately to validate the cron
71+
// If the cron is invalid, it will throw an error
7172
try {
72-
parser.parseExpression(data.cron);
73+
const job = schedule.scheduleJob(data.cron, () => {});
74+
job.cancel();
7375
} catch (error) {
7476
throw new Error(`Invalid cron: ${data.cron}, error: ${error.message}`);
7577
}

app/api/users/device/[id]/route.js

Lines changed: 0 additions & 25 deletions
This file was deleted.

app/api/users/device/route.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@ export async function GET(request) {
88
try {
99
const devices = await prisma.device.findMany({
1010
orderBy: {
11-
lastActiveAt: "desc"
11+
createdAt: "desc"
12+
}
13+
});
14+
15+
// Delete expired devices
16+
await prisma.device.deleteMany({
17+
where: {
18+
expiredAt: {
19+
lt: new Date()
20+
}
1221
}
1322
});
1423

app/settings/user/page.js

Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,11 @@ import {
4343
} from "@/components/ui/form"
4444
import { Button } from "@/components/ui/button";
4545
import { Input } from "@/components/ui/input";
46-
import { Trash2, Eye, EyeOff } from "lucide-react"
46+
import { Eye, EyeOff } from "lucide-react"
4747

4848
export default function Devices() {
4949
const { t } = useTranslation();
5050
const [showPassword, setShowPassword] = useState(false);
51-
const [logoutDevice, setLogoutDevice] = useState(null);
5251

5352
const usernameForm = createForm({
5453
new_username: { schema: "username" }
@@ -60,7 +59,7 @@ export default function Devices() {
6059
})();
6160

6261
const { data: usernameData, isLoading: usernameLoading, mutate: mutateUsername } = useData(API.USERNAME, t("toast.failed.fetch_config"));
63-
const { data: deviceData, isLoading: deviceLoading, mutate: mutateDevice } = useData(API.DEVICE, t("toast.failed.fetch_config"));
62+
const { data: deviceData, isLoading: deviceLoading } = useData(API.DEVICE, t("toast.failed.fetch_config"));
6463

6564
// Set page title
6665
useEffect(() => {
@@ -91,13 +90,6 @@ export default function Devices() {
9190
}
9291
};
9392

94-
const handleDelete = async (id) => {
95-
const result = await handleRequest("DELETE", `${API.DEVICE}/${id}`, null, t("toast.failed.delete"));
96-
if (result) {
97-
mutateDevice();
98-
}
99-
};
100-
10193
if (usernameLoading || deviceLoading) {
10294
return <></>;
10395
}
@@ -178,49 +170,29 @@ export default function Devices() {
178170
<TableHead className="px-2 py-4">{t("st.user.devices.os")}</TableHead>
179171
<TableHead className="px-2 py-4">{t("st.user.devices.browser")}</TableHead>
180172
<TableHead className="px-2 py-4">{t("st.user.devices.ip")}</TableHead>
181-
<TableHead className="px-2 py-4">{t("st.user.devices.last")}</TableHead>
182-
<TableHead className="px-3 py-4 w-4"></TableHead>
173+
<TableHead className="px-2 py-4">{t("st.user.devices.created")}</TableHead>
174+
<TableHead className="px-2 py-4">{t("st.user.devices.expired")}</TableHead>
183175
</TableRow>
184176
</TableHeader>
185177
<TableBody>
186178
{deviceData?.devices.map((device) => (
187179
<TableRow key={device.id} className="hover:bg-transparent">
188180
<TableCell className="px-2 py-4">{device.os}</TableCell>
189-
<TableCell className="px-2 py-4">{device.browser}</TableCell>
190-
<TableCell className="px-2 py-4">{device.ip}</TableCell>
191181
<TableCell className="px-2 py-4">
192-
{deviceData.currentDevice === device.id ? (
193-
t("st.user.devices.current")
194-
) : (
195-
new Date(device.lastActiveAt).toLocaleString()
196-
)}
197-
</TableCell>
198-
<TableCell className="px-3 py-4 w-4">
199-
<Button variant="ghost" size="icon" disabled={deviceData.currentDevice === device.id} onClick={() => setLogoutDevice(device)}>
200-
<Trash2 className="text-muted-foreground"/>
201-
</Button>
182+
<div className="flex items-center gap-2">
183+
{device.browser}
184+
{deviceData.currentDevice === device.id && (
185+
<div className="w-2 h-2 rounded-full bg-green-500"></div>
186+
)}
187+
</div>
202188
</TableCell>
189+
<TableCell className="px-2 py-4">{device.ip}</TableCell>
190+
<TableCell className="px-2 py-4">{new Date(device.createdAt).toLocaleString()}</TableCell>
191+
<TableCell className="px-2 py-4">{new Date(device.expiredAt).toLocaleString()}</TableCell>
203192
</TableRow>
204193
))}
205194
</TableBody>
206195
</Table>
207-
208-
<AlertDialog open={logoutDevice !== null} onOpenChange={(open) => !open && setLogoutDevice(null)}>
209-
<AlertDialogContent>
210-
<AlertDialogHeader>
211-
<AlertDialogTitle>{t("glb.confirm_logout")}</AlertDialogTitle>
212-
<AlertDialogDescription>{t("st.user.devices.alert")}</AlertDialogDescription>
213-
</AlertDialogHeader>
214-
<AlertDialogFooter>
215-
<AlertDialogCancel>
216-
{t("glb.cancel")}
217-
</AlertDialogCancel>
218-
<AlertDialogAction onClick={() => { handleDelete(logoutDevice.id); setLogoutDevice(null); }}>
219-
{t("glb.logout")}
220-
</AlertDialogAction>
221-
</AlertDialogFooter>
222-
</AlertDialogContent>
223-
</AlertDialog>
224196
</CardContent>
225197
</Card>
226198
</>

i18n/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@
283283
"os": "Operating System",
284284
"browser": "Browser",
285285
"ip": "IP Address",
286-
"last": "Last Used",
286+
"created": "Created Time",
287+
"expired": "Expired Time",
287288
"current": "This Device",
288289
"alert": "After logging out, you will need to log in again the next time you use the device."
289290
}

i18n/locales/zh.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@
283283
"os": "操作系统",
284284
"browser": "浏览器",
285285
"ip": "IP 地址",
286-
"last": "最后使用",
286+
"created": "创建时间",
287+
"expired": "过期时间",
287288
"current": "当前设备",
288289
"alert": "登出设备后,下次使用该设备时,需要重新登录"
289290
}

lib/http/request.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { toast } from "sonner";
22

3-
export async function handleRequest(method, api, values = null, errorTitle = "", showError = true) {
3+
export async function handleRequest(method, api, values = null, errorTitle = "") {
44
try {
55
const response = await fetch(api, {
66
method: method,
@@ -15,10 +15,8 @@ export async function handleRequest(method, api, values = null, errorTitle = "",
1515

1616
return result;
1717
} catch (error) {
18-
if (showError) {
19-
toast.error(errorTitle, {
20-
description: error.message
21-
});
22-
}
18+
toast.error(errorTitle, {
19+
description: error.message
20+
});
2321
}
2422
};

middleware.js

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,41 @@
11
import { NextResponse } from "next/server";
2-
import { handleRequest } from "@/lib/http/request";
2+
import { jwtVerify } from "jose";
33

44
export async function middleware(request) {
55
const token = request.cookies.get("auth_token");
6-
const isAuthenticated = await verifyToken(token, request.nextUrl.origin);
7-
8-
// If logged in, redirect login page to homepage
9-
if (request.nextUrl.pathname === "/login") {
10-
return isAuthenticated
11-
? NextResponse.redirect(new URL("/", request.url))
12-
: NextResponse.next();
13-
}
6+
const isAuthenticated = await verifyToken(token);
147

158
// Exclude auth routes
169
if (request.nextUrl.pathname.startsWith("/api/auth")) {
1710
return NextResponse.next();
1811
}
1912

20-
// Check login status
21-
if (isAuthenticated) {
22-
return NextResponse.next();
13+
// Redirect login page by login status
14+
if (request.nextUrl.pathname === "/login") {
15+
return isAuthenticated
16+
? NextResponse.redirect(new URL("/anime", request.url))
17+
: NextResponse.next();
2318
}
2419

2520
// If not logged in, return with different response
26-
if (request.nextUrl.pathname.startsWith("/api")) {
27-
// For API routes, return 401 Unauthorized
28-
return NextResponse.json({ code: 401, message: "Unauthorized" }, { status: 401 });
29-
} else {
30-
// For other routes, redirect to login page
31-
return NextResponse.redirect(new URL("/login", request.url));
21+
if (!isAuthenticated) {
22+
return request.nextUrl.pathname.startsWith("/api")
23+
? NextResponse.json({ code: 401, message: "Unauthorized", data: null }, { status: 401 })
24+
: NextResponse.redirect(new URL("/login", request.url));
3225
}
26+
27+
return NextResponse.next();
3328
}
3429

35-
// Check token availability
36-
async function verifyToken(token, origin) {
30+
async function verifyToken(token) {
3731
if (!token) return false;
38-
const result = await handleRequest("POST", `${origin}/api/auth/verify`, { token }, "", false);
39-
return result;
32+
try {
33+
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
34+
await jwtVerify(token.value, secret);
35+
return true;
36+
} catch (error) {
37+
return false;
38+
}
4039
}
4140

4241
export const config = {

0 commit comments

Comments
 (0)