@codaijs/keel 0.1.0
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/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/cli.test.js +173 -0
- package/dist/__tests__/cli.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +86 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/sail-installer.test.d.ts +2 -0
- package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
- package/dist/__tests__/sail-installer.test.js +158 -0
- package/dist/__tests__/sail-installer.test.js.map +1 -0
- package/dist/create-runner.d.ts +11 -0
- package/dist/create-runner.d.ts.map +1 -0
- package/dist/create-runner.js +63 -0
- package/dist/create-runner.js.map +1 -0
- package/dist/create.d.ts +10 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +15 -0
- package/dist/create.js.map +1 -0
- package/dist/manage.d.ts +24 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +1461 -0
- package/dist/manage.js.map +1 -0
- package/dist/prompts.d.ts +36 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +208 -0
- package/dist/prompts.js.map +1 -0
- package/dist/sail-installer.d.ts +37 -0
- package/dist/sail-installer.d.ts.map +1 -0
- package/dist/sail-installer.js +935 -0
- package/dist/sail-installer.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +297 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +57 -0
- package/sails/_template/addon.json +20 -0
- package/sails/_template/install.ts +402 -0
- package/sails/admin-dashboard/README.md +117 -0
- package/sails/admin-dashboard/addon.json +28 -0
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
- package/sails/admin-dashboard/install.ts +305 -0
- package/sails/analytics/README.md +178 -0
- package/sails/analytics/addon.json +27 -0
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
- package/sails/analytics/install.ts +297 -0
- package/sails/file-uploads/README.md +191 -0
- package/sails/file-uploads/addon.json +30 -0
- package/sails/file-uploads/files/backend/routes/files.ts +198 -0
- package/sails/file-uploads/files/backend/schema/files.ts +36 -0
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
- package/sails/file-uploads/install.ts +466 -0
- package/sails/gdpr/README.md +174 -0
- package/sails/gdpr/addon.json +27 -0
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
- package/sails/gdpr/install.ts +756 -0
- package/sails/google-oauth/README.md +121 -0
- package/sails/google-oauth/addon.json +22 -0
- package/sails/google-oauth/files/GoogleButton.tsx +50 -0
- package/sails/google-oauth/install.ts +252 -0
- package/sails/i18n/README.md +193 -0
- package/sails/i18n/addon.json +30 -0
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
- package/sails/i18n/files/frontend/locales/de/common.json +44 -0
- package/sails/i18n/files/frontend/locales/en/common.json +44 -0
- package/sails/i18n/install.ts +407 -0
- package/sails/push-notifications/README.md +163 -0
- package/sails/push-notifications/addon.json +31 -0
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
- package/sails/push-notifications/install.ts +384 -0
- package/sails/r2-storage/README.md +101 -0
- package/sails/r2-storage/addon.json +29 -0
- package/sails/r2-storage/files/backend/services/storage.ts +71 -0
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
- package/sails/r2-storage/install.ts +412 -0
- package/sails/rate-limiting/README.md +145 -0
- package/sails/rate-limiting/addon.json +20 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
- package/sails/rate-limiting/install.ts +300 -0
- package/sails/registry.json +107 -0
- package/sails/stripe/README.md +214 -0
- package/sails/stripe/addon.json +24 -0
- package/sails/stripe/files/backend/routes/stripe.ts +154 -0
- package/sails/stripe/files/backend/schema/stripe.ts +74 -0
- package/sails/stripe/files/backend/services/stripe.ts +224 -0
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
- package/sails/stripe/install.ts +378 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Google OAuth Sail
|
|
2
|
+
|
|
3
|
+
Adds Google sign-in to your keel application using BetterAuth's social provider system.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- A Google Cloud project
|
|
8
|
+
- OAuth 2.0 credentials (Client ID and Client Secret)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx tsx sails/google-oauth/install.ts
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The installer will prompt for your Google OAuth credentials and configure everything automatically.
|
|
17
|
+
|
|
18
|
+
## Manual Setup: Google Cloud Console
|
|
19
|
+
|
|
20
|
+
### 1. Create or Select a Project
|
|
21
|
+
|
|
22
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
23
|
+
2. Click the project dropdown in the top bar
|
|
24
|
+
3. Click **New Project** or select an existing one
|
|
25
|
+
|
|
26
|
+
### 2. Enable the Google+ API (if not enabled)
|
|
27
|
+
|
|
28
|
+
1. Go to **APIs & Services** > **Library**
|
|
29
|
+
2. Search for "Google+ API" or "Google Identity"
|
|
30
|
+
3. Click **Enable**
|
|
31
|
+
|
|
32
|
+
### 3. Configure the OAuth Consent Screen
|
|
33
|
+
|
|
34
|
+
1. Go to **APIs & Services** > **OAuth consent screen**
|
|
35
|
+
2. Select **External** (or **Internal** for Google Workspace orgs)
|
|
36
|
+
3. Fill in the required fields:
|
|
37
|
+
- **App name**: Your application name
|
|
38
|
+
- **User support email**: Your email
|
|
39
|
+
- **Developer contact information**: Your email
|
|
40
|
+
4. Add scopes: `email`, `profile`, `openid`
|
|
41
|
+
5. Add test users if in "Testing" mode
|
|
42
|
+
|
|
43
|
+
### 4. Create OAuth 2.0 Credentials
|
|
44
|
+
|
|
45
|
+
1. Go to **APIs & Services** > **Credentials**
|
|
46
|
+
2. Click **Create Credentials** > **OAuth client ID**
|
|
47
|
+
3. Select **Web application**
|
|
48
|
+
4. Set the name (e.g., "My App - Web")
|
|
49
|
+
5. Add **Authorized JavaScript origins**:
|
|
50
|
+
- `http://localhost:5173` (Vite dev server)
|
|
51
|
+
- `https://yourdomain.com` (production)
|
|
52
|
+
6. Add **Authorized redirect URIs**:
|
|
53
|
+
- `http://localhost:3000/api/auth/callback/google` (development)
|
|
54
|
+
- `https://yourdomain.com/api/auth/callback/google` (production)
|
|
55
|
+
7. Click **Create**
|
|
56
|
+
8. Copy the **Client ID** and **Client Secret**
|
|
57
|
+
|
|
58
|
+
### 5. Configure Environment Variables
|
|
59
|
+
|
|
60
|
+
Add to your `.env` file:
|
|
61
|
+
|
|
62
|
+
```env
|
|
63
|
+
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
|
64
|
+
GOOGLE_CLIENT_SECRET=your-client-secret
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## How It Works
|
|
68
|
+
|
|
69
|
+
### Authentication Flow
|
|
70
|
+
|
|
71
|
+
1. User clicks "Continue with Google" button
|
|
72
|
+
2. BetterAuth redirects to Google's OAuth consent screen
|
|
73
|
+
3. User authorizes the application
|
|
74
|
+
4. Google redirects back to `/api/auth/callback/google`
|
|
75
|
+
5. BetterAuth creates or links the user account
|
|
76
|
+
6. User is redirected to the dashboard
|
|
77
|
+
|
|
78
|
+
### Files Modified
|
|
79
|
+
|
|
80
|
+
**Backend:**
|
|
81
|
+
- `src/auth/index.ts` -- Google added as a social provider in BetterAuth config
|
|
82
|
+
- `src/env.ts` -- Environment variable validation for Google credentials
|
|
83
|
+
|
|
84
|
+
**Frontend:**
|
|
85
|
+
- `src/components/auth/LoginForm.tsx` -- Google sign-in button added
|
|
86
|
+
- `src/components/auth/SignupForm.tsx` -- Google sign-in button added
|
|
87
|
+
|
|
88
|
+
### Files Added
|
|
89
|
+
|
|
90
|
+
**Frontend:**
|
|
91
|
+
- `src/components/auth/GoogleButton.tsx` -- Styled Google sign-in button component
|
|
92
|
+
|
|
93
|
+
## Capacitor / Native Apps
|
|
94
|
+
|
|
95
|
+
For native mobile apps using Capacitor, Google OAuth works through the system browser (in-app browser tab). The redirect URI remains the same since the native app loads the web app in a WebView.
|
|
96
|
+
|
|
97
|
+
If you need native Google Sign-In (using the Google SDK directly), you will need to:
|
|
98
|
+
|
|
99
|
+
1. Create separate OAuth credentials for iOS and Android
|
|
100
|
+
2. Use `@capacitor/google-auth` plugin
|
|
101
|
+
3. Pass the token to BetterAuth for session creation
|
|
102
|
+
|
|
103
|
+
## Troubleshooting
|
|
104
|
+
|
|
105
|
+
### "redirect_uri_mismatch" Error
|
|
106
|
+
|
|
107
|
+
Make sure your redirect URI in Google Cloud Console exactly matches:
|
|
108
|
+
- Development: `http://localhost:3000/api/auth/callback/google`
|
|
109
|
+
- Production: `https://yourdomain.com/api/auth/callback/google`
|
|
110
|
+
|
|
111
|
+
The port must match your backend server port, and the path is determined by BetterAuth.
|
|
112
|
+
|
|
113
|
+
### "Access blocked: app has not completed verification"
|
|
114
|
+
|
|
115
|
+
Your OAuth consent screen is in "Testing" mode. Either:
|
|
116
|
+
- Add test users in the consent screen settings, or
|
|
117
|
+
- Submit your app for verification (required for production)
|
|
118
|
+
|
|
119
|
+
### Users Not Being Linked
|
|
120
|
+
|
|
121
|
+
If a user signs up with email/password and later tries Google OAuth with the same email, BetterAuth will attempt to link the accounts. Make sure `account linking` is enabled in your BetterAuth configuration.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "google-oauth",
|
|
3
|
+
"displayName": "Google OAuth",
|
|
4
|
+
"description": "Adds Google OAuth sign-in via BetterAuth social provider",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"compatibility": ">=1.0.0",
|
|
7
|
+
"requiredEnvVars": [
|
|
8
|
+
{ "key": "GOOGLE_CLIENT_ID", "description": "Google OAuth Client ID from Google Cloud Console" },
|
|
9
|
+
{ "key": "GOOGLE_CLIENT_SECRET", "description": "Google OAuth Client Secret" }
|
|
10
|
+
],
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"backend": {},
|
|
13
|
+
"frontend": {}
|
|
14
|
+
},
|
|
15
|
+
"modifies": {
|
|
16
|
+
"backend": ["src/auth/index.ts", "src/env.ts"],
|
|
17
|
+
"frontend": ["src/components/auth/LoginForm.tsx", "src/components/auth/SignupForm.tsx"]
|
|
18
|
+
},
|
|
19
|
+
"adds": {
|
|
20
|
+
"frontend": ["src/components/auth/GoogleButton.tsx"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { authClient } from "@/lib/auth-client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Google sign-in button component.
|
|
5
|
+
*
|
|
6
|
+
* Uses BetterAuth's social sign-in to redirect the user to Google's OAuth
|
|
7
|
+
* consent screen. After authorization, Google redirects back to the app and
|
|
8
|
+
* BetterAuth completes the session creation automatically.
|
|
9
|
+
*/
|
|
10
|
+
export function GoogleButton() {
|
|
11
|
+
const handleGoogleSignIn = async () => {
|
|
12
|
+
await authClient.signIn.social({
|
|
13
|
+
provider: "google",
|
|
14
|
+
callbackURL: "/dashboard",
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
onClick={handleGoogleSignIn}
|
|
22
|
+
className="flex w-full items-center justify-center gap-3 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
|
23
|
+
>
|
|
24
|
+
{/* Google "G" logo */}
|
|
25
|
+
<svg
|
|
26
|
+
className="h-5 w-5"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
29
|
+
>
|
|
30
|
+
<path
|
|
31
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
|
|
32
|
+
fill="#4285F4"
|
|
33
|
+
/>
|
|
34
|
+
<path
|
|
35
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
36
|
+
fill="#34A853"
|
|
37
|
+
/>
|
|
38
|
+
<path
|
|
39
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
40
|
+
fill="#FBBC05"
|
|
41
|
+
/>
|
|
42
|
+
<path
|
|
43
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
44
|
+
fill="#EA4335"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
Continue with Google
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth Sail Installer
|
|
3
|
+
*
|
|
4
|
+
* Adds Google OAuth sign-in via BetterAuth social provider.
|
|
5
|
+
* Features a full interactive setup wizard that guides the user through
|
|
6
|
+
* Google Cloud project configuration, credential collection, and installation.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx tsx sails/google-oauth/install.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
readFileSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
copyFileSync,
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { resolve, dirname, join } from "node:path";
|
|
20
|
+
import { input, confirm } from "@inquirer/prompts";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Paths
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
|
|
27
|
+
const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
|
|
28
|
+
const BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
|
|
29
|
+
const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
interface SailManifest {
|
|
36
|
+
name: string;
|
|
37
|
+
displayName: string;
|
|
38
|
+
version: string;
|
|
39
|
+
requiredEnvVars: { key: string; description: string }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadManifest(): SailManifest {
|
|
43
|
+
return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function insertAtMarker(filePath: string, marker: string, code: string): void {
|
|
47
|
+
if (!existsSync(filePath)) {
|
|
48
|
+
console.warn(` Warning: File not found: ${filePath}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let content = readFileSync(filePath, "utf-8");
|
|
52
|
+
if (!content.includes(marker)) {
|
|
53
|
+
console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (content.includes(code.trim())) {
|
|
57
|
+
console.log(` Skipped (already present) -> ${filePath}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
content = content.replace(marker, `${marker}\n${code}`);
|
|
61
|
+
writeFileSync(filePath, content, "utf-8");
|
|
62
|
+
console.log(` Modified -> ${filePath}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function appendToEnvExample(entries: Record<string, string>): void {
|
|
66
|
+
const envPath = join(PROJECT_ROOT, ".env.example");
|
|
67
|
+
if (!existsSync(envPath)) {
|
|
68
|
+
console.warn(" Warning: .env.example not found");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
let content = readFileSync(envPath, "utf-8");
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
74
|
+
if (!content.includes(key)) {
|
|
75
|
+
lines.push(`${key}=${val}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (lines.length > 0) {
|
|
79
|
+
content += `\n# Google OAuth\n${lines.join("\n")}\n`;
|
|
80
|
+
writeFileSync(envPath, content, "utf-8");
|
|
81
|
+
console.log(" Updated .env.example");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Main
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
async function main(): Promise<void> {
|
|
90
|
+
const manifest = loadManifest();
|
|
91
|
+
|
|
92
|
+
// -- Step 1: Welcome message -------------------------------------------------
|
|
93
|
+
console.log("\n------------------------------------------------------------");
|
|
94
|
+
console.log(` Google OAuth Sail Installer (v${manifest.version})`);
|
|
95
|
+
console.log("------------------------------------------------------------");
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(" This sail integrates Google sign-in into your project");
|
|
98
|
+
console.log(" using BetterAuth's social provider system. After installation,");
|
|
99
|
+
console.log(" users will be able to sign in with their Google account via a");
|
|
100
|
+
console.log(' "Sign in with Google" button on your login and signup pages.');
|
|
101
|
+
console.log();
|
|
102
|
+
|
|
103
|
+
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
104
|
+
if (existsSync(pkgPath)) {
|
|
105
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
106
|
+
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
107
|
+
console.log();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// -- Step 2: Prerequisites check ---------------------------------------------
|
|
111
|
+
const hasProject = await confirm({
|
|
112
|
+
message: "Do you already have a Google Cloud project with OAuth configured?",
|
|
113
|
+
default: false,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (!hasProject) {
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(" Follow these steps to create a Google OAuth client:");
|
|
119
|
+
console.log();
|
|
120
|
+
console.log(" 1. Go to https://console.cloud.google.com/");
|
|
121
|
+
console.log(" 2. Create a new project (or select an existing one)");
|
|
122
|
+
console.log(" 3. Go to APIs & Services > Credentials");
|
|
123
|
+
console.log(' 4. Click "Create Credentials" > "OAuth client ID"');
|
|
124
|
+
console.log(' 5. Set application type to "Web application"');
|
|
125
|
+
console.log(" 6. Add authorized redirect URI:");
|
|
126
|
+
console.log(" {BACKEND_URL}/api/auth/callback/google");
|
|
127
|
+
console.log(" For local development, use:");
|
|
128
|
+
console.log(" http://localhost:3000/api/auth/callback/google");
|
|
129
|
+
console.log(" 7. Copy the Client ID and Client Secret");
|
|
130
|
+
console.log();
|
|
131
|
+
|
|
132
|
+
await confirm({
|
|
133
|
+
message: "I have completed the steps above and have my credentials ready",
|
|
134
|
+
default: false,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const googleClientId = await input({
|
|
139
|
+
message: "Google OAuth Client ID:",
|
|
140
|
+
validate: (value) => {
|
|
141
|
+
if (!value || value.trim().length === 0) return "Client ID is required.";
|
|
142
|
+
return true;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const googleClientSecret = await input({
|
|
147
|
+
message: "Google OAuth Client Secret:",
|
|
148
|
+
validate: (value) => {
|
|
149
|
+
if (!value || value.trim().length === 0) return "Client Secret is required.";
|
|
150
|
+
return true;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// -- Step 5: Show summary ----------------------------------------------------
|
|
155
|
+
console.log();
|
|
156
|
+
console.log(" Summary of changes:");
|
|
157
|
+
console.log(" -------------------");
|
|
158
|
+
console.log(" Files to copy:");
|
|
159
|
+
console.log(" + packages/frontend/src/components/auth/GoogleButton.tsx");
|
|
160
|
+
console.log();
|
|
161
|
+
console.log(" Files to modify:");
|
|
162
|
+
console.log(" ~ packages/backend/src/auth/index.ts (add Google provider)");
|
|
163
|
+
console.log(" ~ packages/backend/src/env.ts (add env validation)");
|
|
164
|
+
console.log(" ~ packages/frontend/src/components/auth/LoginForm.tsx");
|
|
165
|
+
console.log(" ~ packages/frontend/src/components/auth/SignupForm.tsx");
|
|
166
|
+
console.log(" ~ .env.example");
|
|
167
|
+
console.log(" ~ .env (if exists)");
|
|
168
|
+
console.log();
|
|
169
|
+
console.log(" Environment variables:");
|
|
170
|
+
console.log(` GOOGLE_CLIENT_ID=${googleClientId}`);
|
|
171
|
+
console.log(` GOOGLE_CLIENT_SECRET=${googleClientSecret.slice(0, 8)}...`);
|
|
172
|
+
console.log();
|
|
173
|
+
|
|
174
|
+
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
175
|
+
if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
|
|
176
|
+
|
|
177
|
+
// -- Step 7: Execute installation --------------------------------------------
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(" Installing...");
|
|
180
|
+
console.log();
|
|
181
|
+
|
|
182
|
+
console.log(" Copying files...");
|
|
183
|
+
const srcFile = join(SAIL_DIR, "files/GoogleButton.tsx");
|
|
184
|
+
const destDir = join(FRONTEND_ROOT, "src/components/auth");
|
|
185
|
+
mkdirSync(destDir, { recursive: true });
|
|
186
|
+
copyFileSync(srcFile, join(destDir, "GoogleButton.tsx"));
|
|
187
|
+
console.log(" Copied -> src/components/auth/GoogleButton.tsx");
|
|
188
|
+
|
|
189
|
+
console.log();
|
|
190
|
+
console.log(" Modifying backend files...");
|
|
191
|
+
|
|
192
|
+
insertAtMarker(
|
|
193
|
+
join(BACKEND_ROOT, "src/auth/index.ts"),
|
|
194
|
+
"// [SAIL_SOCIAL_PROVIDERS]",
|
|
195
|
+
` google: {\n clientId: process.env.GOOGLE_CLIENT_ID!,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n },`
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
insertAtMarker(
|
|
199
|
+
join(BACKEND_ROOT, "src/env.ts"),
|
|
200
|
+
"// [SAIL_ENV_VARS]",
|
|
201
|
+
` GOOGLE_CLIENT_ID: z.string().min(1, "GOOGLE_CLIENT_ID is required"),\n GOOGLE_CLIENT_SECRET: z.string().min(1, "GOOGLE_CLIENT_SECRET is required"),`
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(" Modifying frontend files...");
|
|
206
|
+
|
|
207
|
+
for (const form of ["LoginForm.tsx", "SignupForm.tsx"]) {
|
|
208
|
+
const formPath = join(FRONTEND_ROOT, "src/components/auth", form);
|
|
209
|
+
insertAtMarker(formPath, "// [SAIL_IMPORTS]", 'import { GoogleButton } from "./GoogleButton";');
|
|
210
|
+
insertAtMarker(formPath, "{/* [SAIL_SOCIAL_BUTTONS] */}", " <GoogleButton />");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log();
|
|
214
|
+
console.log(" Updating environment files...");
|
|
215
|
+
appendToEnvExample({ GOOGLE_CLIENT_ID: googleClientId, GOOGLE_CLIENT_SECRET: googleClientSecret });
|
|
216
|
+
|
|
217
|
+
const dotEnvPath = join(PROJECT_ROOT, ".env");
|
|
218
|
+
if (existsSync(dotEnvPath)) {
|
|
219
|
+
let dotEnv = readFileSync(dotEnvPath, "utf-8");
|
|
220
|
+
if (!dotEnv.includes("GOOGLE_CLIENT_ID")) {
|
|
221
|
+
dotEnv += `\n# Google OAuth\nGOOGLE_CLIENT_ID=${googleClientId}\nGOOGLE_CLIENT_SECRET=${googleClientSecret}\n`;
|
|
222
|
+
writeFileSync(dotEnvPath, dotEnv, "utf-8");
|
|
223
|
+
console.log(" Updated .env");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log();
|
|
228
|
+
console.log("------------------------------------------------------------");
|
|
229
|
+
console.log(" Google OAuth installed successfully!");
|
|
230
|
+
console.log("------------------------------------------------------------");
|
|
231
|
+
console.log();
|
|
232
|
+
console.log(" Next steps:");
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(" 1. Verify your Google Cloud Console settings:");
|
|
235
|
+
console.log(" https://console.cloud.google.com/apis/credentials");
|
|
236
|
+
console.log();
|
|
237
|
+
console.log(" 2. Ensure these redirect URIs are configured:");
|
|
238
|
+
console.log(" Development: http://localhost:3000/api/auth/callback/google");
|
|
239
|
+
console.log(" Production: https://yourdomain.com/api/auth/callback/google");
|
|
240
|
+
console.log();
|
|
241
|
+
console.log(" 3. Restart your dev server:");
|
|
242
|
+
console.log(" npm run dev");
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(" Testing:");
|
|
245
|
+
console.log(' - Visit your login page and click "Sign in with Google"');
|
|
246
|
+
console.log(" - You should be redirected to Google's consent screen");
|
|
247
|
+
console.log(" - After authorizing, you should be redirected back and signed in");
|
|
248
|
+
console.log(" - Check the users table in your database for the new account");
|
|
249
|
+
console.log();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Internationalization (i18n) Sail
|
|
2
|
+
|
|
3
|
+
Adds multi-language support to your keel application using i18next, react-i18next, and automatic browser language detection.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- i18next translation framework
|
|
8
|
+
- react-i18next for seamless React integration
|
|
9
|
+
- Automatic browser language detection (via `i18next-browser-languagedetector`)
|
|
10
|
+
- Language switcher dropdown component (dark Keel theme)
|
|
11
|
+
- Translation files for English, German, French, and Spanish
|
|
12
|
+
- Language preference persisted in localStorage
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx tsx sails/i18n/install.ts
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The installer will prompt you to select which languages to include and will configure everything automatically.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Using Translations in Components
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useTranslation } from "react-i18next";
|
|
28
|
+
|
|
29
|
+
function MyComponent() {
|
|
30
|
+
const { t } = useTranslation();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div>
|
|
34
|
+
<h1>{t("nav.home")}</h1>
|
|
35
|
+
<p>{t("common.loading")}</p>
|
|
36
|
+
<button>{t("common.save")}</button>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Using the Language Hook
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useLanguage } from "@/hooks/useLanguage";
|
|
46
|
+
|
|
47
|
+
function SettingsPage() {
|
|
48
|
+
const { currentLanguage, changeLanguage, availableLanguages } = useLanguage();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<select
|
|
52
|
+
value={currentLanguage}
|
|
53
|
+
onChange={(e) => changeLanguage(e.target.value)}
|
|
54
|
+
>
|
|
55
|
+
{availableLanguages.map((lang) => (
|
|
56
|
+
<option key={lang.code} value={lang.code}>
|
|
57
|
+
{lang.label}
|
|
58
|
+
</option>
|
|
59
|
+
))}
|
|
60
|
+
</select>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Translation Keys with Interpolation
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"greeting": "Hello, {{name}}!"
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
t("greeting", { name: user.name })
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Pluralization
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"items_one": "{{count}} item",
|
|
82
|
+
"items_other": "{{count}} items"
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
t("items", { count: 5 }) // "5 items"
|
|
88
|
+
t("items", { count: 1 }) // "1 item"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Adding a New Language
|
|
92
|
+
|
|
93
|
+
1. Create the locale file:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
mkdir -p packages/frontend/src/locales/ja
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
2. Create `packages/frontend/src/locales/ja/common.json` with all translation keys (copy from `en/common.json` as a template).
|
|
100
|
+
|
|
101
|
+
3. Update `packages/frontend/src/lib/i18n.ts`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import jaCommon from "@/locales/ja/common.json";
|
|
105
|
+
|
|
106
|
+
const resources = {
|
|
107
|
+
en: { common: enCommon },
|
|
108
|
+
de: { common: deCommon },
|
|
109
|
+
ja: { common: jaCommon }, // Add this
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
4. Update `packages/frontend/src/hooks/useLanguage.ts`:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const availableLanguages: Language[] = [
|
|
117
|
+
{ code: "en", label: "English" },
|
|
118
|
+
{ code: "de", label: "Deutsch" },
|
|
119
|
+
{ code: "ja", label: "Japanese" }, // Add this
|
|
120
|
+
];
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Adding New Translation Namespaces
|
|
124
|
+
|
|
125
|
+
For larger applications, split translations into namespaces (e.g., per page):
|
|
126
|
+
|
|
127
|
+
1. Create `packages/frontend/src/locales/en/dashboard.json`
|
|
128
|
+
2. Import in `i18n.ts` and add to resources:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import enDashboard from "@/locales/en/dashboard.json";
|
|
132
|
+
|
|
133
|
+
const resources = {
|
|
134
|
+
en: { common: enCommon, dashboard: enDashboard },
|
|
135
|
+
// ...
|
|
136
|
+
};
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
3. Update the `ns` array in `i18n.init()`:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
ns: ["common", "dashboard"],
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
4. Use in components:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const { t } = useTranslation("dashboard");
|
|
149
|
+
return <h1>{t("title")}</h1>;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Language Detection
|
|
153
|
+
|
|
154
|
+
The language detector checks in this order:
|
|
155
|
+
|
|
156
|
+
1. `localStorage` key `i18nextLng`
|
|
157
|
+
2. Browser navigator language
|
|
158
|
+
3. HTML `lang` attribute
|
|
159
|
+
|
|
160
|
+
The selected language is automatically persisted to `localStorage`.
|
|
161
|
+
|
|
162
|
+
## Architecture
|
|
163
|
+
|
|
164
|
+
| File | Purpose |
|
|
165
|
+
|------|---------|
|
|
166
|
+
| `src/lib/i18n.ts` | i18next initialization and configuration |
|
|
167
|
+
| `src/locales/<lang>/common.json` | Translation strings per language |
|
|
168
|
+
| `src/hooks/useLanguage.ts` | Hook for language switching |
|
|
169
|
+
| `src/components/LanguageSwitcher.tsx` | Dropdown UI component |
|
|
170
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
### Changing the Default Language
|
|
174
|
+
|
|
175
|
+
Edit `src/lib/i18n.ts`:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
fallbackLng: "de", // Change from "en" to desired default
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Disabling Language Detection
|
|
182
|
+
|
|
183
|
+
Remove the `LanguageDetector` plugin from `i18n.ts`:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
i18n
|
|
187
|
+
// .use(LanguageDetector) // Remove this line
|
|
188
|
+
.use(initReactI18next)
|
|
189
|
+
.init({
|
|
190
|
+
lng: "en", // Set a fixed language
|
|
191
|
+
// ...
|
|
192
|
+
});
|
|
193
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "i18n",
|
|
3
|
+
"displayName": "Internationalization (i18n)",
|
|
4
|
+
"description": "Multi-language support with i18next, react-i18next, and automatic language detection",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"compatibility": ">=1.0.0",
|
|
7
|
+
"requiredEnvVars": [],
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"backend": {},
|
|
10
|
+
"frontend": {
|
|
11
|
+
"i18next": "^24.0.0",
|
|
12
|
+
"react-i18next": "^15.0.0",
|
|
13
|
+
"i18next-browser-languagedetector": "^8.0.0"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"modifies": {
|
|
17
|
+
"backend": [],
|
|
18
|
+
"frontend": ["src/main.tsx", "src/components/layout/Header.tsx"]
|
|
19
|
+
},
|
|
20
|
+
"adds": {
|
|
21
|
+
"backend": [],
|
|
22
|
+
"frontend": [
|
|
23
|
+
"src/lib/i18n.ts",
|
|
24
|
+
"src/locales/en/common.json",
|
|
25
|
+
"src/locales/de/common.json",
|
|
26
|
+
"src/hooks/useLanguage.ts",
|
|
27
|
+
"src/components/LanguageSwitcher.tsx"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|