From 0112aed6e652c5119cd94b49b7f6a20426d32b7b Mon Sep 17 00:00:00 2001 From: Miguel Romero Karam Date: Mon, 20 May 2024 16:36:55 +0200 Subject: [PATCH] wip(plugins/auth): refactor auth module to use database adapters --- lib/components/dialog.tsx | 2 +- lib/components/head.tsx | 1 - lib/deps/@unocss/utils.ts | 1 + lib/plugins/auth/islands/auth-form.tsx | 27 +-- lib/plugins/auth/middlewares/mod.ts | 27 ++- lib/plugins/auth/plugin.ts | 13 +- lib/plugins/auth/routes/auth.tsx | 31 +++- lib/plugins/auth/routes/mod.ts | 29 ++- lib/plugins/auth/utils/adapters/datastore.ts | 70 +++++++ lib/plugins/auth/utils/db.ts | 182 ------------------- lib/plugins/auth/utils/providers/auth0.ts | 2 +- lib/plugins/auth/utils/providers/email.ts | 2 +- lib/plugins/auth/utils/providers/github.ts | 2 +- lib/plugins/auth/utils/providers/gitlab.ts | 2 +- lib/plugins/auth/utils/providers/google.ts | 2 +- lib/plugins/auth/utils/providers/mod.ts | 4 +- lib/plugins/auth/utils/providers/netzo.ts | 2 +- lib/plugins/auth/utils/providers/okta.ts | 2 +- lib/plugins/auth/utils/providers/slack.ts | 2 +- lib/plugins/auth/utils/types.ts | 121 ++++++++++++ lib/plugins/unocss/preset-shadcn/mod.ts | 9 +- 21 files changed, 292 insertions(+), 241 deletions(-) create mode 100644 lib/deps/@unocss/utils.ts create mode 100644 lib/plugins/auth/utils/adapters/datastore.ts delete mode 100644 lib/plugins/auth/utils/db.ts create mode 100644 lib/plugins/auth/utils/types.ts diff --git a/lib/components/dialog.tsx b/lib/components/dialog.tsx index 6e4d0cb6..ed006375 100644 --- a/lib/components/dialog.tsx +++ b/lib/components/dialog.tsx @@ -117,7 +117,7 @@ export { DialogOverlay, DialogPortal, DialogTitle, - DialogTrigger + DialogTrigger, }; //////////////////////////////////////////////////////////////////////////////// diff --git a/lib/components/head.tsx b/lib/components/head.tsx index 8f0fa057..850f8a1a 100644 --- a/lib/components/head.tsx +++ b/lib/components/head.tsx @@ -22,7 +22,6 @@ export type HeadProps = { export const Head = (props: HeadProps) => { return ( <_Head> -
@@ -74,13 +79,13 @@ export function AuthForm(props: AuthFormProps) { )} {!!providers?.netzo && ( - +
)} {!!providers?.google && ( - + {/* NOTE: use inline SVG instead of logos-google-icon to avoid having to load logos collection (7MB) */} +
)} {!!providers?.gitlab && ( - +
)} {!!providers?.slack && ( - + {/* NOTE: use inline SVG instead of logos-slack-icon to avoid having to load logos collection (7MB) */} + {/* NOTE: use inline SVG instead of simple-icons-auth0 to avoid having to load simple-icons collection (4.4MB) */} + {/* NOTE: use inline SVG instead of simple-icons-okta to avoid having to load simple-icons collection (4.4MB) */} +
)} */ @@ -218,7 +223,7 @@ function ButtonNetzo( props: { text: string; href: string; - children: ComponentChildren; + children: React.ReactNode; }, ) { return ( @@ -235,7 +240,7 @@ function ButtonOAuth2( props: { text: string; href: string; - children: ComponentChildren; + children: React.ReactNode; }, ) { return ( diff --git a/lib/plugins/auth/middlewares/mod.ts b/lib/plugins/auth/middlewares/mod.ts index cbf32a75..d32848d9 100644 --- a/lib/plugins/auth/middlewares/mod.ts +++ b/lib/plugins/auth/middlewares/mod.ts @@ -1,9 +1,14 @@ import type { FreshContext } from "$fresh/server.ts"; +import { AuthState } from "netzo/plugins/auth/plugin.ts"; import { getSessionId } from "../../../deps/deno_kv_oauth/mod.ts"; import type { NetzoState } from "../../../mod.ts"; -import { getUserBySession } from "../utils/db.ts"; +import { createDatastoreAuth } from "../utils/adapters/datastore.ts"; -const skip = (_req: Request, ctx: FreshContext) => { +type NetzoStateWithAuth = NetzoState & { + auth: AuthState; +}; + +const skip = (_req: Request, ctx: FreshContext) => { if (!["route"].includes(ctx.destination)) return true; if (ctx.url.pathname.startsWith("/auth/")) return true; // skip auth routes (signin, callback, signout) if (ctx.url.pathname.startsWith("/database")) return true; // skip database routes @@ -14,9 +19,17 @@ const skip = (_req: Request, ctx: FreshContext) => { return false; }; +export async function setAuthState( + _req: Request, + ctx: FreshContext, +) { + ctx.state.auth ??= createDatastoreAuth(); + return await ctx.next(); +} + export async function setSessionState( req: Request, - ctx: FreshContext, + ctx: FreshContext, ) { if (skip(req, ctx)) return await ctx.next(); @@ -30,7 +43,7 @@ export async function setSessionState( if (sessionId === undefined) return await ctx.next(); // A) not authenticated - const user = await getUserBySession(sessionId); + const user = await ctx.state.auth.getUserBySession(sessionId); if (!user) return await ctx.next(); // B) user not found // set authenticated state (sessionId could be set but expired, @@ -43,7 +56,7 @@ export async function setSessionState( export async function assertUserIsWorkspaceUserOfWorkspaceOfApiKeyIfProviderIsNetzo( req: Request, - ctx: FreshContext, + ctx: FreshContext, ) { if (skip(req, ctx)) return await ctx.next(); @@ -79,7 +92,7 @@ export async function assertUserIsWorkspaceUserOfWorkspaceOfApiKeyIfProviderIsNe export async function setRequestState( req: Request, - ctx: FreshContext, + ctx: FreshContext, ) { if (skip(req, ctx)) return await ctx.next(); @@ -99,7 +112,7 @@ export async function setRequestState( export async function ensureSignedIn( req: Request, - ctx: FreshContext, + ctx: FreshContext, ) { if (skip(req, ctx)) return await ctx.next(); diff --git a/lib/plugins/auth/plugin.ts b/lib/plugins/auth/plugin.ts index 7798e765..0dcb3d13 100644 --- a/lib/plugins/auth/plugin.ts +++ b/lib/plugins/auth/plugin.ts @@ -4,15 +4,15 @@ import type { NetzoState } from "../../mod.ts"; import { assertUserIsWorkspaceUserOfWorkspaceOfApiKeyIfProviderIsNetzo, ensureSignedIn, + setAuthState, setRequestState, setSessionState, } from "./middlewares/mod.ts"; import createAuth from "./routes/auth.tsx"; import { getRoutesByProvider } from "./routes/mod.ts"; -import type { AuthUser } from "./utils/db.ts"; import type { EmailClientConfig } from "./utils/providers/email.ts"; -import type { AuthProvider } from "./utils/providers/mod.ts"; import type { NetzoClientConfig } from "./utils/providers/netzo.ts"; +import type { Auth, AuthProvider, AuthUser } from "./utils/types.ts"; export * from "../../deps/deno_kv_oauth/mod.ts"; @@ -23,6 +23,9 @@ export type AuthConfig = { description?: string; /** HTML content rendered below auth form e.g. to display a link to the terms of service via an a tag. */ caption?: string; + /** An image URL to display to the right side of the login form at /auth. */ + image?: React.ImgHTMLAttributes; + locale?: "en" | "es"; providers: { netzo?: NetzoClientConfig; email?: EmailClientConfig; @@ -35,7 +38,7 @@ export type AuthConfig = { }; }; -export type AuthState = { +export type AuthState = Auth & { /* Session ID used internally by the auth plugin. */ sessionId?: string; /* The user object associated with the session ID. */ @@ -92,6 +95,10 @@ export const auth = (config: AuthConfig): Plugin => { return { name: "netzo.auth", middlewares: [ + { + path: "/", + middleware: { handler: setAuthState }, + }, { path: "/", middleware: { handler: setSessionState }, diff --git a/lib/plugins/auth/routes/auth.tsx b/lib/plugins/auth/routes/auth.tsx index f8754c06..a7f89753 100644 --- a/lib/plugins/auth/routes/auth.tsx +++ b/lib/plugins/auth/routes/auth.tsx @@ -1,4 +1,5 @@ import { defineRoute, type RouteConfig } from "$fresh/server.ts"; +import { cn } from "../../../components/utils.ts"; import { AuthForm } from "../islands/auth-form.tsx"; import { type AuthConfig } from "../plugin.ts"; @@ -13,11 +14,37 @@ export const config: RouteConfig = { export default (config: AuthConfig) => { return defineRoute((req, ctx) => { + const authFormWrapper = "grid gap-6 w-full xs:w-[350px] max-w-[350px]"; + if (config.image) { + return ( +
+
+
+ +
+
+
+ Netzo Authentication +
+
+ ); + } + return (
-
+
{config?.caption && ( diff --git a/lib/plugins/auth/routes/mod.ts b/lib/plugins/auth/routes/mod.ts index b030d728..6ab1a158 100644 --- a/lib/plugins/auth/routes/mod.ts +++ b/lib/plugins/auth/routes/mod.ts @@ -1,18 +1,11 @@ import type { PluginRoute } from "$fresh/server.ts"; import type { AuthConfig } from "../plugin.ts"; import { - type AuthUser, - createUser, - getUser, - updateUser, - updateUserSession, -} from "../utils/db.ts"; -import { - type AuthProvider, getAuthConfig, getFunctionsByProvider, getUserByProvider, } from "../utils/providers/mod.ts"; +import type { AuthProvider, AuthUser } from "../utils/types.ts"; export const getRoutesByProvider = ( provider: AuthProvider, @@ -32,18 +25,19 @@ export const getRoutesByProvider = ( }, { path: `/auth/${provider}/callback`, - handler: async (req, _ctx) => { + handler: async (req, ctx) => { const authConfig = getAuthConfig(provider, providerOptions); - const { response, tokens, sessionId } = await handleCallback( - req, - authConfig, - ); + const { + response, + tokens, + sessionId, + } = await handleCallback(req, authConfig); const userProvider = await getUserByProvider( provider, tokens.accessToken, ); - const userCurrent = await getUser(userProvider.authId); + const userCurrent = await ctx.state.auth.getUser(userProvider.authId); const user = { sessionId, @@ -56,10 +50,11 @@ export const getRoutesByProvider = ( } as unknown as AuthUser; if (userCurrent === null) { - await createUser(user); + await ctx.state.auth.createUser(user); } else { - await updateUser({ ...user, ...userCurrent }); - await updateUserSession({ ...user, ...userCurrent }, sessionId); + const data = { ...user, ...userCurrent }; + await ctx.state.auth.updateUser(data); + await ctx.state.auth.updateUserSession(data, sessionId); } return response; diff --git a/lib/plugins/auth/utils/adapters/datastore.ts b/lib/plugins/auth/utils/adapters/datastore.ts new file mode 100644 index 00000000..787eeccd --- /dev/null +++ b/lib/plugins/auth/utils/adapters/datastore.ts @@ -0,0 +1,70 @@ +import { ulid } from "../../../../datastore/mod.ts"; +import type { Auth, AuthUser } from "../types.ts"; + +const kv = await Deno.openKv(Deno.env.get("DENO_KV_PATH")); + +export const createDatastoreAuth = (): Auth => ({ + createUser: async (user: AuthUser) => { + user.id = ulid(); + user.createdAt = new Date().toISOString(); + user.updatedAt = user.createdAt; + user.deletedAt = ""; + const usersKey = ["$users", user.authId]; + const usersBySessionKey = ["$usersBySession", user.sessionId]; + + const atomicOp = kv.atomic() + .check({ key: usersKey, versionstamp: null }) + .check({ key: usersBySessionKey, versionstamp: null }) + .set(usersKey, user) + .set(usersBySessionKey, user); + + const res = await atomicOp.commit(); + if (!res.ok) throw new Error("Failed to create user"); + }, + updateUser: async (user: AuthUser) => { + user.updatedAt ||= new Date().toISOString(); + const usersKey = ["$users", user.authId]; + const usersBySessionKey = ["$usersBySession", user.sessionId]; + + const atomicOp = kv.atomic() + .set(usersKey, user) + .set(usersBySessionKey, user); + + const res = await atomicOp.commit(); + if (!res.ok) throw new Error("Failed to update user"); + }, + updateUserSession: async (user: AuthUser, sessionId: string) => { + user.updatedAt = new Date().toISOString(); + const userKey = ["$users", user.authId]; + const oldUserBySessionKey = ["$usersBySession", user.sessionId]; + const newUserBySessionKey = ["$usersBySession", sessionId]; + const newUser: AuthUser = { ...user, sessionId }; + + const atomicOp = kv.atomic() + .set(userKey, newUser) + .delete(oldUserBySessionKey) + .check({ key: newUserBySessionKey, versionstamp: null }) + .set(newUserBySessionKey, newUser); + + const res = await atomicOp.commit(); + if (!res.ok) throw new Error("Failed to update user session"); + }, + getUser: async (authId: string) => { + const res = await kv.get(["$users", authId]); + return res.value; + }, + getUserBySession: async (sessionId: string) => { + const key = ["$usersBySession", sessionId]; + const eventualRes = await kv.get(key, { + consistency: "eventual", + }); + if (eventualRes.value !== null) return eventualRes.value; + const res = await kv.get(key); + return res.value; + }, + listUsers: async (options?: Deno.KvListOptions) => { + const iterator = kv.list({ prefix: ["$users"] }, options); + const res = await Array.fromAsync(iterator); + return res.map((entry) => entry.value); + }, +}); diff --git a/lib/plugins/auth/utils/db.ts b/lib/plugins/auth/utils/db.ts deleted file mode 100644 index 9f7c8c1f..00000000 --- a/lib/plugins/auth/utils/db.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { ulid } from "../../../datastore/mod.ts"; -import type { AuthProvider } from "./providers/mod.ts"; - -const kv = await Deno.openKv(Deno.env.get("DENO_KV_PATH")); - -// users: - -export type AuthUser = { - id: string; - provider: AuthProvider; - authId: string; // id from auth provider - sessionId: string; - name: string; - email: string; - avatar: string; - roles: string[]; - createdAt: string; - updatedAt: string; - deletedAt: "" | string; -}; - -export type AuthUserFromProvider = Pick< - AuthUser, - "authId" | "name" | "email" | "avatar" | "provider" ->; - -/** - * Creates a new user in the database. Throws if the user or user session - * already exists. - * - * @example - * ```ts - * import { createUser } from "../../../../auth/utils/db.ts"; - * - * await createUser({ - * authId: "auth0|xxx", - * sessionId: crypto.randomUUID(), - * }); - * ``` - */ -export async function createUser(user: AuthUser) { - user.id = ulid(); - user.createdAt = new Date().toISOString(); - user.updatedAt = user.createdAt; - user.deletedAt = ""; - const usersKey = ["$users", user.authId]; - const usersBySessionKey = ["$usersBySession", user.sessionId]; - - const atomicOp = kv.atomic() - .check({ key: usersKey, versionstamp: null }) - .check({ key: usersBySessionKey, versionstamp: null }) - .set(usersKey, user) - .set(usersBySessionKey, user); - - const res = await atomicOp.commit(); - if (!res.ok) throw new Error("Failed to create user"); -} - -/** - * Creates a user in the database, overwriting any previous data. - * - * @example - * ```ts - * import { updateUser } from "../../../../auth/utils/db.ts"; - * - * await updateUser({ - * authId: "auth0|xxx", - * sessionId: crypto.randomUUID(), - * roles: ["admin"], - * }); - * ``` - */ -export async function updateUser(user: AuthUser) { - user.updatedAt ||= new Date().toISOString(); - const usersKey = ["$users", user.authId]; - const usersBySessionKey = ["$usersBySession", user.sessionId]; - - const atomicOp = kv.atomic() - .set(usersKey, user) - .set(usersBySessionKey, user); - - const res = await atomicOp.commit(); - if (!res.ok) throw new Error("Failed to update user"); -} - -/** - * Updates the session ID of a given user in the database. - * - * @example - * ```ts - * import { updateUserSession } from "../../../../auth/utils/db.ts"; - * - * await updateUserSession({ - * authId: "auth0|xxx", - * sessionId: "xxx", - * roles: ["admin"], - * }, "yyy"); - * ``` - */ -export async function updateUserSession(user: AuthUser, sessionId: string) { - user.updatedAt = new Date().toISOString(); - const userKey = ["$users", user.authId]; - const oldUserBySessionKey = ["$usersBySession", user.sessionId]; - const newUserBySessionKey = ["$usersBySession", sessionId]; - const newUser: AuthUser = { ...user, sessionId }; - - const atomicOp = kv.atomic() - .set(userKey, newUser) - .delete(oldUserBySessionKey) - .check({ key: newUserBySessionKey, versionstamp: null }) - .set(newUserBySessionKey, newUser); - - const res = await atomicOp.commit(); - if (!res.ok) throw new Error("Failed to update user session"); -} - -/** - * Gets the user with the given authId from the database. - * - * @example - * ```ts - * import { getUser } from "../../../../auth/utils/db.ts"; - * - * const user = await getUser("jack"); - * user?.authId; // Returns "auth0|xxx" - * user?.sessionId; // Returns "xxx" - * user?.roles; // Returns ["admin"] - * user?.provider; // Returns "github" - * ``` - */ -export async function getUser(authId: string) { - const res = await kv.get(["$users", authId]); - return res.value; -} - -/** - * Gets the user with the given session ID from the database. The first attempt - * is done with eventual consistency. If that returns `null`, the second - * attempt is done with strong consistency. This is done for performance - * reasons, as this function is called in every route request for checking - * whether the session user is signed in. - * - * @example - * ```ts - * import { getUserBySession } from "../../../../auth/utils/db.ts"; - * - * const user = await getUserBySession("xxx"); - * user?.authId; // Returns "auth0|xxx" - * user?.sessionId; // Returns "xxx" - * user?.roles; // Returns ["admin"] - * user?.provider; // Returns "github" - * ``` - */ -export async function getUserBySession(sessionId: string) { - const key = ["$usersBySession", sessionId]; - const eventualRes = await kv.get(key, { - consistency: "eventual", - }); - if (eventualRes.value !== null) return eventualRes.value; - const res = await kv.get(key); - return res.value; -} - -/** - * Returns a {@linkcode Deno.KvListIterator} which can be used to iterate over - * the users in the database. - * - * @example - * ```ts - * import { listUsers } from "../../../../auth/utils/db.ts"; - * - * for await (const entry of listUsers()) { - * entry.value.authId; // Returns "auth0|xxx" - * entry.value.sessionId; // Returns "xxx" - * entry.value.roles; // Returns ["admin"] - * entry.value.provider; // Returns "github" - * } - * ``` - */ -export function listUsers(options?: Deno.KvListOptions) { - return kv.list({ prefix: ["$users"] }, options); -} diff --git a/lib/plugins/auth/utils/providers/auth0.ts b/lib/plugins/auth/utils/providers/auth0.ts index f8d55e09..ce5ca948 100644 --- a/lib/plugins/auth/utils/providers/auth0.ts +++ b/lib/plugins/auth/utils/providers/auth0.ts @@ -1,5 +1,5 @@ import { createAuth0OAuthConfig } from "../../../../deps/deno_kv_oauth/mod.ts"; -import type { AuthUserFromProvider } from "../db.ts"; +import type { AuthUserFromProvider } from "../types.ts"; export { createAuth0OAuthConfig }; diff --git a/lib/plugins/auth/utils/providers/email.ts b/lib/plugins/auth/utils/providers/email.ts index 8737af0b..82a74ed9 100644 --- a/lib/plugins/auth/utils/providers/email.ts +++ b/lib/plugins/auth/utils/providers/email.ts @@ -1,4 +1,4 @@ -import type { AuthUserFromProvider } from "../db.ts"; +import type { AuthUserFromProvider } from "../types.ts"; type UserEmail = { _id: string; diff --git a/lib/plugins/auth/utils/providers/github.ts b/lib/plugins/auth/utils/providers/github.ts index 09327b23..7ddd205e 100644 --- a/lib/plugins/auth/utils/providers/github.ts +++ b/lib/plugins/auth/utils/providers/github.ts @@ -1,5 +1,5 @@ import { createGitHubOAuthConfig } from "../../../../deps/deno_kv_oauth/mod.ts"; -import type { AuthUserFromProvider } from "../db.ts"; +import type { AuthUserFromProvider } from "../types.ts"; export { createGitHubOAuthConfig }; diff --git a/lib/plugins/auth/utils/providers/gitlab.ts b/lib/plugins/auth/utils/providers/gitlab.ts index 2c3e8de4..049a3767 100644 --- a/lib/plugins/auth/utils/providers/gitlab.ts +++ b/lib/plugins/auth/utils/providers/gitlab.ts @@ -1,5 +1,5 @@ import { createGitLabOAuthConfig } from "../../../../deps/deno_kv_oauth/mod.ts"; -import type { AuthUserFromProvider } from "../db.ts"; +import type { AuthUserFromProvider } from "../types.ts"; export { createGitLabOAuthConfig }; diff --git a/lib/plugins/auth/utils/providers/google.ts b/lib/plugins/auth/utils/providers/google.ts index cee0bf39..f5149c64 100644 --- a/lib/plugins/auth/utils/providers/google.ts +++ b/lib/plugins/auth/utils/providers/google.ts @@ -1,5 +1,5 @@ import { createGoogleOAuthConfig } from "../../../../deps/deno_kv_oauth/mod.ts"; -import type { AuthUserFromProvider } from "../db.ts"; +import type { AuthUserFromProvider } from "../types.ts"; export { createGoogleOAuthConfig }; diff --git a/lib/plugins/auth/utils/providers/mod.ts b/lib/plugins/auth/utils/providers/mod.ts index aacd3acb..aab81c28 100644 --- a/lib/plugins/auth/utils/providers/mod.ts +++ b/lib/plugins/auth/utils/providers/mod.ts @@ -4,7 +4,7 @@ import { signOut, } from "../../../../deps/deno_kv_oauth/mod.ts"; import type { AuthConfig } from "../../plugin.ts"; -import { type AuthUserFromProvider } from "../db.ts"; +import type { AuthProvider, AuthUserFromProvider } from "../types.ts"; import { createAuth0OAuthConfig, getUserAuth0, isAuth0Setup } from "./auth0.ts"; import { createEmailClientConfig, @@ -38,8 +38,6 @@ import { import { createOktaOAuthConfig, getUserOkta, isOktaSetup } from "./okta.ts"; import { createSlackOAuthConfig, getUserSlack, isSlackSetup } from "./slack.ts"; -export type AuthProvider = keyof AuthConfig["providers"]; - const setFromOptionsIfNotInEnv = (name: string, value: string) => { if (!value) value = Deno.env.get(name)!; Deno.env.set(name, value); diff --git a/lib/plugins/auth/utils/providers/netzo.ts b/lib/plugins/auth/utils/providers/netzo.ts index 87f3544f..18df927c 100644 --- a/lib/plugins/auth/utils/providers/netzo.ts +++ b/lib/plugins/auth/utils/providers/netzo.ts @@ -1,5 +1,5 @@ import type { User as UserNetzo } from "../../../types.ts"; -import type { AuthUserFromProvider } from "../db.ts"; +import type { AuthUserFromProvider } from "../types.ts"; import { handleCallback } from "./netzo.handle_callback.ts"; import { signIn } from "./netzo.sign_in.ts"; diff --git a/lib/plugins/auth/utils/providers/okta.ts b/lib/plugins/auth/utils/providers/okta.ts index 037c8aa3..c5738b04 100644 --- a/lib/plugins/auth/utils/providers/okta.ts +++ b/lib/plugins/auth/utils/providers/okta.ts @@ -1,5 +1,5 @@ import { createOktaOAuthConfig } from "../../../../deps/deno_kv_oauth/mod.ts"; -import type { AuthUserFromProvider } from "../db.ts"; +import type { AuthUserFromProvider } from "../types.ts"; export { createOktaOAuthConfig }; diff --git a/lib/plugins/auth/utils/providers/slack.ts b/lib/plugins/auth/utils/providers/slack.ts index 7df11a65..6416a7e9 100644 --- a/lib/plugins/auth/utils/providers/slack.ts +++ b/lib/plugins/auth/utils/providers/slack.ts @@ -1,5 +1,5 @@ import { createSlackOAuthConfig } from "../../../../deps/deno_kv_oauth/mod.ts"; -import type { AuthUserFromProvider } from "../db.ts"; +import type { AuthUserFromProvider } from "../types.ts"; export { createSlackOAuthConfig }; diff --git a/lib/plugins/auth/utils/types.ts b/lib/plugins/auth/utils/types.ts new file mode 100644 index 00000000..89ca5ff9 --- /dev/null +++ b/lib/plugins/auth/utils/types.ts @@ -0,0 +1,121 @@ +import { AuthConfig } from "../plugin.ts"; + +export type AuthProvider = keyof AuthConfig["providers"]; + +export type AuthUser = { + id: string; + provider: AuthProvider; + authId: string; // id from auth provider + sessionId: string; + name: string; + email: string; + avatar: string; + roles: string[]; + createdAt: string; + updatedAt: string; + deletedAt: "" | string; +}; + +export type AuthUserFromProvider = Pick< + AuthUser, + "authId" | "name" | "email" | "avatar" | "provider" +>; + +export type Auth = { + /** + * Creates a new user in the database. Throws if the user or user session + * already exists. + * + * @example + * ```ts + * import { createUser } from "../../../../auth/utils/db.ts"; + * + * await createUser({ + * authId: "auth0|xxx", + * sessionId: crypto.randomUUID(), + * }); + * ``` + */ + createUser: (user: AuthUser) => Promise; + /** + * Creates a user in the database, overwriting any previous data. + * + * @example + * ```ts + * import { updateUser } from "../../../../auth/utils/db.ts"; + * + * await updateUser({ + * authId: "auth0|xxx", + * sessionId: crypto.randomUUID(), + * roles: ["admin"], + * }); + * ``` + */ + updateUser: (user: AuthUser) => Promise; + /** + * Updates the session ID of a given user in the database. + * + * @example + * ```ts + * import { updateUserSession } from "../../../../auth/utils/db.ts"; + * + * await updateUserSession({ + * authId: "auth0|xxx", + * sessionId: "xxx", + * roles: ["admin"], + * }, "yyy"); + * ``` + */ + updateUserSession: (user: AuthUser, sessionId: string) => Promise; + /** + * Gets the user with the given authId from the database. + * + * @example + * ```ts + * import { getUser } from "../../../../auth/utils/db.ts"; + * + * const user = await getUser("jack"); + * user?.authId; // Returns "auth0|xxx" + * user?.sessionId; // Returns "xxx" + * user?.roles; // Returns ["admin"] + * user?.provider; // Returns "github" + * ``` + */ + getUser: (authId: string) => Promise; + /** + * Gets the user with the given session ID from the database. The first attempt + * is done with eventual consistency. If that returns `null`, the second + * attempt is done with strong consistency. This is done for performance + * reasons, as this function is called in every route request for checking + * whether the session user is signed in. + * + * @example + * ```ts + * import { getUserBySession } from "../../../../auth/utils/db.ts"; + * + * const user = await getUserBySession("xxx"); + * user?.authId; // Returns "auth0|xxx" + * user?.sessionId; // Returns "xxx" + * user?.roles; // Returns ["admin"] + * user?.provider; // Returns "github" + * ``` + */ + getUserBySession: (sessionId: string) => Promise; + /** + * Returns a {@linkcode Deno.KvListIterator} which can be used to iterate over + * the users in the database. + * + * @example + * ```ts + * import { listUsers } from "../../../../auth/utils/db.ts"; + * + * for await (const entry of listUsers()) { + * entry.value.authId; // Returns "auth0|xxx" + * entry.value.sessionId; // Returns "xxx" + * entry.value.roles; // Returns ["admin"] + * entry.value.provider; // Returns "github" + * } + * ``` + */ + listUsers: (options?: Deno.KvListOptions) => Promise; +}; diff --git a/lib/plugins/unocss/preset-shadcn/mod.ts b/lib/plugins/unocss/preset-shadcn/mod.ts index 8a42659c..a446e7ad 100644 --- a/lib/plugins/unocss/preset-shadcn/mod.ts +++ b/lib/plugins/unocss/preset-shadcn/mod.ts @@ -4,11 +4,8 @@ import type { VariantContext, VariantObject, } from "../../../deps/@unocss/core.ts"; -import { - h, - variantGetParameter, - type Theme, -} from "../../../deps/@unocss/preset-mini.ts"; +import { h, type Theme } from "../../../deps/@unocss/preset-mini.ts"; +import { variantGetParameter } from "../../../deps/@unocss/utils.ts"; import { generateCSSVars } from "./generate.ts"; import { PresetShadcnOptions } from "./types.ts"; @@ -245,7 +242,7 @@ export function presetShadcn(options: PresetShadcnOptions = {}): Preset { ], // combobox: ...[ - 'whitespace-normal', + "whitespace-normal", "w-full", "justify-between", "hover:bg-secondary/20",