pre-tiptap
This commit is contained in:
@@ -23,8 +23,6 @@ function AppLayout(props: { children: any }) {
|
|||||||
centerWidth,
|
centerWidth,
|
||||||
leftBarVisible,
|
leftBarVisible,
|
||||||
rightBarVisible,
|
rightBarVisible,
|
||||||
toggleLeftBar,
|
|
||||||
toggleRightBar,
|
|
||||||
setLeftBarVisible,
|
setLeftBarVisible,
|
||||||
setRightBarVisible,
|
setRightBarVisible,
|
||||||
barsInitialized
|
barsInitialized
|
||||||
|
|||||||
@@ -156,6 +156,18 @@ export function LeftBar() {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const [isMounted, setIsMounted] = createSignal(false);
|
const [isMounted, setIsMounted] = createSignal(false);
|
||||||
|
const [signOutLoading, setSignOutLoading] = createSignal(false);
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
setSignOutLoading(true);
|
||||||
|
try {
|
||||||
|
await api.auth.signOut.mutate();
|
||||||
|
window.location.href = "/";
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Sign out failed:", error);
|
||||||
|
setSignOutLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Mark as mounted to avoid hydration mismatch
|
// Mark as mounted to avoid hydration mismatch
|
||||||
@@ -416,6 +428,17 @@ export function LeftBar() {
|
|||||||
</a>
|
</a>
|
||||||
</Show>
|
</Show>
|
||||||
</li>
|
</li>
|
||||||
|
<Show when={isMounted() && userInfo()?.isAuthenticated}>
|
||||||
|
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||||
|
<button
|
||||||
|
onClick={handleSignOut}
|
||||||
|
disabled={signOutLoading()}
|
||||||
|
class="text-left disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{signOutLoading() ? "Signing Out..." : "Sign Out"}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</Show>
|
||||||
</ul>
|
</ul>
|
||||||
</Typewriter>
|
</Typewriter>
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ const BarsContext = createContext<{
|
|||||||
setLeftBarVisible: (visible: boolean) => void;
|
setLeftBarVisible: (visible: boolean) => void;
|
||||||
rightBarVisible: Accessor<boolean>;
|
rightBarVisible: Accessor<boolean>;
|
||||||
setRightBarVisible: (visible: boolean) => void;
|
setRightBarVisible: (visible: boolean) => void;
|
||||||
toggleLeftBar: () => void;
|
|
||||||
toggleRightBar: () => void;
|
|
||||||
barsInitialized: Accessor<boolean>;
|
barsInitialized: Accessor<boolean>;
|
||||||
}>({
|
}>({
|
||||||
leftBarSize: () => 0,
|
leftBarSize: () => 0,
|
||||||
@@ -27,8 +25,6 @@ const BarsContext = createContext<{
|
|||||||
setLeftBarVisible: () => {},
|
setLeftBarVisible: () => {},
|
||||||
rightBarVisible: () => true,
|
rightBarVisible: () => true,
|
||||||
setRightBarVisible: () => {},
|
setRightBarVisible: () => {},
|
||||||
toggleLeftBar: () => {},
|
|
||||||
toggleRightBar: () => {},
|
|
||||||
barsInitialized: () => false
|
barsInitialized: () => false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,10 +108,6 @@ export function BarsProvider(props: { children: any }) {
|
|||||||
hapticFeedback(50);
|
hapticFeedback(50);
|
||||||
_setRightBarVisible(visible);
|
_setRightBarVisible(visible);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleLeftBar = () => setLeftBarVisible(!leftBarVisible());
|
|
||||||
const toggleRightBar = () => setRightBarVisible(!rightBarVisible());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BarsContext.Provider
|
<BarsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -129,8 +121,6 @@ export function BarsProvider(props: { children: any }) {
|
|||||||
setLeftBarVisible,
|
setLeftBarVisible,
|
||||||
rightBarVisible,
|
rightBarVisible,
|
||||||
setRightBarVisible,
|
setRightBarVisible,
|
||||||
toggleLeftBar,
|
|
||||||
toggleRightBar,
|
|
||||||
barsInitialized
|
barsInitialized
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export default function Home() {
|
|||||||
<Typewriter speed={30} keepAlive={2000}>
|
<Typewriter speed={30} keepAlive={2000}>
|
||||||
<div class="text-4xl">Hey!</div>
|
<div class="text-4xl">Hey!</div>
|
||||||
</Typewriter>
|
</Typewriter>
|
||||||
|
|
||||||
<Typewriter speed={80} keepAlive={2000}>
|
<Typewriter speed={80} keepAlive={2000}>
|
||||||
<div>
|
<div>
|
||||||
My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
|
My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
|
||||||
@@ -15,10 +14,16 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</Typewriter>
|
</Typewriter>
|
||||||
<Typewriter speed={100}>
|
<Typewriter speed={100}>
|
||||||
I'm a passionate dev tooling and game developer, recently been working
|
I'm a passionate dev tooling, game, and open source software developer.
|
||||||
in the world of Love2D and you can see some of my work here: <a></a>
|
Recently been working in the world of{" "}
|
||||||
I'm a huge lover of open source software, and
|
<a
|
||||||
|
href="https://www.love2d.org"
|
||||||
|
class="text-blue hover-underline-animation"
|
||||||
|
>
|
||||||
|
LÖVE
|
||||||
|
</a>{" "}
|
||||||
</Typewriter>
|
</Typewriter>
|
||||||
|
You can see some of my work <a>here</a>(github)
|
||||||
<Typewriter speed={50} keepAlive={false}>
|
<Typewriter speed={50} keepAlive={false}>
|
||||||
<div>My Collection of By-the-ways:</div>
|
<div>My Collection of By-the-ways:</div>
|
||||||
</Typewriter>
|
</Typewriter>
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import { setCookie, getCookie } from "vinxi/http";
|
|||||||
import type { User } from "~/types/user";
|
import type { User } from "~/types/user";
|
||||||
|
|
||||||
// Helper to create JWT token
|
// Helper to create JWT token
|
||||||
async function createJWT(userId: string, expiresIn: string = "14d"): Promise<string> {
|
async function createJWT(
|
||||||
|
userId: string,
|
||||||
|
expiresIn: string = "14d"
|
||||||
|
): Promise<string> {
|
||||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const token = await new SignJWT({ id: userId })
|
const token = await new SignJWT({ id: userId })
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
@@ -26,11 +29,11 @@ async function sendEmail(to: string, subject: string, htmlContent: string) {
|
|||||||
const sendinblueData = {
|
const sendinblueData = {
|
||||||
sender: {
|
sender: {
|
||||||
name: "freno.me",
|
name: "freno.me",
|
||||||
email: "no_reply@freno.me",
|
email: "no_reply@freno.me"
|
||||||
},
|
},
|
||||||
to: [{ email: to }],
|
to: [{ email: to }],
|
||||||
htmlContent,
|
htmlContent,
|
||||||
subject,
|
subject
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
@@ -38,9 +41,9 @@ async function sendEmail(to: string, subject: string, htmlContent: string) {
|
|||||||
headers: {
|
headers: {
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
"api-key": apiKey,
|
"api-key": apiKey,
|
||||||
"content-type": "application/json",
|
"content-type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(sendinblueData),
|
body: JSON.stringify(sendinblueData)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -65,22 +68,22 @@ export const authRouter = createTRPCRouter({
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
Accept: "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
client_id: env.VITE_GITHUB_CLIENT_ID || env.NEXT_PUBLIC_GITHUB_CLIENT_ID,
|
client_id: env.VITE_GITHUB_CLIENT_ID,
|
||||||
client_secret: env.GITHUB_CLIENT_SECRET,
|
client_secret: env.GITHUB_CLIENT_SECRET,
|
||||||
code,
|
code
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const { access_token } = await tokenResponse.json();
|
const { access_token } = await tokenResponse.json();
|
||||||
|
|
||||||
// Fetch user info from GitHub
|
// Fetch user info from GitHub
|
||||||
const userResponse = await fetch("https://api.github.com/user", {
|
const userResponse = await fetch("https://api.github.com/user", {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `token ${access_token}`,
|
Authorization: `token ${access_token}`
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const user = await userResponse.json();
|
const user = await userResponse.json();
|
||||||
@@ -117,18 +120,18 @@ export const authRouter = createTRPCRouter({
|
|||||||
path: "/",
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: env.NODE_ENV === "production",
|
secure: env.NODE_ENV === "production",
|
||||||
sameSite: "lax",
|
sameSite: "lax"
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
redirectTo: "/account",
|
redirectTo: "/account"
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("GitHub authentication failed:", error);
|
console.error("GitHub authentication failed:", error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "GitHub authentication failed",
|
message: "GitHub authentication failed"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -141,19 +144,22 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Exchange code for access token
|
// Exchange code for access token
|
||||||
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
const tokenResponse = await fetch(
|
||||||
|
"https://oauth2.googleapis.com/token",
|
||||||
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
code: code,
|
code: code,
|
||||||
client_id: env.VITE_GOOGLE_CLIENT_ID || env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "",
|
client_id: env.VITE_GOOGLE_CLIENT_ID || "",
|
||||||
client_secret: env.GOOGLE_CLIENT_SECRET,
|
client_secret: env.GOOGLE_CLIENT_SECRET,
|
||||||
redirect_uri: `${env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN}/api/auth/callback/google`,
|
redirect_uri: `${env.VITE_DOMAIN || "https://freno.me"}/api/auth/callback/google`,
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code"
|
||||||
}),
|
})
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { access_token } = await tokenResponse.json();
|
const { access_token } = await tokenResponse.json();
|
||||||
|
|
||||||
@@ -162,9 +168,9 @@ export const authRouter = createTRPCRouter({
|
|||||||
"https://www.googleapis.com/oauth2/v3/userinfo",
|
"https://www.googleapis.com/oauth2/v3/userinfo",
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${access_token}`,
|
Authorization: `Bearer ${access_token}`
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const userData = await userResponse.json();
|
const userData = await userResponse.json();
|
||||||
@@ -196,11 +202,11 @@ export const authRouter = createTRPCRouter({
|
|||||||
email_verified,
|
email_verified,
|
||||||
name,
|
name,
|
||||||
"google",
|
"google",
|
||||||
image,
|
image
|
||||||
];
|
];
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: insertQuery,
|
sql: insertQuery,
|
||||||
args: insertParams,
|
args: insertParams
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,18 +219,18 @@ export const authRouter = createTRPCRouter({
|
|||||||
path: "/",
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: env.NODE_ENV === "production",
|
secure: env.NODE_ENV === "production",
|
||||||
sameSite: "lax",
|
sameSite: "lax"
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
redirectTo: "/account",
|
redirectTo: "/account"
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Google authentication failed:", error);
|
console.error("Google authentication failed:", error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Google authentication failed",
|
message: "Google authentication failed"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -235,8 +241,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
rememberMe: z.boolean().optional(),
|
rememberMe: z.boolean().optional()
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { email, token, rememberMe } = input;
|
const { email, token, rememberMe } = input;
|
||||||
@@ -250,7 +256,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (payload.email !== email) {
|
if (payload.email !== email) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Email mismatch",
|
message: "Email mismatch"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +268,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (!res.rows[0]) {
|
if (!res.rows[0]) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +282,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
path: "/",
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: env.NODE_ENV === "production",
|
secure: env.NODE_ENV === "production",
|
||||||
sameSite: "lax",
|
sameSite: "lax"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
@@ -284,11 +290,16 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
// If rememberMe is false, cookie will be session-only (no maxAge)
|
// If rememberMe is false, cookie will be session-only (no maxAge)
|
||||||
|
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", userToken, cookieOptions);
|
setCookie(
|
||||||
|
ctx.event.nativeEvent,
|
||||||
|
"userIDToken",
|
||||||
|
userToken,
|
||||||
|
cookieOptions
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
redirectTo: "/account",
|
redirectTo: "/account"
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
@@ -297,7 +308,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
console.error("Email login failed:", error);
|
console.error("Email login failed:", error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Authentication failed",
|
message: "Authentication failed"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -307,8 +318,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
token: z.string(),
|
token: z.string()
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { email, token } = input;
|
const { email, token } = input;
|
||||||
@@ -322,7 +333,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (payload.email !== email) {
|
if (payload.email !== email) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Email mismatch",
|
message: "Email mismatch"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +344,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Email verification success, you may close this window",
|
message: "Email verification success, you may close this window"
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) {
|
if (error instanceof TRPCError) {
|
||||||
@@ -342,7 +353,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
console.error("Email verification failed:", error);
|
console.error("Email verification failed:", error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Invalid token",
|
message: "Invalid token"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -353,8 +364,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string().min(8),
|
password: z.string().min(8),
|
||||||
passwordConfirmation: z.string().min(8),
|
passwordConfirmation: z.string().min(8)
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { email, password, passwordConfirmation } = input;
|
const { email, password, passwordConfirmation } = input;
|
||||||
@@ -362,7 +373,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (password !== passwordConfirmation) {
|
if (password !== passwordConfirmation) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "passwordMismatch",
|
message: "passwordMismatch"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +384,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "INSERT INTO User (id, email, password_hash, provider) VALUES (?, ?, ?, ?)",
|
sql: "INSERT INTO User (id, email, password_hash, provider) VALUES (?, ?, ?, ?)",
|
||||||
args: [userId, email, passwordHash, "email"],
|
args: [userId, email, passwordHash, "email"]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create JWT token
|
// Create JWT token
|
||||||
@@ -385,7 +396,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
path: "/",
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: env.NODE_ENV === "production",
|
secure: env.NODE_ENV === "production",
|
||||||
sameSite: "lax",
|
sameSite: "lax"
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true, message: "success" };
|
return { success: true, message: "success" };
|
||||||
@@ -393,7 +404,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
console.error("Registration error:", e);
|
console.error("Registration error:", e);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "duplicate",
|
message: "duplicate"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -404,8 +415,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
rememberMe: z.boolean().optional(),
|
rememberMe: z.boolean().optional()
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { email, password, rememberMe } = input;
|
const { email, password, rememberMe } = input;
|
||||||
@@ -413,13 +424,13 @@ export const authRouter = createTRPCRouter({
|
|||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE email = ? AND provider = ?",
|
sql: "SELECT * FROM User WHERE email = ? AND provider = ?",
|
||||||
args: [email, "email"],
|
args: [email, "email"]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "no-match",
|
message: "no-match"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +439,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (!user.password_hash) {
|
if (!user.password_hash) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "no-match",
|
message: "no-match"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +448,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (!passwordMatch) {
|
if (!passwordMatch) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "no-match",
|
message: "no-match"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +461,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
path: "/",
|
path: "/",
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: env.NODE_ENV === "production",
|
secure: env.NODE_ENV === "production",
|
||||||
sameSite: "lax",
|
sameSite: "lax"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
@@ -467,21 +478,24 @@ export const authRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
rememberMe: z.boolean().optional(),
|
rememberMe: z.boolean().optional()
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { email, rememberMe } = input;
|
const { email, rememberMe } = input;
|
||||||
|
|
||||||
// Check rate limiting
|
// Check rate limiting
|
||||||
const requested = getCookie(ctx.event.nativeEvent, "emailLoginLinkRequested");
|
const requested = getCookie(
|
||||||
|
ctx.event.nativeEvent,
|
||||||
|
"emailLoginLinkRequested"
|
||||||
|
);
|
||||||
if (requested) {
|
if (requested) {
|
||||||
const expires = new Date(requested);
|
const expires = new Date(requested);
|
||||||
const remaining = expires.getTime() - Date.now();
|
const remaining = expires.getTime() - Date.now();
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "TOO_MANY_REQUESTS",
|
code: "TOO_MANY_REQUESTS",
|
||||||
message: "countdown not expired",
|
message: "countdown not expired"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,25 +503,28 @@ export const authRouter = createTRPCRouter({
|
|||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE email = ?",
|
sql: "SELECT * FROM User WHERE email = ?",
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create JWT token for email link (15min expiry)
|
// Create JWT token for email link (15min expiry)
|
||||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const token = await new SignJWT({ email, rememberMe: rememberMe ?? false })
|
const token = await new SignJWT({
|
||||||
|
email,
|
||||||
|
rememberMe: rememberMe ?? false
|
||||||
|
})
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
.setExpirationTime("15m")
|
.setExpirationTime("15m")
|
||||||
.sign(secret);
|
.sign(secret);
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
const domain = env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN;
|
const domain = env.VITE_DOMAIN || "https://freno.me";
|
||||||
const htmlContent = `<html>
|
const htmlContent = `<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
@@ -550,10 +567,15 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// Set rate limit cookie (2 minutes)
|
// Set rate limit cookie (2 minutes)
|
||||||
const exp = new Date(Date.now() + 2 * 60 * 1000);
|
const exp = new Date(Date.now() + 2 * 60 * 1000);
|
||||||
setCookie(ctx.event.nativeEvent, "emailLoginLinkRequested", exp.toUTCString(), {
|
setCookie(
|
||||||
|
ctx.event.nativeEvent,
|
||||||
|
"emailLoginLinkRequested",
|
||||||
|
exp.toUTCString(),
|
||||||
|
{
|
||||||
maxAge: 2 * 60,
|
maxAge: 2 * 60,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return { success: true, message: "email sent" };
|
return { success: true, message: "email sent" };
|
||||||
}),
|
}),
|
||||||
@@ -565,14 +587,17 @@ export const authRouter = createTRPCRouter({
|
|||||||
const { email } = input;
|
const { email } = input;
|
||||||
|
|
||||||
// Check rate limiting
|
// Check rate limiting
|
||||||
const requested = getCookie(ctx.event.nativeEvent, "passwordResetRequested");
|
const requested = getCookie(
|
||||||
|
ctx.event.nativeEvent,
|
||||||
|
"passwordResetRequested"
|
||||||
|
);
|
||||||
if (requested) {
|
if (requested) {
|
||||||
const expires = new Date(requested);
|
const expires = new Date(requested);
|
||||||
const remaining = expires.getTime() - Date.now();
|
const remaining = expires.getTime() - Date.now();
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "TOO_MANY_REQUESTS",
|
code: "TOO_MANY_REQUESTS",
|
||||||
message: "countdown not expired",
|
message: "countdown not expired"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -580,7 +605,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE email = ?",
|
sql: "SELECT * FROM User WHERE email = ?",
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
@@ -598,7 +623,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
.sign(secret);
|
.sign(secret);
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
const domain = env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN;
|
const domain = env.VITE_DOMAIN || "https://freno.me";
|
||||||
const htmlContent = `<html>
|
const htmlContent = `<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
@@ -641,10 +666,15 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// Set rate limit cookie (5 minutes)
|
// Set rate limit cookie (5 minutes)
|
||||||
const exp = new Date(Date.now() + 5 * 60 * 1000);
|
const exp = new Date(Date.now() + 5 * 60 * 1000);
|
||||||
setCookie(ctx.event.nativeEvent, "passwordResetRequested", exp.toUTCString(), {
|
setCookie(
|
||||||
|
ctx.event.nativeEvent,
|
||||||
|
"passwordResetRequested",
|
||||||
|
exp.toUTCString(),
|
||||||
|
{
|
||||||
maxAge: 5 * 60,
|
maxAge: 5 * 60,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return { success: true, message: "email sent" };
|
return { success: true, message: "email sent" };
|
||||||
}),
|
}),
|
||||||
@@ -655,8 +685,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
newPassword: z.string().min(8),
|
newPassword: z.string().min(8),
|
||||||
newPasswordConfirmation: z.string().min(8),
|
newPasswordConfirmation: z.string().min(8)
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { token, newPassword, newPasswordConfirmation } = input;
|
const { token, newPassword, newPasswordConfirmation } = input;
|
||||||
@@ -664,7 +694,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (newPassword !== newPasswordConfirmation) {
|
if (newPassword !== newPasswordConfirmation) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Password Mismatch",
|
message: "Password Mismatch"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +706,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (!payload.id || typeof payload.id !== "string") {
|
if (!payload.id || typeof payload.id !== "string") {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "bad token",
|
message: "bad token"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,17 +715,17 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
||||||
args: [passwordHash, payload.id],
|
args: [passwordHash, payload.id]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear any session cookies
|
// Clear any session cookies
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true, message: "success" };
|
return { success: true, message: "success" };
|
||||||
@@ -706,7 +736,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
console.error("Password reset error:", error);
|
console.error("Password reset error:", error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "token expired",
|
message: "token expired"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -718,7 +748,10 @@ export const authRouter = createTRPCRouter({
|
|||||||
const { email } = input;
|
const { email } = input;
|
||||||
|
|
||||||
// Check rate limiting
|
// Check rate limiting
|
||||||
const requested = getCookie(ctx.event.nativeEvent, "emailVerificationRequested");
|
const requested = getCookie(
|
||||||
|
ctx.event.nativeEvent,
|
||||||
|
"emailVerificationRequested"
|
||||||
|
);
|
||||||
if (requested) {
|
if (requested) {
|
||||||
const time = parseInt(requested);
|
const time = parseInt(requested);
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
@@ -727,7 +760,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (difference < 15) {
|
if (difference < 15) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "TOO_MANY_REQUESTS",
|
code: "TOO_MANY_REQUESTS",
|
||||||
message: "Please wait before requesting another verification email",
|
message: "Please wait before requesting another verification email"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -735,13 +768,13 @@ export const authRouter = createTRPCRouter({
|
|||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE email = ?",
|
sql: "SELECT * FROM User WHERE email = ?",
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +786,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
.sign(secret);
|
.sign(secret);
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
const domain = env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN;
|
const domain = env.VITE_DOMAIN || "https://freno.me";
|
||||||
const htmlContent = `<html>
|
const htmlContent = `<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<style>
|
||||||
@@ -792,10 +825,15 @@ export const authRouter = createTRPCRouter({
|
|||||||
await sendEmail(email, "freno.me email verification", htmlContent);
|
await sendEmail(email, "freno.me email verification", htmlContent);
|
||||||
|
|
||||||
// Set rate limit cookie
|
// Set rate limit cookie
|
||||||
setCookie(ctx.event.nativeEvent, "emailVerificationRequested", Date.now().toString(), {
|
setCookie(
|
||||||
|
ctx.event.nativeEvent,
|
||||||
|
"emailVerificationRequested",
|
||||||
|
Date.now().toString(),
|
||||||
|
{
|
||||||
maxAge: 15 * 60,
|
maxAge: 15 * 60,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return { success: true, message: "Verification email sent" };
|
return { success: true, message: "Verification email sent" };
|
||||||
}),
|
}),
|
||||||
@@ -804,13 +842,14 @@ export const authRouter = createTRPCRouter({
|
|||||||
signOut: publicProcedure.mutation(async ({ ctx }) => {
|
signOut: publicProcedure.mutation(async ({ ctx }) => {
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/",
|
path: "/"
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createTRPCRouter, publicProcedure } from "../utils";
|
import { createTRPCRouter, publicProcedure } from "../utils";
|
||||||
import { ConnectionFactory } from "~/server/utils";
|
import { ConnectionFactory } from "~/server/utils";
|
||||||
|
import { withCache } from "~/server/cache";
|
||||||
|
|
||||||
// Simple in-memory cache for blog posts to reduce DB load
|
// Simple in-memory cache for blog posts to reduce DB load
|
||||||
let cachedPosts: {
|
let cachedPosts: {
|
||||||
@@ -11,6 +12,7 @@ let cacheTimestamp: number = 0;
|
|||||||
|
|
||||||
export const blogRouter = createTRPCRouter({
|
export const blogRouter = createTRPCRouter({
|
||||||
getRecentPosts: publicProcedure.query(async () => {
|
getRecentPosts: publicProcedure.query(async () => {
|
||||||
|
return withCache("recent-posts", 10 * 60 * 1000, async () => {
|
||||||
// Get database connection
|
// Get database connection
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ export const blogRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const results = await conn.execute(query);
|
const results = await conn.execute(query);
|
||||||
return results.rows;
|
return results.rows;
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getPosts: publicProcedure.query(async ({ ctx }) => {
|
getPosts: publicProcedure.query(async ({ ctx }) => {
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const fullURL = input.banner_photo
|
const fullURL = input.banner_photo
|
||||||
? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo
|
? env.VITE_AWS_BUCKET_STRING + input.banner_photo
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
@@ -346,7 +346,7 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
if (input.banner_photo === "_DELETE_IMAGE_") {
|
if (input.banner_photo === "_DELETE_IMAGE_") {
|
||||||
params.push(null);
|
params.push(null);
|
||||||
} else {
|
} else {
|
||||||
params.push(env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo);
|
params.push(env.VITE_AWS_BUCKET_STRING + input.banner_photo);
|
||||||
}
|
}
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
@@ -593,7 +593,7 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const fullURL = input.imageURL
|
const fullURL = input.imageURL
|
||||||
? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.imageURL
|
? env.VITE_AWS_BUCKET_STRING + input.imageURL
|
||||||
: null;
|
: null;
|
||||||
const query = `UPDATE User SET image = ? WHERE id = ?`;
|
const query = `UPDATE User SET image = ? WHERE id = ?`;
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, publicProcedure } from "../utils";
|
import { createTRPCRouter, publicProcedure } from "../utils";
|
||||||
import { env } from "~/env/server";
|
import { env } from "~/env/server";
|
||||||
|
import { withCache } from "~/server/cache";
|
||||||
|
|
||||||
// Types for commits
|
// Types for commits
|
||||||
interface GitCommit {
|
interface GitCommit {
|
||||||
@@ -22,6 +23,10 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
getGitHubCommits: publicProcedure
|
getGitHubCommits: publicProcedure
|
||||||
.input(z.object({ limit: z.number().default(3) }))
|
.input(z.object({ limit: z.number().default(3) }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
|
return withCache(
|
||||||
|
`github-commits-${input.limit}`,
|
||||||
|
10 * 60 * 1000, // 10 minutes
|
||||||
|
async () => {
|
||||||
try {
|
try {
|
||||||
// Get user's repositories sorted by most recently pushed
|
// Get user's repositories sorted by most recently pushed
|
||||||
const reposResponse = await fetch(
|
const reposResponse = await fetch(
|
||||||
@@ -69,13 +74,15 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
allCommits.push({
|
allCommits.push({
|
||||||
sha: commit.sha?.substring(0, 7) || "unknown",
|
sha: commit.sha?.substring(0, 7) || "unknown",
|
||||||
message:
|
message:
|
||||||
commit.commit?.message?.split("\n")[0] || "No message",
|
commit.commit?.message?.split("\n")[0] ||
|
||||||
|
"No message",
|
||||||
author:
|
author:
|
||||||
commit.commit?.author?.name ||
|
commit.commit?.author?.name ||
|
||||||
commit.author?.login ||
|
commit.author?.login ||
|
||||||
"Unknown",
|
"Unknown",
|
||||||
date:
|
date:
|
||||||
commit.commit?.author?.date || new Date().toISOString(),
|
commit.commit?.author?.date ||
|
||||||
|
new Date().toISOString(),
|
||||||
repo: repo.full_name,
|
repo: repo.full_name,
|
||||||
url: `https://github.com/${repo.full_name}/commit/${commit.sha}`
|
url: `https://github.com/${repo.full_name}/commit/${commit.sha}`
|
||||||
});
|
});
|
||||||
@@ -100,12 +107,18 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
console.error("Error fetching GitHub commits:", error);
|
console.error("Error fetching GitHub commits:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Get recent commits from Gitea
|
// Get recent commits from Gitea
|
||||||
getGiteaCommits: publicProcedure
|
getGiteaCommits: publicProcedure
|
||||||
.input(z.object({ limit: z.number().default(3) }))
|
.input(z.object({ limit: z.number().default(3) }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
|
return withCache(
|
||||||
|
`gitea-commits-${input.limit}`,
|
||||||
|
10 * 60 * 1000, // 10 minutes
|
||||||
|
async () => {
|
||||||
try {
|
try {
|
||||||
// First, get user's repositories
|
// First, get user's repositories
|
||||||
const reposResponse = await fetch(
|
const reposResponse = await fetch(
|
||||||
@@ -119,7 +132,9 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!reposResponse.ok) {
|
if (!reposResponse.ok) {
|
||||||
throw new Error(`Gitea repos API error: ${reposResponse.statusText}`);
|
throw new Error(
|
||||||
|
`Gitea repos API error: ${reposResponse.statusText}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const repos = await reposResponse.json();
|
const repos = await reposResponse.json();
|
||||||
@@ -145,7 +160,9 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
for (const commit of commits) {
|
for (const commit of commits) {
|
||||||
if (
|
if (
|
||||||
(commit.commit?.author?.email &&
|
(commit.commit?.author?.email &&
|
||||||
commit.commit.author.email.includes("michael@freno.me")) ||
|
commit.commit.author.email.includes(
|
||||||
|
"michael@freno.me"
|
||||||
|
)) ||
|
||||||
commit.commit.author.email.includes(
|
commit.commit.author.email.includes(
|
||||||
"michaelt.freno@gmail.com"
|
"michaelt.freno@gmail.com"
|
||||||
) // Filter for your commits
|
) // Filter for your commits
|
||||||
@@ -153,10 +170,12 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
allCommits.push({
|
allCommits.push({
|
||||||
sha: commit.sha?.substring(0, 7) || "unknown",
|
sha: commit.sha?.substring(0, 7) || "unknown",
|
||||||
message:
|
message:
|
||||||
commit.commit?.message?.split("\n")[0] || "No message",
|
commit.commit?.message?.split("\n")[0] ||
|
||||||
|
"No message",
|
||||||
author: commit.commit?.author?.name || repo.owner.login,
|
author: commit.commit?.author?.name || repo.owner.login,
|
||||||
date:
|
date:
|
||||||
commit.commit?.author?.date || new Date().toISOString(),
|
commit.commit?.author?.date ||
|
||||||
|
new Date().toISOString(),
|
||||||
repo: repo.full_name,
|
repo: repo.full_name,
|
||||||
url: `${env.GITEA_URL}/${repo.full_name}/commit/${commit.sha}`
|
url: `${env.GITEA_URL}/${repo.full_name}/commit/${commit.sha}`
|
||||||
});
|
});
|
||||||
@@ -164,7 +183,10 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching commits for ${repo.name}:`, error);
|
console.error(
|
||||||
|
`Error fetching commits for ${repo.name}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,10 +200,13 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
console.error("Error fetching Gitea commits:", error);
|
console.error("Error fetching Gitea commits:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Get GitHub contribution activity (for heatmap)
|
// Get GitHub contribution activity (for heatmap)
|
||||||
getGitHubActivity: publicProcedure.query(async () => {
|
getGitHubActivity: publicProcedure.query(async () => {
|
||||||
|
return withCache("github-activity", 10 * 60 * 1000, async () => {
|
||||||
try {
|
try {
|
||||||
// Use GitHub GraphQL API for contribution data
|
// Use GitHub GraphQL API for contribution data
|
||||||
const query = `
|
const query = `
|
||||||
@@ -227,8 +252,8 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
// Extract contribution days from the response
|
// Extract contribution days from the response
|
||||||
const contributions: ContributionDay[] = [];
|
const contributions: ContributionDay[] = [];
|
||||||
const weeks =
|
const weeks =
|
||||||
data.data?.user?.contributionsCollection?.contributionCalendar?.weeks ||
|
data.data?.user?.contributionsCollection?.contributionCalendar
|
||||||
[];
|
?.weeks || [];
|
||||||
|
|
||||||
for (const week of weeks) {
|
for (const week of weeks) {
|
||||||
for (const day of week.contributionDays) {
|
for (const day of week.contributionDays) {
|
||||||
@@ -244,10 +269,12 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
console.error("Error fetching GitHub activity:", error);
|
console.error("Error fetching GitHub activity:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Get Gitea contribution activity (for heatmap)
|
// Get Gitea contribution activity (for heatmap)
|
||||||
getGiteaActivity: publicProcedure.query(async () => {
|
getGiteaActivity: publicProcedure.query(async () => {
|
||||||
|
return withCache("gitea-activity", 10 * 60 * 1000, async () => {
|
||||||
try {
|
try {
|
||||||
// Get user's repositories
|
// Get user's repositories
|
||||||
const reposResponse = await fetch(
|
const reposResponse = await fetch(
|
||||||
@@ -310,5 +337,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
console.error("Error fetching Gitea activity:", error);
|
console.error("Error fetching Gitea activity:", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { createTRPCRouter, publicProcedure } from "../../utils";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
LineageConnectionFactory,
|
LineageConnectionFactory,
|
||||||
validateLineageRequest,
|
validateLineageRequest,
|
||||||
dumpAndSendDB,
|
dumpAndSendDB
|
||||||
} from "~/server/utils";
|
} from "~/server/utils";
|
||||||
import { env } from "~/env/server";
|
import { env } from "~/env/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { OAuth2Client } from "google-auth-library";
|
import { OAuth2Client } from "google-auth-library";
|
||||||
import { jwtVerify } from "jose";
|
import { jwtVerify } from "jose";
|
||||||
|
import { createTRPCRouter, publicProcedure } from "~/server/api/utils";
|
||||||
|
|
||||||
export const lineageDatabaseRouter = createTRPCRouter({
|
export const lineageDatabaseRouter = createTRPCRouter({
|
||||||
credentials: publicProcedure
|
credentials: publicProcedure
|
||||||
@@ -16,7 +16,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
provider: z.enum(["email", "google", "apple"]),
|
provider: z.enum(["email", "google", "apple"]),
|
||||||
authToken: z.string(),
|
authToken: z.string()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -32,17 +32,17 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
valid_request = true;
|
valid_request = true;
|
||||||
}
|
}
|
||||||
} else if (provider === "google") {
|
} else if (provider === "google") {
|
||||||
const CLIENT_ID = env.VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE || env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE;
|
const CLIENT_ID = env.VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE;
|
||||||
if (!CLIENT_ID) {
|
if (!CLIENT_ID) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Google client ID not configured",
|
message: "Google client ID not configured"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const client = new OAuth2Client(CLIENT_ID);
|
const client = new OAuth2Client(CLIENT_ID);
|
||||||
const ticket = await client.verifyIdToken({
|
const ticket = await client.verifyIdToken({
|
||||||
idToken: authToken,
|
idToken: authToken,
|
||||||
audience: CLIENT_ID,
|
audience: CLIENT_ID
|
||||||
});
|
});
|
||||||
if (ticket.getPayload()?.email === email) {
|
if (ticket.getPayload()?.email === email) {
|
||||||
valid_request = true;
|
valid_request = true;
|
||||||
@@ -67,25 +67,25 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
db_name: user.database_name as string,
|
db_name: user.database_name as string,
|
||||||
db_token: user.database_token as string,
|
db_token: user.database_token as string
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "No user found",
|
message: "No user found"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Invalid credentials",
|
message: "Invalid credentials"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TRPCError) throw error;
|
if (error instanceof TRPCError) throw error;
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Authentication failed",
|
message: "Authentication failed"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -98,35 +98,42 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
db_token: z.string(),
|
db_token: z.string(),
|
||||||
authToken: z.string(),
|
authToken: z.string(),
|
||||||
skip_cron: z.boolean().optional(),
|
skip_cron: z.boolean().optional(),
|
||||||
send_dump_target: z.string().email().optional(),
|
send_dump_target: z.string().email().optional()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { email, db_name, db_token, authToken, skip_cron, send_dump_target } = input;
|
const {
|
||||||
|
email,
|
||||||
|
db_name,
|
||||||
|
db_token,
|
||||||
|
authToken,
|
||||||
|
skip_cron,
|
||||||
|
send_dump_target
|
||||||
|
} = input;
|
||||||
|
|
||||||
const conn = LineageConnectionFactory();
|
const conn = LineageConnectionFactory();
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: `SELECT * FROM User WHERE email = ?`,
|
sql: `SELECT * FROM User WHERE email = ?`,
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
const userRow = res.rows[0];
|
const userRow = res.rows[0];
|
||||||
|
|
||||||
if (!userRow) {
|
if (!userRow) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "User not found"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = await validateLineageRequest({
|
const valid = await validateLineageRequest({
|
||||||
auth_token: authToken,
|
auth_token: authToken,
|
||||||
userRow,
|
userRow
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Invalid Verification",
|
message: "Invalid Verification"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +142,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
if (database_token !== db_token || database_name !== db_name) {
|
if (database_token !== db_token || database_name !== db_name) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Incorrect Verification",
|
message: "Incorrect Verification"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +151,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
const dumpRes = await dumpAndSendDB({
|
const dumpRes = await dumpAndSendDB({
|
||||||
dbName: db_name,
|
dbName: db_name,
|
||||||
dbToken: db_token,
|
dbToken: db_token,
|
||||||
sendTarget: send_dump_target,
|
sendTarget: send_dump_target
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dumpRes.success) {
|
if (dumpRes.success) {
|
||||||
@@ -153,31 +160,31 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
|
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (deleteRes.ok) {
|
if (deleteRes.ok) {
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: `DELETE FROM User WHERE email = ?`,
|
sql: `DELETE FROM User WHERE email = ?`,
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
message: `Account and Database deleted, db dump sent to email: ${send_dump_target}`,
|
message: `Account and Database deleted, db dump sent to email: ${send_dump_target}`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to delete database",
|
message: "Failed to delete database"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: dumpRes.reason || "Failed to dump database",
|
message: dumpRes.reason || "Failed to dump database"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -186,44 +193,44 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
|
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (deleteRes.ok) {
|
if (deleteRes.ok) {
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: `DELETE FROM User WHERE email = ?`,
|
sql: `DELETE FROM User WHERE email = ?`,
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
message: `Account and Database deleted`,
|
message: `Account and Database deleted`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to delete database",
|
message: "Failed to delete database"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const insertRes = await conn.execute({
|
const insertRes = await conn.execute({
|
||||||
sql: `INSERT INTO cron (email, db_name, db_token, send_dump_target) VALUES (?, ?, ?, ?)`,
|
sql: `INSERT INTO cron (email, db_name, db_token, send_dump_target) VALUES (?, ?, ?, ?)`,
|
||||||
args: [email, db_name, db_token, send_dump_target],
|
args: [email, db_name, db_token, send_dump_target]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (insertRes.rowsAffected > 0) {
|
if (insertRes.rowsAffected > 0) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
message: `Deletion scheduled.`,
|
message: `Deletion scheduled.`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: `Deletion not scheduled, due to server failure`,
|
message: `Deletion not scheduled, due to server failure`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +245,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: `SELECT * FROM cron WHERE email = ?`,
|
sql: `SELECT * FROM cron WHERE email = ?`,
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
const cronRow = res.rows[0];
|
const cronRow = res.rows[0];
|
||||||
|
|
||||||
@@ -249,12 +256,12 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
created_at: cronRow.created_at as string,
|
created_at: cronRow.created_at as string
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to check deletion status",
|
message: "Failed to check deletion status"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -263,7 +270,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
authToken: z.string(),
|
authToken: z.string()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -273,13 +280,13 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const resUser = await conn.execute({
|
const resUser = await conn.execute({
|
||||||
sql: `SELECT * FROM User WHERE email = ?;`,
|
sql: `SELECT * FROM User WHERE email = ?;`,
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resUser.rows.length === 0) {
|
if (resUser.rows.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found.",
|
message: "User not found."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,31 +294,31 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const valid = await validateLineageRequest({
|
const valid = await validateLineageRequest({
|
||||||
auth_token: authToken,
|
auth_token: authToken,
|
||||||
userRow,
|
userRow
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Invalid credentials for cancelation.",
|
message: "Invalid credentials for cancelation."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await conn.execute({
|
const result = await conn.execute({
|
||||||
sql: `DELETE FROM cron WHERE email = ?;`,
|
sql: `DELETE FROM cron WHERE email = ?;`,
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.rowsAffected > 0) {
|
if (result.rowsAffected > 0) {
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "Cron job(s) canceled successfully.",
|
message: "Cron job(s) canceled successfully."
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "No cron job found for the given email.",
|
message: "No cron job found for the given email."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -332,7 +339,7 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
const dumpRes = await dumpAndSendDB({
|
const dumpRes = await dumpAndSendDB({
|
||||||
dbName: db_name as string,
|
dbName: db_name as string,
|
||||||
dbToken: db_token as string,
|
dbToken: db_token as string,
|
||||||
sendTarget: send_dump_target as string,
|
sendTarget: send_dump_target as string
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dumpRes.success) {
|
if (dumpRes.success) {
|
||||||
@@ -341,15 +348,15 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
|
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (deleteRes.ok) {
|
if (deleteRes.ok) {
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: `DELETE FROM User WHERE email = ?`,
|
sql: `DELETE FROM User WHERE email = ?`,
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
executed_ids.push(id as number);
|
executed_ids.push(id as number);
|
||||||
}
|
}
|
||||||
@@ -360,15 +367,15 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`,
|
Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (deleteRes.ok) {
|
if (deleteRes.ok) {
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: `DELETE FROM User WHERE email = ?`,
|
sql: `DELETE FROM User WHERE email = ?`,
|
||||||
args: [email],
|
args: [email]
|
||||||
});
|
});
|
||||||
executed_ids.push(id as number);
|
executed_ids.push(id as number);
|
||||||
}
|
}
|
||||||
@@ -383,11 +390,11 @@ export const lineageDatabaseRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
message:
|
message:
|
||||||
"Processed databases deleted and corresponding cron rows removed.",
|
"Processed databases deleted and corresponding cron rows removed."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status: 200, ok: true };
|
return { status: 200, ok: true };
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ export async function getPrivilegeLevel(
|
|||||||
|
|
||||||
if (userIDToken) {
|
if (userIDToken) {
|
||||||
try {
|
try {
|
||||||
const secret = new TextEncoder().encode(env().JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const { payload } = await jwtVerify(userIDToken, secret);
|
const { payload } = await jwtVerify(userIDToken, secret);
|
||||||
|
|
||||||
if (payload.id && typeof payload.id === "string") {
|
if (payload.id && typeof payload.id === "string") {
|
||||||
return payload.id === env().ADMIN_ID ? "admin" : "user";
|
return payload.id === env.ADMIN_ID ? "admin" : "user";
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Failed to authenticate token.");
|
console.log("Failed to authenticate token.");
|
||||||
@@ -38,7 +38,7 @@ export async function getUserID(event: H3Event): Promise<string | null> {
|
|||||||
|
|
||||||
if (userIDToken) {
|
if (userIDToken) {
|
||||||
try {
|
try {
|
||||||
const secret = new TextEncoder().encode(env().JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const { payload } = await jwtVerify(userIDToken, secret);
|
const { payload } = await jwtVerify(userIDToken, secret);
|
||||||
|
|
||||||
if (payload.id && typeof payload.id === "string") {
|
if (payload.id && typeof payload.id === "string") {
|
||||||
@@ -79,7 +79,7 @@ export async function validateLineageRequest({
|
|||||||
const { provider, email } = userRow;
|
const { provider, email } = userRow;
|
||||||
if (provider === "email") {
|
if (provider === "email") {
|
||||||
try {
|
try {
|
||||||
const secret = new TextEncoder().encode(env().JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const { payload } = await jwtVerify(auth_token, secret);
|
const { payload } = await jwtVerify(auth_token, secret);
|
||||||
if (email !== payload.email) {
|
if (email !== payload.email) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
54
src/server/cache.ts
Normal file
54
src/server/cache.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
interface CacheEntry<T> {
|
||||||
|
data: T;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleCache {
|
||||||
|
private cache: Map<string, CacheEntry<any>> = new Map();
|
||||||
|
|
||||||
|
get<T>(key: string, ttlMs: number): T | null {
|
||||||
|
const entry = this.cache.get(key);
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - entry.timestamp > ttlMs) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
set<T>(key: string, data: T): void {
|
||||||
|
this.cache.set(key, {
|
||||||
|
data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: string): void {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cache = new SimpleCache();
|
||||||
|
|
||||||
|
// Helper function to wrap async operations with caching
|
||||||
|
export async function withCache<T>(
|
||||||
|
key: string,
|
||||||
|
ttlMs: number,
|
||||||
|
fn: () => Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
|
const cached = cache.get<T>(key, ttlMs);
|
||||||
|
if (cached !== null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fn();
|
||||||
|
cache.set(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ let lineageDBConnection: ReturnType<typeof createClient> | null = null;
|
|||||||
export function ConnectionFactory() {
|
export function ConnectionFactory() {
|
||||||
if (!mainDBConnection) {
|
if (!mainDBConnection) {
|
||||||
const config = {
|
const config = {
|
||||||
url: env().TURSO_DB_URL,
|
url: env.TURSO_DB_URL,
|
||||||
authToken: env().TURSO_DB_TOKEN
|
authToken: env.TURSO_DB_TOKEN
|
||||||
};
|
};
|
||||||
mainDBConnection = createClient(config);
|
mainDBConnection = createClient(config);
|
||||||
}
|
}
|
||||||
@@ -21,8 +21,8 @@ export function ConnectionFactory() {
|
|||||||
export function LineageConnectionFactory() {
|
export function LineageConnectionFactory() {
|
||||||
if (!lineageDBConnection) {
|
if (!lineageDBConnection) {
|
||||||
const config = {
|
const config = {
|
||||||
url: env().TURSO_LINEAGE_URL,
|
url: env.TURSO_LINEAGE_URL,
|
||||||
authToken: env().TURSO_LINEAGE_TOKEN
|
authToken: env.TURSO_LINEAGE_TOKEN
|
||||||
};
|
};
|
||||||
lineageDBConnection = createClient(config);
|
lineageDBConnection = createClient(config);
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ export function LineageConnectionFactory() {
|
|||||||
export async function LineageDBInit() {
|
export async function LineageDBInit() {
|
||||||
const turso = createAPIClient({
|
const turso = createAPIClient({
|
||||||
org: "mikefreno",
|
org: "mikefreno",
|
||||||
token: env().TURSO_DB_API_TOKEN
|
token: env.TURSO_DB_API_TOKEN
|
||||||
});
|
});
|
||||||
|
|
||||||
const db_name = uuid();
|
const db_name = uuid();
|
||||||
@@ -95,7 +95,7 @@ export async function dumpAndSendDB({
|
|||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
const base64Content = Buffer.from(text, "utf-8").toString("base64");
|
const base64Content = Buffer.from(text, "utf-8").toString("base64");
|
||||||
|
|
||||||
const apiKey = env().SENDINBLUE_KEY as string;
|
const apiKey = env.SENDINBLUE_KEY as string;
|
||||||
const apiUrl = "https://api.brevo.com/v3/smtp/email";
|
const apiUrl = "https://api.brevo.com/v3/smtp/email";
|
||||||
|
|
||||||
const emailPayload = {
|
const emailPayload = {
|
||||||
|
|||||||
@@ -8,17 +8,16 @@ export async function sendEmailVerification(userEmail: string): Promise<{
|
|||||||
messageId?: string;
|
messageId?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
}> {
|
}> {
|
||||||
const apiKey = env().SENDINBLUE_KEY;
|
const apiKey = env.SENDINBLUE_KEY;
|
||||||
const apiUrl = "https://api.brevo.com/v3/smtp/email";
|
const apiUrl = "https://api.brevo.com/v3/smtp/email";
|
||||||
|
|
||||||
const secret = new TextEncoder().encode(env().JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const token = await new SignJWT({ email: userEmail })
|
const token = await new SignJWT({ email: userEmail })
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
.setExpirationTime("15m")
|
.setExpirationTime("15m")
|
||||||
.sign(secret);
|
.sign(secret);
|
||||||
|
|
||||||
const domain =
|
const domain = env.VITE_DOMAIN || "https://freno.me";
|
||||||
env().VITE_DOMAIN || env().NEXT_PUBLIC_DOMAIN || "https://freno.me";
|
|
||||||
|
|
||||||
const emailPayload = {
|
const emailPayload = {
|
||||||
sender: {
|
sender: {
|
||||||
|
|||||||
Reference in New Issue
Block a user