mirror of
https://github.com/woodchen-ink/Q58Connect.git
synced 2025-07-18 14:01:55 +08:00
refactor: Replace Discourse SSO with Q58 authentication flow
This commit is contained in:
parent
25f262c1ac
commit
0c1d7fceea
@ -15,7 +15,7 @@ export async function getDiscourseSSOUrl(searchParams: string) {
|
||||
console.log(`searchParams: ${searchParams}`);
|
||||
|
||||
const nonce = WordArray.random(16).toString();
|
||||
const return_url = `${appHost}/discourse/callback?oauth=${btoa(searchParams)}`;
|
||||
const return_url = `${appHost}/q58/callback?oauth=${btoa(searchParams)}`;
|
||||
const sso = btoa(`nonce=${nonce}&return_sso_url=${encodeURI(return_url)}`);
|
||||
const sig = hmacSHA256(sso, oauthSecret).toString(Hex);
|
||||
cookies().set(AUTH_NONCE, nonce, { maxAge: 60 * 10 });
|
||||
|
@ -1,22 +1,22 @@
|
||||
import { Suspense } from "react";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { discourseCallbackVerify } from "@/lib/discourse/verify";
|
||||
import { findAuthorization } from "@/lib/dto/authorization";
|
||||
import { getClientByClientId } from "@/lib/dto/client";
|
||||
import { getAuthorizeUrl } from "@/lib/oauth/authorize-url";
|
||||
import { q58CallbackVerify } from "@/lib/q58/verify";
|
||||
import { AuthorizationCard } from "@/components/auth/authorization-card";
|
||||
|
||||
export interface DiscourseCallbackParams extends Record<string, string> {
|
||||
export interface Q58CallbackParams extends Record<string, string> {
|
||||
sig: string;
|
||||
sso: string;
|
||||
oauth: string;
|
||||
}
|
||||
|
||||
export default async function DiscourseCallbackPage({
|
||||
export default async function Q58CallbackPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: DiscourseCallbackParams;
|
||||
searchParams: Q58CallbackParams;
|
||||
}) {
|
||||
const oauthParams = new URLSearchParams(atob(searchParams.oauth));
|
||||
// check client info
|
||||
@ -27,11 +27,8 @@ export default async function DiscourseCallbackPage({
|
||||
throw new Error("Client Id invalid (code: -1004).");
|
||||
}
|
||||
|
||||
// verify discourse callback
|
||||
const user = await discourseCallbackVerify(
|
||||
searchParams.sso,
|
||||
searchParams.sig,
|
||||
);
|
||||
// verify q58 callback
|
||||
const user = await q58CallbackVerify(searchParams.sso, searchParams.sig);
|
||||
|
||||
// check authorization
|
||||
const authorization = await findAuthorization(user.id, client.id);
|
@ -1,7 +1,7 @@
|
||||
import type { NextAuthConfig } from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
|
||||
import { discourseCallbackVerify } from "./lib/discourse/verify";
|
||||
import { q58CallbackVerify } from "./lib/q58/verify";
|
||||
|
||||
// Notice this is only an object, not a full Auth.js instance
|
||||
export default {
|
||||
@ -14,7 +14,7 @@ export default {
|
||||
authorize: async (credentials) => {
|
||||
const sso = credentials.sso as string;
|
||||
const sig = credentials.sig as string;
|
||||
const user = await discourseCallbackVerify(sso, sig);
|
||||
const user = await q58CallbackVerify(sso, sig);
|
||||
return user;
|
||||
},
|
||||
}),
|
||||
|
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { handleAuthorizeAction } from "@/actions/authorizing";
|
||||
import { Client } from "@prisma/client";
|
||||
@ -75,9 +76,19 @@ export function AuthorizationCard({
|
||||
<div className="group relative">
|
||||
<div className="absolute -inset-0.5 rounded-full bg-gradient-to-r from-pink-600 to-purple-600 opacity-50 blur transition duration-300 group-hover:opacity-75"></div>
|
||||
<div className="relative flex h-20 w-20 items-center justify-center rounded-full bg-white">
|
||||
<span className="bg-gradient-to-r from-pink-600 to-purple-600 bg-clip-text text-4xl font-bold text-transparent">
|
||||
{client.name[0].toUpperCase()}
|
||||
</span>
|
||||
{client.logo ? (
|
||||
<Image
|
||||
src={client.logo}
|
||||
alt={client.name}
|
||||
width={64}
|
||||
height={64}
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="bg-gradient-to-r from-pink-600 to-purple-600 bg-clip-text text-4xl font-bold text-transparent">
|
||||
{client.name[0].toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -90,18 +101,23 @@ export function AuthorizationCard({
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3 rounded-lg bg-gray-50 p-4">
|
||||
<h3 className="text-lg font-semibold">请求的权限</h3>
|
||||
<div className="space-y-3 rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
请求的权限
|
||||
</h3>
|
||||
{permissions.map((permission) => (
|
||||
<div
|
||||
key={permission.id}
|
||||
className="rounded-md border bg-white p-4 transition-all duration-200 hover:border-purple-200"
|
||||
className="rounded-md border border-gray-200 bg-white p-4 shadow-sm transition-all duration-200 hover:border-purple-200 dark:border-gray-700 dark:bg-gray-900"
|
||||
onClick={() => togglePermission(permission.id)}
|
||||
>
|
||||
<div className="flex cursor-pointer items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id={permission.id} checked disabled />
|
||||
<label htmlFor={permission.id} className="font-medium">
|
||||
<label
|
||||
htmlFor={permission.id}
|
||||
className="font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{permission.name}
|
||||
</label>
|
||||
</div>
|
||||
@ -112,7 +128,7 @@ export function AuthorizationCard({
|
||||
)}
|
||||
</div>
|
||||
{expandedPermission === permission.id && (
|
||||
<div className="mt-2 pl-6 text-sm text-gray-500">
|
||||
<div className="mt-2 pl-6 text-sm text-gray-600 dark:text-gray-400">
|
||||
{permission.description}
|
||||
</div>
|
||||
)}
|
||||
|
@ -22,7 +22,7 @@ export function UserAuthForm({
|
||||
|
||||
const signIn = () => {
|
||||
React.startTransition(async () => {
|
||||
const response = await fetch("/api/auth/discourse", { method: "POST" });
|
||||
const response = await fetch("/api/auth/q58", { method: "POST" });
|
||||
if (!response.ok || response.status !== 200) {
|
||||
setIsLoading(false);
|
||||
toast({
|
||||
|
@ -8,11 +8,11 @@ import hmacSHA256 from "crypto-js/hmac-sha256";
|
||||
import { AUTH_NONCE } from "@/lib/constants";
|
||||
import { createUser, getUserById, updateUser } from "@/lib/dto/user";
|
||||
|
||||
const DISCOUSE_SECRET = process.env.DISCOURSE_SECRET as string;
|
||||
const Q58_SECRET = process.env.DISCOURSE_SECRET as string;
|
||||
|
||||
export async function discourseCallbackVerify(sso: string, sig: string) {
|
||||
export async function q58CallbackVerify(sso: string, sig: string) {
|
||||
// 校验数据正确性
|
||||
if (hmacSHA256(sso, DISCOUSE_SECRET).toString(Hex) != sig) {
|
||||
if (hmacSHA256(sso, Q58_SECRET).toString(Hex) != sig) {
|
||||
throw new Error("Request params is invalid (code: -1001).");
|
||||
}
|
||||
// 校验 nonce
|
||||
|
74
src/lib/q58/verify.ts
Normal file
74
src/lib/q58/verify.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import "server-only";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { UserRole } from "@prisma/client";
|
||||
import Hex from "crypto-js/enc-hex";
|
||||
import hmacSHA256 from "crypto-js/hmac-sha256";
|
||||
|
||||
import { AUTH_NONCE } from "@/lib/constants";
|
||||
import { createUser, getUserById, updateUser } from "@/lib/dto/user";
|
||||
|
||||
const Q58_SECRET = process.env.DISCOURSE_SECRET as string;
|
||||
|
||||
export async function q58CallbackVerify(sso: string, sig: string) {
|
||||
// 校验数据正确性
|
||||
if (hmacSHA256(sso, Q58_SECRET).toString(Hex) != sig) {
|
||||
throw new Error("Request params is invalid (code: -1001).");
|
||||
}
|
||||
// 校验 nonce
|
||||
const cookieStore = cookies();
|
||||
let searchParams = new URLSearchParams(atob(sso as string));
|
||||
const nonce = searchParams.get("nonce");
|
||||
if (!cookieStore.has(AUTH_NONCE) || !nonce) {
|
||||
throw new Error("Request params is invalid (code: -1002).");
|
||||
}
|
||||
if (cookieStore.get(AUTH_NONCE)?.value != nonce) {
|
||||
throw new Error("Request params is invalid (code: -1003).");
|
||||
}
|
||||
// cookieStore.delete(AUTH_NONCE);
|
||||
|
||||
const id = searchParams.get("external_id");
|
||||
const email = searchParams.get("email");
|
||||
const username = searchParams.get("username");
|
||||
const name = searchParams.get("name");
|
||||
const avatarUrl = searchParams.get("avatar_url");
|
||||
const isAdmin = searchParams.get("admin") == "true";
|
||||
const moderator = searchParams.get("moderator") == "true";
|
||||
const groups = searchParams.get("groups")?.split(",");
|
||||
if (!id || !email || !username) {
|
||||
throw new Error("User not found.");
|
||||
}
|
||||
|
||||
// 查数据库是否有人
|
||||
let dbUser = await getUserById(id);
|
||||
if (dbUser) {
|
||||
// 更新
|
||||
dbUser = await updateUser(id, {
|
||||
username,
|
||||
email,
|
||||
name,
|
||||
avatarUrl,
|
||||
role: isAdmin ? UserRole.ADMIN : UserRole.USER,
|
||||
moderator,
|
||||
groups,
|
||||
});
|
||||
} else {
|
||||
// 创建
|
||||
dbUser = await createUser({
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
name,
|
||||
avatarUrl,
|
||||
role: isAdmin ? UserRole.ADMIN : UserRole.USER,
|
||||
moderator,
|
||||
groups,
|
||||
});
|
||||
}
|
||||
|
||||
if (!dbUser) {
|
||||
throw new Error("User not create success.");
|
||||
}
|
||||
|
||||
return dbUser;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user