@studiocms/google 0.1.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present StudioCMS - withstudiocms (https://github.com/withstudiocms)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @studiocms/google Plugin
2
+
3
+ This plugin integrates Google OAuth authentication into StudioCMS. It defines the necessary configuration, including the required environment variables, OAuth provider details, and the endpoint paths for authentication.
4
+
5
+ ## Usage
6
+
7
+ Add this plugin in your StudioCMS config. (`studiocms.config.mjs`)
8
+
9
+ ```ts
10
+ import { defineStudioCMSConfig } from 'studiocms/config';
11
+ import google from '@studiocms/google';
12
+
13
+ export default defineStudioCMSConfig({
14
+ // other options here
15
+ plugins: [google()]
16
+ });
17
+ ```
18
+
19
+ ## Required ENV Variables
20
+
21
+ - `CMS_GOOGLE_CLIENT_ID`
22
+ - `CMS_GOOGLE_CLIENT_SECRET`
23
+ - `CMS_GOOGLE_REDIRECT_URI`
24
+
25
+ ## License
26
+
27
+ [MIT Licensed](./LICENSE).
@@ -0,0 +1,80 @@
1
+ import { HttpClient } from '@effect/platform';
2
+ import type { APIContext } from 'astro';
3
+ import { Effect, Schema } from 'studiocms/effect';
4
+ declare const GoogleUser_base: Schema.Class<GoogleUser, {
5
+ sub: typeof Schema.String;
6
+ picture: typeof Schema.String;
7
+ name: typeof Schema.String;
8
+ email: typeof Schema.String;
9
+ }, Schema.Struct.Encoded<{
10
+ sub: typeof Schema.String;
11
+ picture: typeof Schema.String;
12
+ name: typeof Schema.String;
13
+ email: typeof Schema.String;
14
+ }>, never, {
15
+ readonly sub: string;
16
+ } & {
17
+ readonly picture: string;
18
+ } & {
19
+ readonly name: string;
20
+ } & {
21
+ readonly email: string;
22
+ }, {}, {}>;
23
+ /**
24
+ * Represents a user authenticated via Google OAuth.
25
+ *
26
+ * @property sub - The unique identifier for the user (subject).
27
+ * @property picture - The URL of the user's profile picture.
28
+ * @property name - The full name of the user.
29
+ * @property email - The user's email address.
30
+ */
31
+ export declare class GoogleUser extends GoogleUser_base {
32
+ }
33
+ declare const GoogleOAuthAPI_base: Effect.Service.Class<GoogleOAuthAPI, "GoogleOAuthAPI", {
34
+ readonly dependencies: readonly [import("effect/Layer").Layer<import("studiocms/lib/auth/session").Session, never, never>, import("effect/Layer").Layer<import("studiocms/lib/auth/verify-email").VerifyEmail, import("studiocms/lib/effects/smtp").SMTPError | import("effect/Cause").UnknownException, never>, import("effect/Layer").Layer<import("studiocms/lib/auth/user").User, import("studiocms/lib/effects/smtp").SMTPError | import("effect/Cause").UnknownException, never>, import("effect/Layer").Layer<HttpClient.HttpClient, never, never>];
35
+ readonly effect: Effect.Effect<{
36
+ initSession: (context: APIContext) => Effect.Effect<Response, import("studiocms/lib/auth/session").SessionError, never>;
37
+ initCallback: (context: APIContext) => Effect.Effect<Response, import("studiocms/sdk/effect/db").LibSQLDatabaseError | import("studiocms/sdk/errors").SDKCoreError | Error, never>;
38
+ }, never, import("studiocms/lib/auth/session").Session | import("studiocms/lib/auth/verify-email").VerifyEmail | import("studiocms/lib/auth/user").User | HttpClient.HttpClient>;
39
+ }>;
40
+ /**
41
+ * Provides Google OAuth authentication effects for the StudioCMS API.
42
+ *
43
+ * @remarks
44
+ * This service handles the OAuth flow for Google authentication, including session initialization,
45
+ * authorization code validation, user account linking, and user creation. It integrates with
46
+ * session management, user data libraries, and email verification.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const googleOAuth = new GoogleOAuthAPI();
51
+ * yield* googleOAuth.initSession(context);
52
+ * yield* googleOAuth.initCallback(context);
53
+ * ```
54
+ *
55
+ * @method initSession
56
+ * Initializes the OAuth session by generating a state and code verifier, setting cookies,
57
+ * and redirecting the user to Google's authorization URL.
58
+ *
59
+ * @param context - The API context containing request and response information.
60
+ * @returns Redirects the user to the Google OAuth authorization URL.
61
+ *
62
+ * @method initCallback
63
+ * Handles the OAuth callback from Google. Validates the authorization code and state,
64
+ * fetches user information, links or creates user accounts, verifies email, and creates a user session.
65
+ *
66
+ * @param context - The API context containing request and response information.
67
+ * @returns Redirects the user to the appropriate page based on authentication and verification status.
68
+ *
69
+ * @dependencies
70
+ * - Session.Default: Session management utilities.
71
+ * - SDKCore.Default: Core SDK for user and OAuth provider operations.
72
+ * - VerifyEmail.Default: Email verification utilities.
73
+ * - User.Default: User data management utilities.
74
+ */
75
+ export declare class GoogleOAuthAPI extends GoogleOAuthAPI_base {
76
+ static ProviderID: string;
77
+ static ProviderCookieName: string;
78
+ static ProviderCodeVerifier: string;
79
+ }
80
+ export {};
@@ -0,0 +1,158 @@
1
+ import { getSecret } from "astro:env/server";
2
+ import { Session, User, VerifyEmail } from "studiocms:auth/lib";
3
+ import config from "studiocms:config";
4
+ import { StudioCMSRoutes } from "studiocms:lib";
5
+ import { SDKCore } from "studiocms:sdk";
6
+ import { FetchHttpClient, HttpClient, HttpClientResponse } from "@effect/platform";
7
+ import { Google, generateCodeVerifier, generateState } from "arctic";
8
+ import { Effect, genLogger, Schema } from "studiocms/effect";
9
+ import { getCookie, getUrlParam, ValidateAuthCodeError } from "studiocms/oAuthUtils";
10
+ class GoogleUser extends Schema.Class("GoogleUser")({
11
+ sub: Schema.String,
12
+ picture: Schema.String,
13
+ name: Schema.String,
14
+ email: Schema.String
15
+ }) {
16
+ }
17
+ const GOOGLE = {
18
+ CLIENT_ID: getSecret("GOOGLE_CLIENT_ID") ?? "",
19
+ CLIENT_SECRET: getSecret("GOOGLE_CLIENT_SECRET") ?? "",
20
+ REDIRECT_URI: getSecret("GOOGLE_REDIRECT_URI") ?? ""
21
+ };
22
+ class GoogleOAuthAPI extends Effect.Service()("GoogleOAuthAPI", {
23
+ dependencies: [Session.Default, VerifyEmail.Default, User.Default, FetchHttpClient.layer],
24
+ effect: genLogger("studiocms/routes/api/auth/google/effect")(function* () {
25
+ const [
26
+ sdk,
27
+ fetchClient,
28
+ { setOAuthSessionTokenCookie, createUserSession },
29
+ { isEmailVerified, sendVerificationEmail },
30
+ { getUserData, createOAuthUser }
31
+ ] = yield* Effect.all([SDKCore, HttpClient.HttpClient, Session, VerifyEmail, User]);
32
+ const { CLIENT_ID, CLIENT_SECRET, REDIRECT_URI } = GOOGLE;
33
+ const google = new Google(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);
34
+ const validateAuthCode = (code, codeVerifier) => genLogger("studiocms/routes/api/auth/google/effect.validateAuthCode")(function* () {
35
+ const tokens = yield* Effect.tryPromise(
36
+ () => google.validateAuthorizationCode(code, codeVerifier)
37
+ );
38
+ return yield* fetchClient.get("https://openidconnect.googleapis.com/v1/userinfo", {
39
+ headers: {
40
+ Authorization: `Bearer ${tokens.accessToken}`
41
+ }
42
+ }).pipe(
43
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(GoogleUser)),
44
+ Effect.catchAll(
45
+ (error) => Effect.fail(
46
+ new ValidateAuthCodeError({
47
+ provider: GoogleOAuthAPI.ProviderID,
48
+ message: `Failed to fetch user info: ${error.message}`
49
+ })
50
+ )
51
+ )
52
+ );
53
+ });
54
+ return {
55
+ initSession: (context) => genLogger("studiocms/routes/api/auth/google/effect.initSession")(function* () {
56
+ const state = generateState();
57
+ const codeVerifier = generateCodeVerifier();
58
+ const scopes = ["profile", "email"];
59
+ const url = google.createAuthorizationURL(state, codeVerifier, scopes);
60
+ yield* setOAuthSessionTokenCookie(context, GoogleOAuthAPI.ProviderCookieName, state);
61
+ yield* setOAuthSessionTokenCookie(
62
+ context,
63
+ GoogleOAuthAPI.ProviderCodeVerifier,
64
+ codeVerifier
65
+ );
66
+ return context.redirect(url.toString());
67
+ }),
68
+ initCallback: (context) => genLogger("studiocms/routes/api/auth/google/effect.initCallback")(function* () {
69
+ const { cookies, redirect } = context;
70
+ const [code, state, storedState, codeVerifier] = yield* Effect.all([
71
+ getUrlParam(context, "code"),
72
+ getUrlParam(context, "state"),
73
+ getCookie(context, GoogleOAuthAPI.ProviderCookieName),
74
+ getCookie(context, GoogleOAuthAPI.ProviderCodeVerifier)
75
+ ]);
76
+ if (!code || !storedState || !codeVerifier || state !== storedState) {
77
+ return redirect(StudioCMSRoutes.authLinks.loginURL);
78
+ }
79
+ const googleUser = yield* validateAuthCode(code, codeVerifier);
80
+ const { sub: googleUserId, name: googleUsername } = googleUser;
81
+ const existingOAuthAccount = yield* sdk.AUTH.oAuth.searchProvidersForId(
82
+ GoogleOAuthAPI.ProviderID,
83
+ googleUserId
84
+ );
85
+ if (existingOAuthAccount) {
86
+ const user = yield* sdk.GET.users.byId(existingOAuthAccount.userId);
87
+ if (!user) {
88
+ return new Response("User not found", { status: 404 });
89
+ }
90
+ const isEmailAccountVerified2 = yield* isEmailVerified(user);
91
+ if (!isEmailAccountVerified2) {
92
+ return new Response("Email not verified, please verify your account first.", {
93
+ status: 400
94
+ });
95
+ }
96
+ yield* createUserSession(user.id, context);
97
+ return redirect(StudioCMSRoutes.mainLinks.dashboardIndex);
98
+ }
99
+ const loggedInUser = yield* getUserData(context);
100
+ const linkNewOAuth = !!cookies.get(User.LinkNewOAuthCookieName)?.value;
101
+ if (loggedInUser.user && linkNewOAuth) {
102
+ const existingUser2 = yield* sdk.GET.users.byId(loggedInUser.user.id);
103
+ if (existingUser2) {
104
+ yield* sdk.AUTH.oAuth.create({
105
+ userId: existingUser2.id,
106
+ provider: GoogleOAuthAPI.ProviderID,
107
+ providerUserId: googleUserId
108
+ });
109
+ const isEmailAccountVerified2 = yield* isEmailVerified(existingUser2);
110
+ if (!isEmailAccountVerified2) {
111
+ return new Response("Email not verified, please verify your account first.", {
112
+ status: 400
113
+ });
114
+ }
115
+ yield* createUserSession(existingUser2.id, context);
116
+ return redirect(StudioCMSRoutes.mainLinks.dashboardIndex);
117
+ }
118
+ }
119
+ const newUser = yield* createOAuthUser(
120
+ {
121
+ // @ts-expect-error drizzle broke the id variable...
122
+ id: crypto.randomUUID(),
123
+ username: googleUsername,
124
+ email: googleUser.email,
125
+ name: googleUser.name,
126
+ avatar: googleUser.picture,
127
+ createdAt: /* @__PURE__ */ new Date()
128
+ },
129
+ { provider: GoogleOAuthAPI.ProviderID, providerUserId: googleUserId }
130
+ );
131
+ if ("error" in newUser) {
132
+ return new Response("Error creating user", { status: 500 });
133
+ }
134
+ if (config.dbStartPage) {
135
+ return redirect("/done");
136
+ }
137
+ yield* sendVerificationEmail(newUser.id, true);
138
+ const existingUser = yield* sdk.GET.users.byId(newUser.id);
139
+ const isEmailAccountVerified = yield* isEmailVerified(existingUser);
140
+ if (!isEmailAccountVerified) {
141
+ return new Response("Email not verified, please verify your account first.", {
142
+ status: 400
143
+ });
144
+ }
145
+ yield* createUserSession(newUser.id, context);
146
+ return redirect(StudioCMSRoutes.mainLinks.dashboardIndex);
147
+ })
148
+ };
149
+ })
150
+ }) {
151
+ static ProviderID = "google";
152
+ static ProviderCookieName = "google_oauth_state";
153
+ static ProviderCodeVerifier = "google_oauth_code_verifier";
154
+ }
155
+ export {
156
+ GoogleOAuthAPI,
157
+ GoogleUser
158
+ };
@@ -0,0 +1,24 @@
1
+ import type { APIRoute } from 'astro';
2
+ /**
3
+ * API route handler for initializing a Google session.
4
+ *
5
+ * This function uses the Effect system to compose asynchronous operations,
6
+ * retrieving the `initSession` method from the `GoogleOAuthAPI` and invoking it
7
+ * with the provided API context. The result is converted to a vanilla response
8
+ * using `convertToVanilla`.
9
+ *
10
+ * @param context - The API context containing request and environment information.
11
+ * @returns A promise resolving to the API response after session initialization.
12
+ */
13
+ export declare const initSession: APIRoute;
14
+ /**
15
+ * Handles the Google OAuth callback endpoint.
16
+ *
17
+ * This API route initializes the Google OAuth callback process by invoking the `initCallback`
18
+ * method from the `GoogleOAuthAPI`. It uses the Effect system to manage dependencies and
19
+ * asynchronous control flow, providing the default implementation of `GoogleOAuthAPI`.
20
+ *
21
+ * @param context - The API context containing request and response objects.
22
+ * @returns A promise resolving to the result of the Google OAuth callback process.
23
+ */
24
+ export declare const initCallback: APIRoute;
@@ -0,0 +1,18 @@
1
+ import { convertToVanilla, Effect } from "studiocms/effect";
2
+ import { GoogleOAuthAPI } from "./effect/google.js";
3
+ const initSession = async (context) => await convertToVanilla(
4
+ Effect.gen(function* () {
5
+ const { initSession: initSession2 } = yield* GoogleOAuthAPI;
6
+ return yield* initSession2(context);
7
+ }).pipe(Effect.provide(GoogleOAuthAPI.Default))
8
+ );
9
+ const initCallback = async (context) => await convertToVanilla(
10
+ Effect.gen(function* () {
11
+ const { initCallback: initCallback2 } = yield* GoogleOAuthAPI;
12
+ return yield* initCallback2(context);
13
+ }).pipe(Effect.provide(GoogleOAuthAPI.Default))
14
+ );
15
+ export {
16
+ initCallback,
17
+ initSession
18
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * These triple-slash directives defines dependencies to various declaration files that will be
3
+ * loaded when a user imports the StudioCMS plugin in their Astro configuration file. These
4
+ * directives must be first at the top of the file and can only be preceded by this comment.
5
+ */
6
+ import { type StudioCMSPlugin } from 'studiocms/plugins';
7
+ /**
8
+ * Creates and returns the StudioCMS Google Plugin.
9
+ *
10
+ * This plugin integrates Google OAuth authentication into StudioCMS.
11
+ * It defines the necessary configuration, including the required environment variables,
12
+ * OAuth provider details, and the endpoint path for authentication.
13
+ *
14
+ * @returns {StudioCMSPlugin} The configured StudioCMS Google Plugin instance.
15
+ *
16
+ * @remarks
17
+ * - Requires the following environment variables to be set:
18
+ * - `CMS_GOOGLE_CLIENT_ID`
19
+ * - `CMS_GOOGLE_CLIENT_SECRET`
20
+ * - `CMS_GOOGLE_REDIRECT_URI`
21
+ * - Minimum supported StudioCMS version: `0.1.0-beta.22`
22
+ * - Registers the Google OAuth provider with a custom SVG logo.
23
+ */
24
+ export declare function studiocmsGoogle(): StudioCMSPlugin;
25
+ export default studiocmsGoogle;
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ import { createResolver } from "astro-integration-kit";
2
+ import { definePlugin } from "studiocms/plugins";
3
+ function studiocmsGoogle() {
4
+ const { resolve } = createResolver(import.meta.url);
5
+ const packageIdentifier = "@studiocms/google";
6
+ return definePlugin({
7
+ identifier: packageIdentifier,
8
+ name: "StudioCMS Google Plugin",
9
+ studiocmsMinimumVersion: "0.1.0-beta.22",
10
+ hooks: {
11
+ "studiocms:config:setup": ({ setAuthService }) => {
12
+ setAuthService({
13
+ oAuthProvider: {
14
+ name: "google",
15
+ formattedName: "Google",
16
+ endpointPath: resolve("./endpoint.js"),
17
+ requiredEnvVariables: [
18
+ "CMS_GOOGLE_CLIENT_ID",
19
+ "CMS_GOOGLE_CLIENT_SECRET",
20
+ "CMS_GOOGLE_REDIRECT_URI"
21
+ ],
22
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 256 262" class="oauth-logo"><path fill="#4285f4" d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"/><path fill="#34a853" d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"/><path fill="#fbbc05" d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"/><path fill="#eb4335" d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"/></svg>'
23
+ }
24
+ });
25
+ }
26
+ }
27
+ });
28
+ }
29
+ var index_default = studiocmsGoogle;
30
+ export {
31
+ index_default as default,
32
+ studiocmsGoogle
33
+ };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@studiocms/google",
3
+ "version": "0.1.0-beta.23",
4
+ "description": "Add Google OAuth Support to your StudioCMS project with ease!",
5
+ "author": {
6
+ "name": "withstudiocms",
7
+ "url": "https://studiocms.dev"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/withstudiocms/studiocms.git",
12
+ "directory": "packages/@studiocms/google"
13
+ },
14
+ "contributors": [
15
+ "Adammatthiesen",
16
+ "jdtjenkins",
17
+ "dreyfus92",
18
+ "code.spirit"
19
+ ],
20
+ "license": "MIT",
21
+ "keywords": [
22
+ "astro",
23
+ "astrocms",
24
+ "astrodb",
25
+ "astrostudio",
26
+ "astro-integration",
27
+ "astro-studio",
28
+ "astro-studiocms",
29
+ "cms",
30
+ "studiocms",
31
+ "withastro",
32
+ "plugin",
33
+ "studiocms-plugin"
34
+ ],
35
+ "homepage": "https://studiocms.dev",
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
40
+ "sideEffects": false,
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "exports": {
45
+ ".": {
46
+ "types": "./dist/index.d.ts",
47
+ "default": "./dist/index.js"
48
+ }
49
+ },
50
+ "type": "module",
51
+ "dependencies": {
52
+ "astro-integration-kit": "^0.18",
53
+ "arctic": "^3.7.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^22.0.0"
57
+ },
58
+ "peerDependencies": {
59
+ "@effect/platform": "^0.90.0",
60
+ "astro": "^5.12.6",
61
+ "effect": "^3.17.3",
62
+ "vite": "^6.3.4",
63
+ "studiocms": "0.1.0-beta.23"
64
+ },
65
+ "scripts": {
66
+ "build": "buildkit build 'src/**/*.{ts,astro,css,json,png}'",
67
+ "dev": "buildkit dev 'src/**/*.{ts,astro,css,json,png}'",
68
+ "typecheck": "tspc -p tsconfig.tspc.json"
69
+ }
70
+ }