diff --git a/.changeset/chatty-kangaroos-rule.md b/.changeset/chatty-kangaroos-rule.md new file mode 100644 index 000000000..962f4a1eb --- /dev/null +++ b/.changeset/chatty-kangaroos-rule.md @@ -0,0 +1,119 @@ +--- +"@studiocms/dashboard": patch +"@studiocms/auth": patch +"@studiocms/core": patch +"@studiocms/ui": patch +"studiocms": patch +--- + +Auth system overhaul: + +## **`studiocms`** + +- Updated all Dependencies + +## **`@studiocms/auth`** + +- Update `astro:env` schema: + - `CMS_ENCRYPTION_KEY`: NEW - Required variable used for auth encryption, can be generated using `openssl rand --base64 16`. + - `CMS_GITHUB_REDIRECT_URI`: NEW - Optional variable for GitHub Redirect URI if using multiple redirect URIs with Github oAuth. +- Removed `Luicia` based auth system and `Lucia-astrodb-adapter` +- Removed old `authHelper` +- Add new OAuthButton components + - `` + - `` + - `oAuthButtonProviders.ts` +- Add new `` component and CSS +- Add new authentication library: + - Auth library is built using the lucia-next resources and will now be maintained under `@studiocms/auth` as its own full module + - Created Virtual module exports available during runtime +- Add new login/signup backgrounds +- Remove Middleware +- Add `studiocms-logo.glb` for usage with New ThreeJS login/signup page +- Update all Auth Routes +- Update schema +- Add new Scripts for ThreeJS +- Update Stubs files and Utils +- Refactor Integration to use new system. + +## **`@studiocms/core`** + +- Disable interactivity for `` component. (Will always show a empty profile icon until we setup the new system for the front-end) +- Update table schema: + - `StudioCMSUsers`: Removed oAuth ID's from main user table + + ```diff + export const StudioCMSUsers = defineTable({ + columns: { + id: column.text({ primaryKey: true }), + url: column.text({ optional: true }), + name: column.text(), + email: column.text({ unique: true, optional: true }), + avatar: column.text({ optional: true }), + - githubId: column.number({ unique: true, optional: true }), + - githubURL: column.text({ optional: true }), + - discordId: column.text({ unique: true, optional: true }), + - googleId: column.text({ unique: true, optional: true }), + - auth0Id: column.text({ unique: true, optional: true }), + username: column.text(), + password: column.text({ optional: true }), + updatedAt: column.date({ default: NOW, optional: true }), + createdAt: column.date({ default: NOW, optional: true }), + }, + }); + ``` + + - `StudioCMSOAuthAccounts`: New table to handle all oAuth accounts and linking to Users + + ```ts + export const StudioCMSOAuthAccounts = defineTable({ + columns: { + provider: column.text(), // github, google, discord, auth0 + providerUserId: column.text({ primaryKey: true }), + userId: column.text({ references: () => StudioCMSUsers.columns.id }), + }, + }); + ``` + + - `StudioCMSPermissions`: Updated to use direct reference to users table + + ```ts + export const StudioCMSPermissions = defineTable({ + columns: { + user: column.text({ references: () => StudioCMSUsers.columns.id }), + rank: column.text(), + }, + }); + ``` + + - `StudioCMSSiteConfig`: Added new options for login page + + ```ts + export const StudioCMSSiteConfig = defineTable({ + columns: { + id: column.number({ primaryKey: true }), + title: column.text(), + description: column.text(), + defaultOgImage: column.text({ optional: true }), + siteIcon: column.text({ optional: true }), + loginPageBackground: column.text({ default: 'studiocms-curves' }), + loginPageCustomImage: column.text({ optional: true }), + }, + }); + ``` + +- Updated Routemap: + - All Auth api routes are now located at `yourhost.tld/studiocms_api/auth/*` + +- Updated Strings: + - Add new Encryption messages for the new `CMS_ENCRYPTION_KEY` variable + +- Removed now unused auth types. + +## **`@studiocms/dashboard`** + +- Refactor to utilize new `@studiocms/auth` lib for user verification + +## **`@studiocms/ui`** + +- Update `` component's available types \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 450627957..44fcf1104 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ "editor.defaultFormatter": "biomejs.biome", "[mdx]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "editor.gotoLocation.multipleDefinitions": "goto" } diff --git a/biome.json b/biome.json index 192101cf9..f5643ec91 100644 --- a/biome.json +++ b/biome.json @@ -8,7 +8,7 @@ }, "files": { "ignoreUnknown": true, - "ignore": ["**/.astro/**", "**/package.json", "**/dist/**", "**/ogBackgrounds/**"] + "ignore": ["**/.astro/**", "**/package.json", "**/dist/**"] }, "formatter": { "lineWidth": 100, diff --git a/packages/studiocms/package.json b/packages/studiocms/package.json index cbe8b6377..a09334d7e 100644 --- a/packages/studiocms/package.json +++ b/packages/studiocms/package.json @@ -43,14 +43,6 @@ }, "type": "module", "dependencies": { - "@cloudinary/url-gen": "catalog:studiocms-imagehandler", - "@inox-tools/runtime-logger": "catalog:studiocms-shared", - "@markdoc/markdoc": "catalog:studiocms-shared", - "@matthiesenxyz/astrolace": "catalog:studiocms-shared", - "@matthiesenxyz/integration-utils": "catalog:studiocms-shared", - "@matthiesenxyz/unocss-preset-daisyui": "catalog:studiocms-shared", - "@noble/hashes": "catalog:studiocms-shared", - "@shikijs/transformers": "catalog:studiocms-renderer", "@studiocms/assets": "workspace:*", "@studiocms/auth": "workspace:*", "@studiocms/betaresources": "workspace:*", @@ -60,23 +52,48 @@ "@studiocms/imagehandler": "workspace:*", "@studiocms/renderers": "workspace:*", "@studiocms/robotstxt": "workspace:*", - "@unocss/astro": "catalog:studiocms-shared", - "@unocss/reset": "catalog:studiocms-shared", + "astro-integration-kit": "catalog:", - "arctic": "catalog:studiocms-shared", - "daisyui": "catalog:studiocms-shared", - "lucia": "catalog:studiocms-shared", - "marked": "catalog:studiocms-shared", + + "package-json": "catalog:studiocms", + "semver": "catalog:studiocms", + + "mrmime": "catalog:studiocms-core", + "remark-rehype": "catalog:studiocms-core", + "mdast-util-to-hast": "catalog:studiocms-core", + + "@oslojs/crypto": "catalog:studiocms-auth", + "@oslojs/encoding": "catalog:studiocms-auth", + "@oslojs/binary": "catalog:studiocms-auth", + "@types/bcryptjs": "catalog:studiocms-auth", + "bcryptjs": "catalog:studiocms-auth", + "@types/three": "catalog:studiocms-auth", + "arctic": "catalog:studiocms-auth", + "three": "catalog:studiocms-auth", + + "@fontsource-variable/onest": "catalog:studiocms-shared", + "@inox-tools/runtime-logger": "catalog:studiocms-shared", + "@matthiesenxyz/astrodtsbuilder": "catalog:studiocms-shared", + "@matthiesenxyz/integration-utils": "catalog:studiocms-shared", + "rollup-plugin-copy": "catalog:studiocms-shared", + + "marked": "catalog:studiocms-renderer", "marked-alert": "catalog:studiocms-renderer", "marked-emoji": "catalog:studiocms-renderer", "marked-footnote": "catalog:studiocms-renderer", "marked-shiki": "catalog:studiocms-renderer", "marked-smartypants": "catalog:studiocms-renderer", - "micromatch": "catalog:studiocms-shared", - "mrmime": "catalog:studiocms-shared", - "package-json": "catalog:studiocms", - "semver": "catalog:studiocms", - "shiki": "catalog:studiocms-shared", + "@markdoc/markdoc": "catalog:studiocms-renderer", + "shiki": "catalog:studiocms-renderer", + "@shikijs/transformers": "catalog:studiocms-renderer", + + "@cloudinary/url-gen": "catalog:studiocms-imagehandler", + + "@matthiesenxyz/astrolace": "catalog:studiocms-shared", + "@matthiesenxyz/unocss-preset-daisyui": "catalog:studiocms-shared", + "@unocss/astro": "catalog:studiocms-shared", + "@unocss/reset": "catalog:studiocms-shared", + "daisyui": "catalog:studiocms-shared", "unocss": "catalog:studiocms-shared" }, "peerDependencies": { @@ -92,7 +109,6 @@ "devDependencies": { "vite": "catalog:", "typescript": "catalog:", - "@types/micromatch": "catalog:studiocms-shared", "@types/semver": "catalog:studiocms" } } diff --git a/packages/studiocms_auth/env.d.ts b/packages/studiocms_auth/env.d.ts index 7c8c624f5..86fd4d5bb 100644 --- a/packages/studiocms_auth/env.d.ts +++ b/packages/studiocms_auth/env.d.ts @@ -6,36 +6,3 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; } - -declare namespace App { - interface Locals { - isLoggedIn: boolean; - dbUser?: { - id: string; - url: string | null; - name: string; - email: string | null; - avatar: string | null; - githubId: number | null; - githubURL: string | null; - discordId: string | null; - googleId: string | null; - auth0Id: string | null; - username: string; - password: string | null; - updatedAt: Date | null; - createdAt: Date | null; - } | null; - user?: { - id: string; - username?: string; - githubId?: number; - } | null; - session?: { - id: string; - userId: string; - fresh: boolean; - expiresAt: Date; - } | null; - } -} diff --git a/packages/studiocms_auth/package.json b/packages/studiocms_auth/package.json index 0d4fb3851..35f9c0d3b 100644 --- a/packages/studiocms_auth/package.json +++ b/packages/studiocms_auth/package.json @@ -30,32 +30,33 @@ "src" ], "exports": { - ".": "./src/index.ts", - "./lucia": "./src/auth/index.ts" + ".": "./src/index.ts" }, "type": "module", "dependencies": { + "@studiocms/core": "workspace:*", + "@studiocms/ui": "workspace:*", + "astro-integration-kit": "catalog:", + "@fontsource-variable/onest": "catalog:studiocms-shared", "@inox-tools/runtime-logger": "catalog:studiocms-shared", - "@matthiesenxyz/astrolace": "catalog:studiocms-shared", "@matthiesenxyz/astrodtsbuilder": "catalog:studiocms-shared", "@matthiesenxyz/integration-utils": "catalog:studiocms-shared", - "@noble/hashes": "catalog:studiocms-shared", - "@studiocms/assets": "workspace:*", - "@studiocms/core": "workspace:*", - "micromatch": "catalog:studiocms-shared", - "lucia": "catalog:studiocms-shared", - "arctic": "catalog:studiocms-shared", - "astro-integration-kit": "catalog:" + "rollup-plugin-copy": "catalog:studiocms-shared", + "@oslojs/binary": "catalog:studiocms-auth", + "@oslojs/crypto": "catalog:studiocms-auth", + "@oslojs/encoding": "catalog:studiocms-auth", + "@types/three": "catalog:studiocms-auth", + "arctic": "catalog:studiocms-auth", + "three": "catalog:studiocms-auth", + "@types/bcryptjs": "catalog:studiocms-auth", + "bcryptjs": "catalog:studiocms-auth" }, "peerDependencies": { - "@studiocms/core": "workspace:*", - "@studiocms/dashboard": "workspace:*", "@astrojs/db": "catalog:min", "astro": "catalog:min" }, "devDependencies": { "vite": "catalog:", - "typescript": "catalog:", - "@types/micromatch": "catalog:studiocms-shared" + "typescript": "catalog:" } } diff --git a/packages/studiocms_auth/src/astroenv/env.ts b/packages/studiocms_auth/src/astroenv/env.ts index 369f7b9ab..e72998b95 100644 --- a/packages/studiocms_auth/src/astroenv/env.ts +++ b/packages/studiocms_auth/src/astroenv/env.ts @@ -4,6 +4,12 @@ import { envField } from 'astro/config'; export const astroENV: AstroConfig['experimental']['env'] = { validateSecrets: true, schema: { + // Auth Encryption Key + CMS_ENCRYPTION_KEY: envField.string({ + context: 'server', + access: 'secret', + optional: false, + }), // GitHub Auth Provider Environment Variables CMS_GITHUB_CLIENT_ID: envField.string({ context: 'server', @@ -15,6 +21,11 @@ export const astroENV: AstroConfig['experimental']['env'] = { access: 'secret', optional: true, }), + CMS_GITHUB_REDIRECT_URI: envField.string({ + context: 'server', + access: 'secret', + optional: true, + }), // Discord Auth Provider Environment Variables CMS_DISCORD_CLIENT_ID: envField.string({ context: 'server', diff --git a/packages/studiocms_auth/src/auth/index.ts b/packages/studiocms_auth/src/auth/index.ts deleted file mode 100644 index 8d99f5086..000000000 --- a/packages/studiocms_auth/src/auth/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { db } from 'astro:db'; -import { tsSessionTable, tsUsers } from '@studiocms/core/db/tsTables'; -import { Lucia, TimeSpan } from 'lucia'; -import { AstroDBAdapter } from './lucia-astrodb-adapter'; - -const adapter = new AstroDBAdapter(db, tsSessionTable, tsUsers); -export const lucia = new Lucia(adapter, { - sessionExpiresIn: new TimeSpan(2, 'w'), - sessionCookie: { - attributes: { - secure: import.meta.env.PROD, - }, - }, - getUserAttributes: (attributes) => { - return { - // attributes has the type of DatabaseUserAttributes - id: attributes.id, - username: attributes.username, - }; - }, -}); - -declare module 'lucia' { - interface Register { - Lucia: typeof lucia; - DatabaseUserAttributes: DatabaseUserAttributes; - } -} - -interface DatabaseUserAttributes { - username: string; - id: string; -} diff --git a/packages/studiocms_auth/src/auth/lucia-astrodb-adapter.ts b/packages/studiocms_auth/src/auth/lucia-astrodb-adapter.ts deleted file mode 100644 index a66d82214..000000000 --- a/packages/studiocms_auth/src/auth/lucia-astrodb-adapter.ts +++ /dev/null @@ -1,179 +0,0 @@ -/// -import { eq, lte } from 'astro:db'; - -import type { Database, Table } from '@astrojs/db/runtime'; -import type { Adapter, DatabaseSession, DatabaseUser, UserId } from 'lucia'; - -export class AstroDBAdapter implements Adapter { - private db: Database; - - private sessionTable: SessionTable; - private userTable: UserTable; - - constructor(db: Database, sessionTable: SessionTable, userTable: UserTable) { - this.db = db; - this.sessionTable = sessionTable; - this.userTable = userTable; - } - - public async deleteSession(sessionId: string): Promise { - await this.db.delete(this.sessionTable).where(eq(this.sessionTable.id, sessionId)); - } - - public async deleteUserSessions(userId: UserId): Promise { - await this.db.delete(this.sessionTable).where(eq(this.sessionTable.userId, userId)); - } - - public async getSessionAndUser( - sessionId: string - ): Promise<[session: DatabaseSession | null, user: DatabaseUser | null]> { - const result = await this.db - .select({ - user: this.userTable, - session: this.sessionTable, - }) - .from(this.sessionTable) - .innerJoin(this.userTable, eq(this.sessionTable.userId, this.userTable.id)) - .where(eq(this.sessionTable.id, sessionId)) - .get(); - if (!result) return [null, null]; - return [transformIntoDatabaseSession(result.session), transformIntoDatabaseUser(result.user)]; - } - - public async getUserSessions(userId: UserId): Promise { - const result = await this.db - .select() - .from(this.sessionTable) - .where(eq(this.sessionTable.userId, userId)) - .all(); - return result.map((val) => { - return transformIntoDatabaseSession(val); - }); - } - - public async setSession(session: DatabaseSession): Promise { - await this.db - .insert(this.sessionTable) - .values({ - id: session.id, - userId: session.userId, - expiresAt: session.expiresAt, - ...session.attributes, - }) - .run(); - } - - public async updateSessionExpiration(sessionId: string, expiresAt: Date): Promise { - await this.db - .update(this.sessionTable) - .set({ - expiresAt: expiresAt, - }) - .where(eq(this.sessionTable.id, sessionId)) - .run(); - } - - public async deleteExpiredSessions(): Promise { - await this.db.delete(this.sessionTable).where(lte(this.sessionTable.expiresAt, new Date())); - } -} - -// biome-ignore lint/suspicious/noExplicitAny: -function transformIntoDatabaseSession(raw: any): DatabaseSession { - const { id, userId, expiresAt, ...attributes } = raw; - return { - userId, - id, - expiresAt, - attributes, - }; -} - -// biome-ignore lint/suspicious/noExplicitAny: -function transformIntoDatabaseUser(raw: any): DatabaseUser { - const { id, ...attributes } = raw; - return { - id, - attributes, - }; -} - -export type UserTable = Table< - // biome-ignore lint/suspicious/noExplicitAny: - any, - { - id: { - type: UserIdColumnType; - schema: { - unique: false; - // biome-ignore lint/suspicious/noExplicitAny: - deprecated: any; - // biome-ignore lint/suspicious/noExplicitAny: - name: any; - // biome-ignore lint/suspicious/noExplicitAny: - collection: any; - primaryKey: true; - }; - }; - } ->; - -export type SessionTable = Table< - // biome-ignore lint/suspicious/noExplicitAny: - any, - { - id: { - type: 'text'; - schema: { - unique: false; - // biome-ignore lint/suspicious/noExplicitAny: - deprecated: any; - // biome-ignore lint/suspicious/noExplicitAny: - name: any; - // biome-ignore lint/suspicious/noExplicitAny: - collection: any; - primaryKey: true; - }; - }; - expiresAt: { - type: 'date'; - schema: { - optional: false; - unique: false; - // biome-ignore lint/suspicious/noExplicitAny: - deprecated: any; - // biome-ignore lint/suspicious/noExplicitAny: - name: any; - // biome-ignore lint/suspicious/noExplicitAny: - collection: any; - }; - }; - userId: { - type: UserIdColumnType; - schema: { - unique: false; - deprecated: false; - // biome-ignore lint/suspicious/noExplicitAny: - name: any; - // biome-ignore lint/suspicious/noExplicitAny: - collection: any; - primaryKey: false; - optional: false; - references: { - type: UserIdColumnType; - schema: { - unique: false; - deprecated: false; - // biome-ignore lint/suspicious/noExplicitAny: - name: any; - // biome-ignore lint/suspicious/noExplicitAny: - collection: any; - primaryKey: true; - }; - }; - }; - }; - } ->; - -type UserIdColumnType = UserId extends string ? 'text' : UserId extends number ? 'number' : never; diff --git a/packages/studiocms_auth/src/components/OAuthButton.astro b/packages/studiocms_auth/src/components/OAuthButton.astro new file mode 100644 index 000000000..d36d78531 --- /dev/null +++ b/packages/studiocms_auth/src/components/OAuthButton.astro @@ -0,0 +1,21 @@ +--- +import { Button } from '@studiocms/ui/components'; + +interface Props { + href: string; + label: string; + image: string; +} + +const { href, label, image } = Astro.props; +--- + + + \ No newline at end of file diff --git a/packages/studiocms_auth/src/components/OAuthButtonStack.astro b/packages/studiocms_auth/src/components/OAuthButtonStack.astro new file mode 100644 index 000000000..731c4f205 --- /dev/null +++ b/packages/studiocms_auth/src/components/OAuthButtonStack.astro @@ -0,0 +1,15 @@ +--- +import { Divider } from '@studiocms/ui/components'; +import OAuthButton from './OAuthButton.astro'; +import { providerData, showOAuth } from './oAuthButtonProviders'; + +const shouldShowOAuth = showOAuth && providerData.some(({ enabled }) => enabled); +--- +{ shouldShowOAuth && ( + or log in using +
+ { + providerData.map(({enabled, ...props}) => enabled && ) + } +
+)} \ No newline at end of file diff --git a/packages/studiocms_auth/src/components/StudioCMSLogoSVG.astro b/packages/studiocms_auth/src/components/StudioCMSLogoSVG.astro new file mode 100644 index 000000000..639329a7c --- /dev/null +++ b/packages/studiocms_auth/src/components/StudioCMSLogoSVG.astro @@ -0,0 +1,13 @@ +--- +import type { HTMLAttributes } from 'astro/types'; + +interface Props extends HTMLAttributes<'svg'> {} + +const { class: className } = Astro.props; +--- + + + + + + diff --git a/packages/studiocms_auth/src/components/oAuthButtonProviders.ts b/packages/studiocms_auth/src/components/oAuthButtonProviders.ts new file mode 100644 index 000000000..ca56993a9 --- /dev/null +++ b/packages/studiocms_auth/src/components/oAuthButtonProviders.ts @@ -0,0 +1,57 @@ +import { authEnvCheck } from 'studiocms:auth/utils/authEnvCheck'; +import { StudioCMSRoutes } from 'studiocms:helpers/routemap'; +import Config from 'virtual:studiocms/config'; + +const { + dashboardConfig: { + AuthConfig: { providers }, + }, +} = Config; + +const { + authLinks: { googleIndex, auth0Index, discordIndex, githubIndex }, +} = StudioCMSRoutes; + +const { + DISCORD: { ENABLED: discordEnabled }, + GITHUB: { ENABLED: githubEnabled }, + GOOGLE: { ENABLED: googleEnabled }, + AUTH0: { ENABLED: auth0Enabled }, + SHOW_OAUTH, +} = await authEnvCheck(providers); + +export const showOAuth = SHOW_OAUTH; + +type ProviderData = { + enabled: boolean; + href: string; + label: string; + image: string; +}; + +export const providerData: ProviderData[] = [ + { + enabled: githubEnabled, + href: githubIndex, + label: 'GitHub', + image: ``, + }, + { + enabled: discordEnabled, + href: discordIndex, + label: 'Discord', + image: ``, + }, + { + enabled: googleEnabled, + href: googleIndex, + label: 'Google', + image: ``, + }, + { + enabled: auth0Enabled, + href: auth0Index, + label: 'Auth0', + image: ``, + }, +]; diff --git a/packages/studiocms_auth/src/helpers/authHelper.ts b/packages/studiocms_auth/src/helpers/authHelper.ts deleted file mode 100644 index 1a5ae014b..000000000 --- a/packages/studiocms_auth/src/helpers/authHelper.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { db, sql } from 'astro:db'; -import { tsPermissions } from '@studiocms/core/db/tsTables'; -import type { Session } from 'lucia'; -import { lucia } from '../auth'; - -type authHelperResponse = { - id: string; - username: string | null; - name: string | null; - email: string | null; - avatar: string | null; - githubURL: string | null; - permissionLevel: 'admin' | 'editor' | 'visitor' | 'unknown'; - currentUserSession: Session | undefined; -}; - -/** - * # Auth Helper Function - * - * @param locals The Astro.locals object - * @returns The current user data and session information and permission level - */ -export default async function authHelper(locals: App.Locals): Promise { - let user: authHelperResponse = { - id: '', - permissionLevel: 'unknown', - username: null, - name: null, - email: null, - avatar: null, - githubURL: null, - currentUserSession: undefined, - }; - - if (locals.isLoggedIn && locals.dbUser) { - let permissionLevel: 'admin' | 'editor' | 'visitor' | 'unknown' = 'unknown'; - const permissions = await db - .select() - .from(tsPermissions) - .where(sql`lower(${tsPermissions.username}) = ${locals.dbUser.username.toLowerCase()}`) - .get(); - - if ( - permissions && - permissions.username.toLowerCase() === locals.dbUser.username.toLowerCase() - ) { - switch (permissions.rank) { - case 'admin': - permissionLevel = 'admin'; - break; - case 'editor': - permissionLevel = 'editor'; - break; - case 'visitor': - permissionLevel = 'visitor'; - break; - default: - permissionLevel = 'unknown'; - break; - } - } - - user = { - id: locals.dbUser.id, - username: locals.dbUser.username, - name: locals.dbUser.name, - email: locals.dbUser.email, - avatar: locals.dbUser.avatar, - githubURL: locals.dbUser.githubURL, - currentUserSession: (await lucia.getUserSessions(locals.dbUser.id))[0], - permissionLevel, - }; - } - - return user; -} diff --git a/packages/studiocms_auth/src/integration.ts b/packages/studiocms_auth/src/integration.ts index 4b41f340b..9d2471ac6 100644 --- a/packages/studiocms_auth/src/integration.ts +++ b/packages/studiocms_auth/src/integration.ts @@ -1,21 +1,40 @@ import { runtimeLogger } from '@inox-tools/runtime-logger'; import { integrationLogger } from '@matthiesenxyz/integration-utils/astroUtils'; -import { AuthProviderLogStrings, DashboardStrings } from '@studiocms/core/strings'; +import { DashboardStrings } from '@studiocms/core/strings'; import { addAstroEnvConfig } from '@studiocms/core/utils'; import { addVirtualImports, createResolver, defineIntegration } from 'astro-integration-kit'; +import copy from 'rollup-plugin-copy'; import { name } from '../package.json'; import { astroENV } from './astroenv/env'; import { StudioCMSAuthOptionsSchema } from './schema'; -import authConfigDTS from './stubs/auth-config'; -import authHelperDTS from './stubs/auth-helpers'; +import authLibDTS from './stubs/auth-lib'; +import authScriptsDTS from './stubs/auth-scripts'; +import authUtilsDTS from './stubs/auth-utils'; import { checkEnvKeys } from './utils/checkENV'; -import { injectAuthRouteArray } from './utils/injectAuthRoutes'; -import { usernameAndPasswordAuthConfig } from './utils/studioauth-config'; +import { injectAuthAPIRoutes, injectAuthPageRoutes } from './utils/routeBuilder'; export default defineIntegration({ name, optionsSchema: StudioCMSAuthOptionsSchema, - setup({ name, options }) { + setup({ + name, + options, + options: { + dashboardConfig: { + dashboardEnabled, + AuthConfig: { + providers: { + github: githubAPI, + discord: discordAPI, + google: googleAPI, + auth0: auth0API, + usernameAndPassword: usernameAndPasswordAPI, + usernameAndPasswordConfig: { allowUserRegistration }, + }, + }, + }, + }, + }) { // Create resolver relative to this file const { resolve } = createResolver(import.meta.url); @@ -23,7 +42,7 @@ export default defineIntegration({ hooks: { 'astro:config:setup': async (params) => { // Destructure Params - const { logger } = params; + const { logger, updateConfig } = params; // Log that Setup is Starting integrationLogger( @@ -40,132 +59,133 @@ export default defineIntegration({ // Update Astro Config with Environment Variables (`astro:env`) addAstroEnvConfig(params, astroENV); - // If Username and Password Auth is enabled Verify the Auth Config File Exists and is setup! - usernameAndPasswordAuthConfig(params, { options, name }); - // injectAuthHelper addVirtualImports(params, { name, imports: { - 'studiocms:auth/helpers': `export { default as authHelper } from '${resolve('./helpers/authHelper.ts')}'`, + 'studiocms:auth/lib/encryption': `export * from '${resolve('./lib/encryption.ts')}'`, + 'studiocms:auth/lib/password': `export * from '${resolve('./lib/password.ts')}'`, + 'studiocms:auth/lib/rate-limit': `export * from '${resolve('./lib/rate-limit.ts')}'`, + 'studiocms:auth/lib/session': `export * from '${resolve('./lib/session.ts')}'`, + 'studiocms:auth/lib/types': `export * from '${resolve('./lib/types.ts')}'`, + 'studiocms:auth/lib/user': `export * from '${resolve('./lib/user.ts')}'`, + 'studiocms:auth/utils/authEnvCheck': `export * from '${resolve('./utils/authEnvCheck.ts')}'`, + 'studiocms:auth/utils/validImages': `export * from '${resolve('./utils/validImages.ts')}'`, + 'studiocms:auth/scripts/three': `import ${JSON.stringify(resolve('./scripts/three.ts'))}`, + 'studiocms:auth/scripts/formListener': `export * from '${resolve('./scripts/formListener.ts')}'`, }, }); - // Inject Routes - injectAuthRouteArray(params, { + // Update Astro Config + updateConfig({ + security: { + checkOrigin: true, + }, + experimental: { + directRenderScript: true, + }, + vite: { + optimizeDeps: { + exclude: ['astro:db', 'three'], + }, + plugins: [ + copy({ + copyOnce: true, + hook: 'buildStart', + targets: [ + { + src: resolve('./resources/*'), + dest: 'public/studiocms-auth/', + }, + ], + }), + ], + }, + }); + + // Inject API Routes + injectAuthAPIRoutes(params, { options, - middleware: resolve('./middleware/index.ts'), - providerRoutes: [ + routes: [ + { + pattern: 'login', + entrypoint: resolve('./routes/api/login.ts'), + enabled: usernameAndPasswordAPI, + }, + { + pattern: 'register', + entrypoint: resolve('./routes/api/register.ts'), + enabled: usernameAndPasswordAPI && allowUserRegistration, + }, { - enabled: options.dashboardConfig.AuthConfig.providers.github, - logs: AuthProviderLogStrings.githubLogs, - routes: [ - { - pattern: 'login/github', - entrypoint: resolve('./routes/login/github/index.ts'), - }, - { - pattern: 'login/github/callback', - entrypoint: resolve('./routes/login/github/callback.ts'), - }, - ], + pattern: 'github', + entrypoint: resolve('./routes/api/github/index.ts'), + enabled: githubAPI, }, { - enabled: options.dashboardConfig.AuthConfig.providers.discord, - logs: AuthProviderLogStrings.discordLogs, - routes: [ - { - pattern: 'login/discord', - entrypoint: resolve('./routes/login/discord/index.ts'), - }, - { - pattern: 'login/discord/callback', - entrypoint: resolve('./routes/login/discord/callback.ts'), - }, - ], + pattern: 'github/callback', + entrypoint: resolve('./routes/api/github/callback.ts'), + enabled: githubAPI, }, { - enabled: options.dashboardConfig.AuthConfig.providers.google, - logs: AuthProviderLogStrings.googleLogs, - routes: [ - { - pattern: 'login/google', - entrypoint: resolve('./routes/login/google/index.ts'), - }, - { - pattern: 'login/google/callback', - entrypoint: resolve('./routes/login/google/callback.ts'), - }, - ], + pattern: 'discord', + entrypoint: resolve('./routes/api/discord/index.ts'), + enabled: discordAPI, }, { - enabled: options.dashboardConfig.AuthConfig.providers.auth0, - logs: AuthProviderLogStrings.auth0Logs, - routes: [ - { - pattern: 'login/auth0', - entrypoint: resolve('./routes/login/auth0/index.ts'), - }, - { - pattern: 'login/auth0/callback', - entrypoint: resolve('./routes/login/auth0/callback.ts'), - }, - ], + pattern: 'discord/callback', + entrypoint: resolve('./routes/api/discord/callback.ts'), + enabled: discordAPI, }, { - enabled: options.dashboardConfig.AuthConfig.providers.usernameAndPassword, - logs: AuthProviderLogStrings.usernameAndPasswordLogs, - routes: [ - { - pattern: 'login/api/login', - entrypoint: resolve('./routes/login/api/login.ts'), - }, - ], + pattern: 'google', + entrypoint: resolve('./routes/api/google/index.ts'), + enabled: googleAPI, + }, + { + pattern: 'google/callback', + entrypoint: resolve('./routes/api/google/callback.ts'), + enabled: googleAPI, + }, + { + pattern: 'auth0', + entrypoint: resolve('./routes/api/auth0/index.ts'), + enabled: auth0API, + }, + { + pattern: 'auth0/callback', + entrypoint: resolve('./routes/api/auth0/callback.ts'), + enabled: auth0API, + }, + ], + }); + + injectAuthPageRoutes(params, { + options, + routes: [ + { + pattern: 'login/', + entrypoint: resolve('./routes/login.astro'), + enabled: dashboardEnabled && !options.dbStartPage, }, { - enabled: - options.dashboardConfig.AuthConfig.providers.usernameAndPassword && - options.dashboardConfig.AuthConfig.providers.usernameAndPasswordConfig - .allowUserRegistration, - logs: AuthProviderLogStrings.allowUserRegistration, - routes: [ - { - pattern: 'signup/', - entrypoint: resolve('./routes/login/signup.astro'), - }, - { - pattern: 'login/api/register', - entrypoint: resolve('./routes/login/api/register.ts'), - }, - ], + pattern: 'logout/', + entrypoint: resolve('./routes/logout.ts'), + enabled: dashboardEnabled && !options.dbStartPage, }, { - enabled: - options.dashboardConfig.dashboardEnabled && - !options.dbStartPage && - options.dashboardConfig.AuthConfig.enabled, - logs: { - enabledMessage: 'Auth Enabled, Injecting Login and Logout Pages', - disabledMessage: 'Auth Disabled', - }, - routes: [ - { - pattern: 'login/', - entrypoint: resolve('./routes/login/index.astro'), - }, - { - pattern: 'logout/', - entrypoint: resolve('./routes/logout.ts'), - }, - ], + pattern: 'signup/', + entrypoint: resolve('./routes/signup.astro'), + enabled: usernameAndPasswordAPI && allowUserRegistration, }, ], }); }, 'astro:config:done': async ({ injectTypes }) => { // Inject Types - injectTypes(authConfigDTS); - injectTypes(authHelperDTS); + injectTypes(authLibDTS); + injectTypes(authUtilsDTS); + injectTypes(authScriptsDTS); }, }, }; diff --git a/packages/studiocms_auth/src/layouts/AuthLayout.astro b/packages/studiocms_auth/src/layouts/AuthLayout.astro new file mode 100644 index 000000000..e378f984b --- /dev/null +++ b/packages/studiocms_auth/src/layouts/AuthLayout.astro @@ -0,0 +1,106 @@ +--- +import '@fontsource-variable/onest/index.css'; +import '@studiocms/ui/css/global.css'; +import './authlayout.css'; +import { Image } from 'astro:assets'; +import { db, eq } from 'astro:db'; +import { validImages } from 'studiocms:auth/utils/validImages'; +import Config from 'virtual:studiocms/config'; +import version from 'virtual:studiocms/version'; +import onestWoff2 from '@fontsource-variable/onest/files/onest-latin-wght-normal.woff2?url'; +import { CMSSiteConfigId } from '@studiocms/core/consts'; +import { tsSiteConfig } from '@studiocms/core/db/tsTables'; +import { Toaster } from '@studiocms/ui/components'; +import OAuthButtonStack from '../components/OAuthButtonStack.astro'; +import StudioCMSLogoSVG from '../components/StudioCMSLogoSVG.astro'; + +const { + dashboardConfig: { faviconURL }, +} = Config; + +interface Props { + title: string; + description: string; + lang?: string; +} + +const { title, description, lang = 'en' } = Astro.props; + +// Get the site config +const siteConfig = await db + .select() + .from(tsSiteConfig) + .where(eq(tsSiteConfig.id, CMSSiteConfigId)) + .get(); + +// Get the login page background and custom image from the site config +const loginPageBackground = siteConfig?.loginPageBackground; +const loginPageCustomImage = siteConfig?.loginPageCustomImage; + +const fallbackImageSrc = loginPageBackground === 'custom' + ? loginPageCustomImage + : validImages.find((x) => x.name !== 'custom' && x.name === loginPageBackground)?.dark; // TODO: Adapt to theme +--- + + + + + + + + + + {title} + + + + + +
+
+
+
+
+ +
+