mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-12 00:38:28 +00:00
Feat: ingest api
This commit is contained in:
parent
dcf10aeb71
commit
8312cc342d
@ -1,45 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Paragraph } from "../ui/Paragraph";
|
||||
import { Header3 } from "../ui/Headers";
|
||||
import { Button } from "../ui";
|
||||
import Logo from "../logo/logo";
|
||||
import { Theme, useTheme } from "remix-themes";
|
||||
|
||||
interface QuoteType {
|
||||
quote: string;
|
||||
}
|
||||
|
||||
const quotes: QuoteType[] = [
|
||||
{
|
||||
quote:
|
||||
"Recall remembers that I prefer emails in dark mode and hate promotional content. It automatically filters and formats my communications just the way I like.",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"When I mention liking Nike's latest running shoes, Recall remembers this preference and helps surface relevant product launches and deals across my browsing.",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"Echo knows I'm a vegetarian and helps filter restaurant recommendations and recipes accordingly, without me having to specify it every time.",
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"By remembering that I prefer technical documentation with code examples, Echo helps prioritize learning resources that match my learning style.",
|
||||
},
|
||||
];
|
||||
|
||||
export function LoginPageLayout({ children }: { children: React.ReactNode }) {
|
||||
const [randomQuote, setRandomQuote] = useState<QuoteType | null>(null);
|
||||
useEffect(() => {
|
||||
const randomIndex = Math.floor(Math.random() * quotes.length);
|
||||
setRandomQuote(quotes[randomIndex]);
|
||||
}, []);
|
||||
const [, setTheme] = useTheme();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center">
|
||||
<div className="pt-8">
|
||||
<Logo width={20} height={20} />
|
||||
<Logo width={5} height={5} />
|
||||
<Button onClick={() => setTheme(Theme.DARK)}>theme</Button>
|
||||
</div>
|
||||
|
||||
|
||||
13
apps/webapp/app/components/ui/Fieldset.tsx
Normal file
13
apps/webapp/app/components/ui/Fieldset.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export function Fieldset({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-y-5", className)}>{children}</div>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Prisma, User } from "@recall/database";
|
||||
import type { GoogleProfile } from "remix-auth-google";
|
||||
import type { GoogleProfile } from "@coji/remix-auth-google";
|
||||
import { prisma } from "~/db.server";
|
||||
export type { User } from "@recall/database";
|
||||
|
||||
|
||||
26
apps/webapp/app/routes/auth.google.callback.tsx
Normal file
26
apps/webapp/app/routes/auth.google.callback.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { redirect, type LoaderFunction } from "@remix-run/node";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
import { redirectCookie } from "./auth.google";
|
||||
import { logger } from "~/services/logger.service";
|
||||
import { saveSession } from "~/services/sessionStorage.server";
|
||||
|
||||
export let loader: LoaderFunction = async ({ request }) => {
|
||||
const cookie = request.headers.get("Cookie");
|
||||
const redirectValue = await redirectCookie.parse(cookie);
|
||||
const redirectTo = redirectValue ?? "/";
|
||||
|
||||
logger.debug("auth.google.callback loader", {
|
||||
redirectTo,
|
||||
});
|
||||
|
||||
const authuser = await authenticator.authenticate("google", request);
|
||||
const headers = await saveSession(request, authuser);
|
||||
|
||||
logger.debug("auth.google.callback authuser", {
|
||||
authuser,
|
||||
});
|
||||
|
||||
return redirect(redirectTo, {
|
||||
headers,
|
||||
});
|
||||
};
|
||||
39
apps/webapp/app/routes/auth.google.tsx
Normal file
39
apps/webapp/app/routes/auth.google.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import {
|
||||
redirect,
|
||||
createCookie,
|
||||
type ActionFunction,
|
||||
type LoaderFunction,
|
||||
} from "@remix-run/node";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
|
||||
export let loader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const redirectTo = url.searchParams.get("redirectTo");
|
||||
|
||||
try {
|
||||
// call authenticate as usual, in successRedirect use returnTo or a fallback
|
||||
const rf = await authenticator.authenticate("google", request);
|
||||
console.log(rf);
|
||||
|
||||
return rf;
|
||||
} catch (error) {
|
||||
// here we catch anything authenticator.authenticate throw, this will
|
||||
// include redirects
|
||||
// if the error is a Response and is a redirect
|
||||
if (error instanceof Response) {
|
||||
// we need to append a Set-Cookie header with a cookie storing the
|
||||
// returnTo value
|
||||
error.headers.append(
|
||||
"Set-Cookie",
|
||||
await redirectCookie.serialize(redirectTo),
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const redirectCookie = createCookie("redirect-to", {
|
||||
maxAge: 60 * 60, // 1 hour
|
||||
httpOnly: true,
|
||||
});
|
||||
@ -1,31 +1,71 @@
|
||||
import { type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useNavigation } from "@remix-run/react";
|
||||
import { typedjson } from "remix-typedjson";
|
||||
import { Form } from "@remix-run/react";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { LoginPageLayout } from "~/components/layout/LoginPageLayout";
|
||||
import { authenticator } from "~/services/auth.server";
|
||||
import {
|
||||
commitSession,
|
||||
getUserSession,
|
||||
} from "~/services/sessionStorage.server";
|
||||
import { Fieldset } from "~/components/ui/Fieldset";
|
||||
import { Header1 } from "~/components/ui/Headers";
|
||||
import { Paragraph } from "~/components/ui/Paragraph";
|
||||
import { isGoogleAuthSupported } from "~/services/auth.server";
|
||||
import { setRedirectTo } from "~/services/redirectTo.server";
|
||||
import { getUserId } from "~/services/session.server";
|
||||
import { commitSession } from "~/services/sessionStorage.server";
|
||||
import { requestUrl } from "~/utils/requestUrl.server";
|
||||
|
||||
import { RiGoogleLine } from "@remixicon/react";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
await authenticator.isAuthenticated(request, {
|
||||
successRedirect: "/",
|
||||
});
|
||||
const userId = await getUserId(request);
|
||||
if (userId) return redirect("/");
|
||||
|
||||
const session = await getUserSession(request);
|
||||
const url = requestUrl(request);
|
||||
const redirectTo = url.searchParams.get("redirectTo");
|
||||
|
||||
return typedjson({
|
||||
headers: { "Set-Cookie": await commitSession(session) },
|
||||
});
|
||||
if (redirectTo) {
|
||||
const session = await setRedirectTo(request, redirectTo);
|
||||
|
||||
return typedjson(
|
||||
{ redirectTo, showGoogleAuth: isGoogleAuthSupported },
|
||||
{
|
||||
headers: {
|
||||
"Set-Cookie": await commitSession(session),
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return typedjson({
|
||||
redirectTo: null,
|
||||
showGoogleAuth: isGoogleAuthSupported,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigation();
|
||||
const data = useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<LoginPageLayout>
|
||||
<h2>Lohin</h2>
|
||||
</LoginPageLayout>
|
||||
<Form
|
||||
action={`/auth/google${data.redirectTo ? `?redirectTo=${data.redirectTo}` : ""}`}
|
||||
method="GET"
|
||||
className="w-full"
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<Header1 className="pb-4 font-semibold sm:text-2xl md:text-3xl lg:text-4xl">
|
||||
Welcome
|
||||
</Header1>
|
||||
<Paragraph variant="base" className="mb-6">
|
||||
Create an account or login
|
||||
</Paragraph>
|
||||
<Fieldset className="w-full">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{data.showGoogleAuth && (
|
||||
<button type="submit" data-action="continue with google">
|
||||
<RiGoogleLine className={"mr-2 size-5"} />
|
||||
<span className="text-text-bright">Continue with Google</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Fieldset>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ const ParamsSchema = z.object({
|
||||
export const IngestBodyRequest = z.object({
|
||||
name: z.string(),
|
||||
episodeBody: z.string(),
|
||||
referenceTime: z.date(),
|
||||
referenceTime: z.string(),
|
||||
type: z.enum(["CONVERSATION", "TEXT"]), // Assuming these are the EpisodeType values
|
||||
source: z.string(),
|
||||
userId: z.string(),
|
||||
|
||||
@ -2,19 +2,23 @@ import { Authenticator } from "remix-auth";
|
||||
import type { AuthUser } from "./authUser";
|
||||
|
||||
import { addGoogleStrategy } from "./googleAuth.server";
|
||||
import { sessionStorage } from "./sessionStorage.server";
|
||||
|
||||
import { env } from "~/env.server";
|
||||
|
||||
// Create an instance of the authenticator, pass a generic with what
|
||||
// strategies will return and will store in the session
|
||||
const authenticator = new Authenticator<AuthUser>(sessionStorage);
|
||||
const authenticator = new Authenticator<AuthUser>();
|
||||
|
||||
const isGoogleAuthSupported =
|
||||
typeof env.AUTH_GOOGLE_CLIENT_ID === "string" &&
|
||||
typeof env.AUTH_GOOGLE_CLIENT_SECRET === "string";
|
||||
|
||||
if (env.AUTH_GOOGLE_CLIENT_ID && env.AUTH_GOOGLE_CLIENT_SECRET) {
|
||||
addGoogleStrategy(authenticator, env.AUTH_GOOGLE_CLIENT_ID, env.AUTH_GOOGLE_CLIENT_SECRET);
|
||||
addGoogleStrategy(
|
||||
authenticator,
|
||||
env.AUTH_GOOGLE_CLIENT_ID,
|
||||
env.AUTH_GOOGLE_CLIENT_SECRET,
|
||||
);
|
||||
}
|
||||
|
||||
export { authenticator, isGoogleAuthSupported };
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Authenticator } from "remix-auth";
|
||||
import { GoogleStrategy } from "remix-auth-google";
|
||||
import { GoogleStrategy } from "@coji/remix-auth-google";
|
||||
import { env } from "~/env.server";
|
||||
import { findOrCreateUser } from "~/models/user.server";
|
||||
import type { AuthUser } from "./authUser";
|
||||
@ -8,34 +8,36 @@ import { logger } from "./logger.service";
|
||||
|
||||
export function addGoogleStrategy(
|
||||
authenticator: Authenticator<AuthUser>,
|
||||
clientID: string,
|
||||
clientSecret: string
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
) {
|
||||
const googleStrategy = new GoogleStrategy(
|
||||
{
|
||||
clientID,
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackURL: `${env.LOGIN_ORIGIN}/auth/google/callback`,
|
||||
redirectURI: `${env.LOGIN_ORIGIN}/auth/google/callback`,
|
||||
},
|
||||
async ({ extraParams, profile }) => {
|
||||
async ({ tokens }) => {
|
||||
const profile = await GoogleStrategy.userProfile(tokens);
|
||||
const emails = profile.emails;
|
||||
|
||||
if (!emails) {
|
||||
throw new Error("Google login requires an email address");
|
||||
}
|
||||
|
||||
console.log(tokens);
|
||||
|
||||
try {
|
||||
logger.debug("Google login", {
|
||||
emails,
|
||||
profile,
|
||||
extraParams,
|
||||
});
|
||||
|
||||
const { user, isNewUser } = await findOrCreateUser({
|
||||
email: emails[0].value,
|
||||
authenticationMethod: "GOOGLE",
|
||||
authenticationProfile: profile,
|
||||
authenticationExtraParams: extraParams,
|
||||
authenticationExtraParams: {},
|
||||
});
|
||||
|
||||
await postAuthentication({ user, isNewUser, loginMethod: "GOOGLE" });
|
||||
@ -47,8 +49,8 @@ export function addGoogleStrategy(
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
authenticator.use(googleStrategy);
|
||||
authenticator.use(googleStrategy as any, "google");
|
||||
}
|
||||
|
||||
373
apps/webapp/app/services/personalAccessToken.server.ts
Normal file
373
apps/webapp/app/services/personalAccessToken.server.ts
Normal file
@ -0,0 +1,373 @@
|
||||
import { type PersonalAccessToken } from "@recall/database";
|
||||
import { customAlphabet, nanoid } from "nanoid";
|
||||
import nodeCrypto from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { logger } from "./logger.service";
|
||||
|
||||
const tokenValueLength = 40;
|
||||
//lowercase only, removed 0 and l to avoid confusion
|
||||
const tokenGenerator = customAlphabet(
|
||||
"123456789abcdefghijkmnopqrstuvwxyz",
|
||||
tokenValueLength,
|
||||
);
|
||||
|
||||
type CreatePersonalAccessTokenOptions = {
|
||||
name: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
/** Returns obfuscated access tokens that aren't revoked */
|
||||
export async function getValidPersonalAccessTokens(userId: string) {
|
||||
const personalAccessTokens = await prisma.personalAccessToken.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
obfuscatedToken: true,
|
||||
createdAt: true,
|
||||
lastAccessedAt: true,
|
||||
},
|
||||
where: {
|
||||
userId,
|
||||
revokedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
return personalAccessTokens.map((pat) => ({
|
||||
id: pat.id,
|
||||
name: pat.name,
|
||||
obfuscatedToken: pat.obfuscatedToken,
|
||||
createdAt: pat.createdAt,
|
||||
lastAccessedAt: pat.lastAccessedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
export type ObfuscatedPersonalAccessToken = Awaited<
|
||||
ReturnType<typeof getValidPersonalAccessTokens>
|
||||
>[number];
|
||||
|
||||
/** Gets a PersonalAccessToken from an Auth Code, this only works within 10 mins of the auth code being created */
|
||||
export async function getPersonalAccessTokenFromAuthorizationCode(
|
||||
authorizationCode: string,
|
||||
) {
|
||||
//only allow authorization codes that were created less than 10 mins ago
|
||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
||||
const code = await prisma.authorizationCode.findUnique({
|
||||
select: {
|
||||
personalAccessToken: true,
|
||||
},
|
||||
where: {
|
||||
code: authorizationCode,
|
||||
createdAt: {
|
||||
gte: tenMinutesAgo,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!code) {
|
||||
throw new Error("Invalid authorization code, or code expired");
|
||||
}
|
||||
|
||||
//there's no PersonalAccessToken associated with this code
|
||||
if (!code.personalAccessToken) {
|
||||
return {
|
||||
token: null,
|
||||
};
|
||||
}
|
||||
|
||||
const decryptedToken = decryptPersonalAccessToken(code.personalAccessToken);
|
||||
return {
|
||||
token: {
|
||||
token: decryptedToken,
|
||||
obfuscatedToken: code.personalAccessToken.obfuscatedToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function revokePersonalAccessToken(tokenId: string) {
|
||||
await prisma.personalAccessToken.update({
|
||||
where: {
|
||||
id: tokenId,
|
||||
},
|
||||
data: {
|
||||
revokedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type PersonalAccessTokenAuthenticationResult = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const EncryptedSecretValueSchema = z.object({
|
||||
nonce: z.string(),
|
||||
ciphertext: z.string(),
|
||||
tag: z.string(),
|
||||
});
|
||||
|
||||
const AuthorizationHeaderSchema = z.string().regex(/^Bearer .+$/);
|
||||
|
||||
export async function authenticateApiRequestWithPersonalAccessToken(
|
||||
request: Request,
|
||||
): Promise<PersonalAccessTokenAuthenticationResult | undefined> {
|
||||
const token = getPersonalAccessTokenFromRequest(request);
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
return authenticatePersonalAccessToken(token);
|
||||
}
|
||||
|
||||
function getPersonalAccessTokenFromRequest(request: Request) {
|
||||
const rawAuthorization = request.headers.get("Authorization");
|
||||
|
||||
const authorization = AuthorizationHeaderSchema.safeParse(rawAuthorization);
|
||||
if (!authorization.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const personalAccessToken = authorization.data.replace(/^Bearer /, "");
|
||||
return personalAccessToken;
|
||||
}
|
||||
|
||||
export async function authenticatePersonalAccessToken(
|
||||
token: string,
|
||||
): Promise<PersonalAccessTokenAuthenticationResult | undefined> {
|
||||
if (!token.startsWith(tokenPrefix)) {
|
||||
logger.warn(`PAT doesn't start with ${tokenPrefix}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hashedToken = hashToken(token);
|
||||
|
||||
const personalAccessToken = await prisma.personalAccessToken.findFirst({
|
||||
where: {
|
||||
hashedToken,
|
||||
revokedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!personalAccessToken) {
|
||||
// The token may have been revoked or is entirely invalid
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.personalAccessToken.update({
|
||||
where: {
|
||||
id: personalAccessToken.id,
|
||||
},
|
||||
data: {
|
||||
lastAccessedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const decryptedToken = decryptPersonalAccessToken(personalAccessToken);
|
||||
|
||||
if (decryptedToken !== token) {
|
||||
logger.error(
|
||||
`PersonalAccessToken with id: ${personalAccessToken.id} was found in the database with hash ${hashedToken}, but the decrypted token did not match the provided token.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: personalAccessToken.userId,
|
||||
};
|
||||
}
|
||||
|
||||
export function isPersonalAccessToken(token: string) {
|
||||
return token.startsWith(tokenPrefix);
|
||||
}
|
||||
|
||||
export function createAuthorizationCode() {
|
||||
return prisma.authorizationCode.create({
|
||||
data: {
|
||||
code: nanoid(64),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Creates a PersonalAccessToken from an Auth Code, and return the token. We only ever return the unencrypted token once. */
|
||||
export async function createPersonalAccessTokenFromAuthorizationCode(
|
||||
authorizationCode: string,
|
||||
userId: string,
|
||||
) {
|
||||
//only allow authorization codes that were created less than 10 mins ago
|
||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
||||
const code = await prisma.authorizationCode.findUnique({
|
||||
where: {
|
||||
code: authorizationCode,
|
||||
personalAccessTokenId: null,
|
||||
createdAt: {
|
||||
gte: tenMinutesAgo,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
throw new Error(
|
||||
"Invalid authorization code, code already used, or code expired",
|
||||
);
|
||||
}
|
||||
|
||||
const existingCliPersonalAccessToken =
|
||||
await prisma.personalAccessToken.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
name: "cli",
|
||||
},
|
||||
});
|
||||
|
||||
//we only allow you to have one CLI PAT at a time, so return this
|
||||
if (existingCliPersonalAccessToken) {
|
||||
//associate this authorization code with the existing personal access token
|
||||
await prisma.authorizationCode.update({
|
||||
where: {
|
||||
code: authorizationCode,
|
||||
},
|
||||
data: {
|
||||
personalAccessTokenId: existingCliPersonalAccessToken.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCliPersonalAccessToken.revokedAt) {
|
||||
// re-activate revoked CLI PAT so we can use it again
|
||||
await prisma.personalAccessToken.update({
|
||||
where: {
|
||||
id: existingCliPersonalAccessToken.id,
|
||||
},
|
||||
data: {
|
||||
revokedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//we don't return the decrypted token
|
||||
return {
|
||||
id: existingCliPersonalAccessToken.id,
|
||||
name: existingCliPersonalAccessToken.name,
|
||||
userId: existingCliPersonalAccessToken.userId,
|
||||
obfuscateToken: existingCliPersonalAccessToken.obfuscatedToken,
|
||||
};
|
||||
}
|
||||
|
||||
const token = await createPersonalAccessToken({
|
||||
name: "cli",
|
||||
userId,
|
||||
});
|
||||
|
||||
await prisma.authorizationCode.update({
|
||||
where: {
|
||||
code: authorizationCode,
|
||||
},
|
||||
data: {
|
||||
personalAccessTokenId: token.id,
|
||||
},
|
||||
});
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/** Created a new PersonalAccessToken, and return the token. We only ever return the unencrypted token once. */
|
||||
export async function createPersonalAccessToken({
|
||||
name,
|
||||
userId,
|
||||
}: CreatePersonalAccessTokenOptions) {
|
||||
const token = createToken();
|
||||
const encryptedToken = encryptToken(token);
|
||||
|
||||
const personalAccessToken = await prisma.personalAccessToken.create({
|
||||
data: {
|
||||
name,
|
||||
userId,
|
||||
encryptedToken,
|
||||
obfuscatedToken: obfuscateToken(token),
|
||||
hashedToken: hashToken(token),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: personalAccessToken.id,
|
||||
name,
|
||||
userId,
|
||||
token,
|
||||
obfuscatedToken: personalAccessToken.obfuscatedToken,
|
||||
};
|
||||
}
|
||||
|
||||
export type CreatedPersonalAccessToken = Awaited<
|
||||
ReturnType<typeof createPersonalAccessToken>
|
||||
>;
|
||||
|
||||
const tokenPrefix = "rc_pat_";
|
||||
|
||||
/** Creates a PersonalAccessToken that starts with tr_pat_ */
|
||||
function createToken() {
|
||||
return `${tokenPrefix}${tokenGenerator()}`;
|
||||
}
|
||||
|
||||
/** Obfuscates all but the first and last 4 characters of the token, so it looks like tr_pat_bhbd•••••••••••••••••••fd4a */
|
||||
function obfuscateToken(token: string) {
|
||||
const withoutPrefix = token.replace(tokenPrefix, "");
|
||||
const obfuscated = `${withoutPrefix.slice(0, 4)}${"•".repeat(18)}${withoutPrefix.slice(-4)}`;
|
||||
return `${tokenPrefix}${obfuscated}`;
|
||||
}
|
||||
|
||||
function encryptToken(value: string) {
|
||||
const nonce = nodeCrypto.randomBytes(12);
|
||||
const cipher = nodeCrypto.createCipheriv(
|
||||
"aes-256-gcm",
|
||||
env.ENCRYPTION_KEY,
|
||||
nonce,
|
||||
);
|
||||
|
||||
let encrypted = cipher.update(value, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
const tag = cipher.getAuthTag().toString("hex");
|
||||
|
||||
return {
|
||||
nonce: nonce.toString("hex"),
|
||||
ciphertext: encrypted,
|
||||
tag,
|
||||
};
|
||||
}
|
||||
|
||||
function decryptPersonalAccessToken(personalAccessToken: PersonalAccessToken) {
|
||||
const encryptedData = EncryptedSecretValueSchema.safeParse(
|
||||
personalAccessToken.encryptedToken,
|
||||
);
|
||||
if (!encryptedData.success) {
|
||||
throw new Error(
|
||||
`Unable to parse encrypted PersonalAccessToken with id: ${personalAccessToken.id}: ${encryptedData.error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const decryptedToken = decryptToken(
|
||||
encryptedData.data.nonce,
|
||||
encryptedData.data.ciphertext,
|
||||
encryptedData.data.tag,
|
||||
);
|
||||
return decryptedToken;
|
||||
}
|
||||
|
||||
function decryptToken(nonce: string, ciphertext: string, tag: string): string {
|
||||
const decipher = nodeCrypto.createDecipheriv(
|
||||
"aes-256-gcm",
|
||||
env.ENCRYPTION_KEY,
|
||||
Buffer.from(nonce, "hex"),
|
||||
);
|
||||
|
||||
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
||||
|
||||
let decrypted = decipher.update(ciphertext, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
function hashToken(token: string): string {
|
||||
const hash = nodeCrypto.createHash("sha256");
|
||||
hash.update(token);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
51
apps/webapp/app/services/redirectTo.server.ts
Normal file
51
apps/webapp/app/services/redirectTo.server.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { createCookieSessionStorage } from "@remix-run/node";
|
||||
import { z } from "zod";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
const ONE_DAY = 60 * 60 * 24;
|
||||
|
||||
export const { commitSession, getSession } = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "__redirectTo",
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secrets: [env.SESSION_SECRET],
|
||||
secure: env.NODE_ENV === "production",
|
||||
maxAge: ONE_DAY,
|
||||
},
|
||||
});
|
||||
|
||||
export function getRedirectSession(request: Request) {
|
||||
return getSession(request.headers.get("Cookie"));
|
||||
}
|
||||
|
||||
export async function setRedirectTo(request: Request, redirectTo: string) {
|
||||
const session = await getRedirectSession(request);
|
||||
|
||||
if (session) {
|
||||
session.set("redirectTo", redirectTo);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function clearRedirectTo(request: Request) {
|
||||
const session = await getRedirectSession(request);
|
||||
|
||||
if (session) {
|
||||
session.unset("redirectTo");
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function getRedirectTo(
|
||||
request: Request,
|
||||
): Promise<string | undefined> {
|
||||
const session = await getRedirectSession(request);
|
||||
|
||||
if (session) {
|
||||
return z.string().optional().parse(session.get("redirectTo"));
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { getUserById } from "~/models/user.server";
|
||||
import { authenticator } from "./auth.server";
|
||||
import { sessionStorage } from "./sessionStorage.server";
|
||||
import { getImpersonationId } from "./impersonation.server";
|
||||
|
||||
export async function getUserId(request: Request): Promise<string | undefined> {
|
||||
@ -8,8 +8,10 @@ export async function getUserId(request: Request): Promise<string | undefined> {
|
||||
|
||||
if (impersonatedUserId) return impersonatedUserId;
|
||||
|
||||
let authUser = await authenticator.isAuthenticated(request);
|
||||
return authUser?.userId;
|
||||
let session = await sessionStorage.getSession(request.headers.get("cookie"));
|
||||
let user = session.get("user");
|
||||
|
||||
return user?.userId;
|
||||
}
|
||||
|
||||
export async function getUser(request: Request) {
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { createCookieSessionStorage } from "@remix-run/node";
|
||||
import { createThemeSessionResolver } from "remix-themes";
|
||||
import { env } from "~/env.server";
|
||||
import { type AuthUser } from "./authUser";
|
||||
|
||||
export const sessionStorage = createCookieSessionStorage({
|
||||
let SESSION_KEY = "user";
|
||||
|
||||
export const sessionStorage = createCookieSessionStorage<{
|
||||
[SESSION_KEY]: AuthUser;
|
||||
}>({
|
||||
cookie: {
|
||||
name: "__session", // use any name you want here
|
||||
sameSite: "lax", // this helps with CSRF
|
||||
@ -26,6 +31,18 @@ export const themeStorage = createCookieSessionStorage({
|
||||
},
|
||||
});
|
||||
|
||||
export const getSessionFromStore = async (request: Request) => {
|
||||
return await sessionStorage.getSession(request.headers.get("Cookie"));
|
||||
};
|
||||
|
||||
export const saveSession = async (request: Request, user: AuthUser) => {
|
||||
const session = await getSessionFromStore(request);
|
||||
session.set(SESSION_KEY, user);
|
||||
return new Headers({
|
||||
"Set-Cookie": await sessionStorage.commitSession(session),
|
||||
});
|
||||
};
|
||||
|
||||
export const themeSessionResolver = createThemeSessionResolver(sessionStorage);
|
||||
|
||||
export function getUserSession(request: Request) {
|
||||
|
||||
10
apps/webapp/app/utils/requestUrl.server.ts
Normal file
10
apps/webapp/app/utils/requestUrl.server.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// Updates the protocol of the request url to match the request.headers x-forwarded-proto
|
||||
export function requestUrl(request: Request): URL {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.headers.get("x-forwarded-proto") === "https") {
|
||||
url.protocol = "https:";
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "remix vite:build",
|
||||
"dev": "node ./server.js",
|
||||
"dev": "node ./server.mjs",
|
||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||
"start": "remix-serve ./build/server/index.js",
|
||||
"typecheck": "tsc"
|
||||
@ -41,8 +41,10 @@
|
||||
"posthog-js": "^1.116.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"remix-auth": "^3.6.0",
|
||||
"remix-auth-google": "^2.0.0",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"remix-auth": "^4.2.0",
|
||||
"@coji/remix-auth-google": "^4.2.0",
|
||||
"remix-auth-oauth2": "^3.4.1",
|
||||
"remix-themes": "^1.3.1",
|
||||
"remix-typedjson": "0.3.1",
|
||||
"remix-utils": "^7.7.0",
|
||||
|
||||
@ -23,4 +23,8 @@ export default defineConfig({
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
server: {
|
||||
allowedHosts: true,
|
||||
port: 3033,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
|
||||
{
|
||||
"vector_config": {
|
||||
"m": 16,
|
||||
"ef_construction": 128,
|
||||
"ef_search": 768
|
||||
},
|
||||
"graph_config": {
|
||||
"secondary_indices": []
|
||||
},
|
||||
"db_max_size_gb": 10
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
// Start writing your queries here.
|
||||
//
|
||||
// You can use the schema to help you write your queries.
|
||||
//
|
||||
// Queries take the form:
|
||||
// QUERY {query name}({input name}: {input type}) =>
|
||||
// {variable} <- {traversal}
|
||||
// RETURN {variable}
|
||||
//
|
||||
// Example:
|
||||
// QUERY GetUserFriends(user_id: String) =>
|
||||
// friends <- N<User>(user_id)::Out<Knows>
|
||||
// RETURN friends
|
||||
//
|
||||
//
|
||||
// For more information on how to write queries,
|
||||
// see the documentation at https://docs.helix-db.com
|
||||
// or checkout our GitHub at https://github.com/HelixDB/helix-db
|
||||
|
||||
QUERY hnswinsert(vector: [F64]) =>
|
||||
AddV<Embedding>(vector)
|
||||
RETURN "Success"
|
||||
|
||||
QUERY hnswsearch(query: [F64], k: I32) =>
|
||||
res <- SearchV<Embedding>(query, k)
|
||||
RETURN res
|
||||
@ -1,38 +0,0 @@
|
||||
// Start building your schema here.
|
||||
//
|
||||
// The schema is used to to ensure a level of type safety in your queries.
|
||||
//
|
||||
// The schema is made up of Node types, denoted by N::,
|
||||
// and Edge types, denoted by E::
|
||||
//
|
||||
// Under the Node types you can define fields that
|
||||
// will be stored in the database.
|
||||
//
|
||||
// Under the Edge types you can define what type of node
|
||||
// the edge will connect to and from, and also the
|
||||
// properties that you want to store on the edge.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// N::User {
|
||||
// Name: String,
|
||||
// Label: String,
|
||||
// Age: Integer,
|
||||
// IsAdmin: Boolean,
|
||||
// }
|
||||
//
|
||||
// E::Knows {
|
||||
// From: User,
|
||||
// To: User,
|
||||
// Properties: {
|
||||
// Since: Integer,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// For more information on how to write queries,
|
||||
// see the documentation at https://docs.helix-db.com
|
||||
// or checkout our GitHub at https://github.com/HelixDB/helix-db
|
||||
|
||||
V::Embedding {
|
||||
vec: [F64]
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Workspace" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deleted" TIMESTAMP(3),
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"icon" TEXT,
|
||||
"userId" TEXT,
|
||||
|
||||
CONSTRAINT "Workspace_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Workspace_slug_key" ON "Workspace"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Workspace_userId_key" ON "Workspace"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Workspace" ADD CONSTRAINT "Workspace_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
147
pnpm-lock.yaml
generated
147
pnpm-lock.yaml
generated
@ -33,6 +33,9 @@ importers:
|
||||
'@ai-sdk/openai':
|
||||
specifier: ^1.3.21
|
||||
version: 1.3.22(zod@3.23.8)
|
||||
'@coji/remix-auth-google':
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
'@opentelemetry/api':
|
||||
specifier: 1.9.0
|
||||
version: 1.9.0
|
||||
@ -66,6 +69,9 @@ importers:
|
||||
'@remix-run/v1-meta':
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3))
|
||||
'@remixicon/react':
|
||||
specifier: ^4.2.0
|
||||
version: 4.6.0(react@18.3.1)
|
||||
'@tailwindcss/container-queries':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(tailwindcss@4.1.7)
|
||||
@ -121,11 +127,11 @@ importers:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
remix-auth:
|
||||
specifier: ^3.6.0
|
||||
version: 3.7.0(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3))
|
||||
remix-auth-google:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(@remix-run/server-runtime@2.16.7(typescript@5.8.3))(remix-auth@3.7.0(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3)))
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
remix-auth-oauth2:
|
||||
specifier: ^3.4.1
|
||||
version: 3.4.1(remix-auth@4.2.0)
|
||||
remix-themes:
|
||||
specifier: ^1.3.1
|
||||
version: 1.6.1(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3))
|
||||
@ -570,6 +576,13 @@ packages:
|
||||
'@changesets/write@0.2.3':
|
||||
resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==}
|
||||
|
||||
'@coji/remix-auth-google@4.2.0':
|
||||
resolution: {integrity: sha512-H9i3fvVz0GE18GUZHpz7p7FQjuiuloTIBAPjW7cfv7lUEk+mI6WRTVLEHJBLLuTlAF1+0EbzvPRYKutxZiFdfw==}
|
||||
|
||||
'@edgefirst-dev/data@0.0.4':
|
||||
resolution: {integrity: sha512-VLhlvEPDJ0Sd0pE6sAYTQkIqZCXVonaWlgRJIQQHzfjTXCadF77qqHj5NxaPSc4wCul0DJO/0MnejVqJAXUiRg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@emnapi/core@1.4.3':
|
||||
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
|
||||
|
||||
@ -1086,6 +1099,9 @@ packages:
|
||||
'@mdx-js/mdx@2.3.0':
|
||||
resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
|
||||
|
||||
'@mjackson/headers@0.10.0':
|
||||
resolution: {integrity: sha512-U1Eu1gF979k7ZoIBsJyD+T5l9MjtPONsZfoXfktsQHPJD0s7SokBGx+tLKDLsOY+gzVYAWS0yRFDNY8cgbQzWQ==}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.10':
|
||||
resolution: {integrity: sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==}
|
||||
|
||||
@ -1128,6 +1144,24 @@ packages:
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@oslojs/asn1@1.0.0':
|
||||
resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==}
|
||||
|
||||
'@oslojs/binary@1.0.0':
|
||||
resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==}
|
||||
|
||||
'@oslojs/crypto@1.0.1':
|
||||
resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==}
|
||||
|
||||
'@oslojs/encoding@0.4.1':
|
||||
resolution: {integrity: sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==}
|
||||
|
||||
'@oslojs/encoding@1.1.0':
|
||||
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
|
||||
|
||||
'@oslojs/jwt@0.2.0':
|
||||
resolution: {integrity: sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
@ -1301,6 +1335,11 @@ packages:
|
||||
'@remix-run/web-stream@1.1.0':
|
||||
resolution: {integrity: sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==}
|
||||
|
||||
'@remixicon/react@4.6.0':
|
||||
resolution: {integrity: sha512-bY56maEgT5IYUSRotqy9h03IAKJC85vlKtWFg2FKzfs8JPrkdBAYSa9dxoUSKFwGzup8Ux6vjShs9Aec3jvr2w==}
|
||||
peerDependencies:
|
||||
react: '>=18.2.0'
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.41.1':
|
||||
resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==}
|
||||
cpu: [arm]
|
||||
@ -1936,6 +1975,9 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
arctic@3.7.0:
|
||||
resolution: {integrity: sha512-ZMQ+f6VazDgUJOd+qNV+H7GohNSYal1mVjm5kEaZfE2Ifb7Ss70w+Q7xpJC87qZDkMZIXYf0pTIYZA0OPasSbw==}
|
||||
|
||||
arg@5.0.2:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
|
||||
@ -4752,23 +4794,15 @@ packages:
|
||||
remark-rehype@10.1.0:
|
||||
resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==}
|
||||
|
||||
remix-auth-google@2.0.0:
|
||||
resolution: {integrity: sha512-qP38N1ZJADz+HlH2lrEn/rSL6m5Dwcnz8xUFDhbHecDMl9FU/UpkUx3w8jj5XhNSnkSue/GWT+p7OEkatgbXNA==}
|
||||
remix-auth-oauth2@3.4.1:
|
||||
resolution: {integrity: sha512-ZhGon1czdIsOw1/O9EcTCzapZB6FpT3u9vtXSVeEMwGNs+iWljRsibnUC1RtwJbzzCdLBSwIXTNTbiRmLZ4cZw==}
|
||||
engines: {node: ^20.0.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
'@remix-run/server-runtime': ^2.0.1
|
||||
remix-auth: ^3.2.1
|
||||
remix-auth: ^4.0.0
|
||||
|
||||
remix-auth-oauth2@1.11.2:
|
||||
resolution: {integrity: sha512-5ORP+LMi5CVCA/Wb8Z+FCAJ73Uiy4uyjEzhlVwNBfdAkPOnfxzoi+q/pY/CrueYv3OniCXRM35ZYqkVi3G1UPw==}
|
||||
peerDependencies:
|
||||
'@remix-run/server-runtime': ^1.0.0 || ^2.0.0
|
||||
remix-auth: ^3.6.0
|
||||
|
||||
remix-auth@3.7.0:
|
||||
resolution: {integrity: sha512-2QVjp2nJVaYxuFBecMQwzixCO7CLSssttLBU5eVlNcNlVeNMmY1g7OkmZ1Ogw9sBcoMXZ18J7xXSK0AISVFcfQ==}
|
||||
peerDependencies:
|
||||
'@remix-run/react': ^1.0.0 || ^2.0.0
|
||||
'@remix-run/server-runtime': ^1.0.0 || ^2.0.0
|
||||
remix-auth@4.2.0:
|
||||
resolution: {integrity: sha512-3LSfWEvSgG2CgbG/p4ge5hbV8tTXWNnnYIGbTr9oSSiHz9dD7wh6S0MEyo3pwh7MlKezB2WIlevGeyqUZykk7g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
remix-themes@1.6.1:
|
||||
resolution: {integrity: sha512-wqJyNKJ2hiOweycQzsAk7CZm+2mKNAbW2QZcX0riw52XepAxf9R2v8NYyeUz+uWmb3Fulyi71s4aipNRTxCysw==}
|
||||
@ -5476,14 +5510,6 @@ packages:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
|
||||
uvu@0.5.6:
|
||||
resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==}
|
||||
engines: {node: '>=8'}
|
||||
@ -6205,6 +6231,10 @@ snapshots:
|
||||
human-id: 1.0.2
|
||||
prettier: 2.8.8
|
||||
|
||||
'@coji/remix-auth-google@4.2.0': {}
|
||||
|
||||
'@edgefirst-dev/data@0.0.4': {}
|
||||
|
||||
'@emnapi/core@1.4.3':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.0.2
|
||||
@ -6549,6 +6579,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@mjackson/headers@0.10.0': {}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.10':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.4.3
|
||||
@ -6609,6 +6641,25 @@ snapshots:
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@oslojs/asn1@1.0.0':
|
||||
dependencies:
|
||||
'@oslojs/binary': 1.0.0
|
||||
|
||||
'@oslojs/binary@1.0.0': {}
|
||||
|
||||
'@oslojs/crypto@1.0.1':
|
||||
dependencies:
|
||||
'@oslojs/asn1': 1.0.0
|
||||
'@oslojs/binary': 1.0.0
|
||||
|
||||
'@oslojs/encoding@0.4.1': {}
|
||||
|
||||
'@oslojs/encoding@1.1.0': {}
|
||||
|
||||
'@oslojs/jwt@0.2.0':
|
||||
dependencies:
|
||||
'@oslojs/encoding': 0.4.1
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
@ -6884,6 +6935,10 @@ snapshots:
|
||||
dependencies:
|
||||
web-streams-polyfill: 3.3.3
|
||||
|
||||
'@remixicon/react@4.6.0(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.41.1':
|
||||
optional: true
|
||||
|
||||
@ -7534,6 +7589,12 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
arctic@3.7.0:
|
||||
dependencies:
|
||||
'@oslojs/crypto': 1.0.1
|
||||
'@oslojs/encoding': 1.1.0
|
||||
'@oslojs/jwt': 0.2.0
|
||||
|
||||
arg@5.0.2: {}
|
||||
|
||||
argparse@1.0.10:
|
||||
@ -10750,28 +10811,14 @@ snapshots:
|
||||
mdast-util-to-hast: 12.3.0
|
||||
unified: 10.1.2
|
||||
|
||||
remix-auth-google@2.0.0(@remix-run/server-runtime@2.16.7(typescript@5.8.3))(remix-auth@3.7.0(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3))):
|
||||
remix-auth-oauth2@3.4.1(remix-auth@4.2.0):
|
||||
dependencies:
|
||||
'@remix-run/server-runtime': 2.16.7(typescript@5.8.3)
|
||||
remix-auth: 3.7.0(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3))
|
||||
remix-auth-oauth2: 1.11.2(@remix-run/server-runtime@2.16.7(typescript@5.8.3))(remix-auth@3.7.0(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3)))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
'@edgefirst-dev/data': 0.0.4
|
||||
'@mjackson/headers': 0.10.0
|
||||
arctic: 3.7.0
|
||||
remix-auth: 4.2.0
|
||||
|
||||
remix-auth-oauth2@1.11.2(@remix-run/server-runtime@2.16.7(typescript@5.8.3))(remix-auth@3.7.0(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3))):
|
||||
dependencies:
|
||||
'@remix-run/server-runtime': 2.16.7(typescript@5.8.3)
|
||||
debug: 4.4.1
|
||||
remix-auth: 3.7.0(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3))
|
||||
uuid: 9.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remix-auth@3.7.0(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@remix-run/react': 2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)
|
||||
'@remix-run/server-runtime': 2.16.7(typescript@5.8.3)
|
||||
uuid: 8.3.2
|
||||
remix-auth@4.2.0: {}
|
||||
|
||||
remix-themes@1.6.1(@remix-run/react@2.16.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@remix-run/server-runtime@2.16.7(typescript@5.8.3)):
|
||||
dependencies:
|
||||
@ -11565,10 +11612,6 @@ snapshots:
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
uvu@0.5.6:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user