Major changes: - Replace Payload CMS with Tina CMS (self-hosted) - Add Astro DB for consent logging (PDPA compliant) - Update Tailwind v3 to v4 (@tailwindcss/vite plugin) - Add astro-tina-starter template - Rewrite consent template for Astro (ConsentBanner.astro, Astro DB, Nano Stores) - Add install-tina-backend.sh for self-hosted Tina per customer - Rename convert-astro.sh to migrate-tina.sh - Add AGENTS.md template for generated websites - Delete all Payload/Next.js files Technical updates: - Astro DB using defineDb with eq operators for queries - Tailwind v4 with @theme block - Tina CMS local development mode - Proper Astro API routes for consent Research-verified with official documentation (April 2026)
327 lines
8.0 KiB
Bash
Executable File
327 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -e
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
BACKEND_PATH="${1:-./tina-backend}"
|
|
|
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
|
|
print_usage() {
|
|
cat << EOF
|
|
Usage: $(basename "$0") [target-path]
|
|
|
|
Install Tina CMS Backend (self-hosted)
|
|
|
|
Arguments:
|
|
target-path Path where Tina backend will be installed (default: ./tina-backend)
|
|
|
|
Examples:
|
|
$(basename "$0") /opt/tina-backend
|
|
|
|
This script installs a self-hosted Tina CMS backend with:
|
|
- Auth.js authentication
|
|
- SQLite database adapter
|
|
- Git provider for content
|
|
- Next.js API routes
|
|
|
|
Requirements:
|
|
- Node.js 18+
|
|
- npm or yarn
|
|
- git
|
|
|
|
EOF
|
|
}
|
|
|
|
main() {
|
|
echo "=============================================="
|
|
echo " Tina CMS Backend Installer (Self-Hosted)"
|
|
echo "=============================================="
|
|
echo ""
|
|
|
|
if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
|
|
print_usage
|
|
exit 0
|
|
fi
|
|
|
|
if [ -d "$BACKEND_PATH" ]; then
|
|
log_error "Directory already exists: $BACKEND_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
log_info "Creating Tina backend at: $BACKEND_PATH"
|
|
mkdir -p "$BACKEND_PATH"
|
|
cd "$BACKEND_PATH"
|
|
|
|
log_info "Creating package.json..."
|
|
cat > package.json << 'PKGEOF'
|
|
{
|
|
"name": "tina-backend",
|
|
"version": "1.0.0",
|
|
"private": true,
|
|
"scripts": {
|
|
"dev": "tinacms dev",
|
|
"build": "tinacms build",
|
|
"start": "tinacms start"
|
|
},
|
|
"dependencies": {
|
|
"@auth/core": "^0.34.0",
|
|
"@auth/drizzle-adapter": "^1.4.0",
|
|
"@libsql/client": "^0.14.0",
|
|
"@tinacms/auth": "^2.0.0",
|
|
"@tinacms/database": "^2.0.0",
|
|
"@tinacms/git-provider": "^2.0.0",
|
|
"@tinacms/graphql": "^2.0.0",
|
|
"@tinacms/mssql": "^2.0.0",
|
|
"@tinacms/server": "^2.0.0",
|
|
"drizzle-orm": "^0.38.0",
|
|
"next": "^14.0.0",
|
|
"react": "^18.0.0",
|
|
"react-dom": "^18.0.0",
|
|
"tinacms": "^2.0.0"
|
|
},
|
|
"devDependencies": {
|
|
"@types/node": "^20.0.0",
|
|
"typescript": "^5.0.0"
|
|
}
|
|
}
|
|
PKGEOF
|
|
|
|
log_info "Creating TypeScript config..."
|
|
cat > tsconfig.json << 'TSEOF'
|
|
{
|
|
"compilerOptions": {
|
|
"target": "ES2020",
|
|
"module": "ESNext",
|
|
"moduleResolution": "bundler",
|
|
"strict": true,
|
|
"esModuleInterop": true,
|
|
"skipLibCheck": true,
|
|
"outDir": "./dist",
|
|
"rootDir": "./src",
|
|
"jsx": "react-jsx"
|
|
},
|
|
"include": ["src/**/*", "tina/**/*"],
|
|
"exclude": ["node_modules"]
|
|
}
|
|
TSEOF
|
|
|
|
mkdir -p src/app/api/auth/\[...nextauth\]
|
|
mkdir -p src/app/api/tina/\[\[...tina\]\]
|
|
mkdir -p tina
|
|
|
|
log_info "Creating Auth.js configuration..."
|
|
cat > src/auth.config.ts << 'AUTHEOF'
|
|
import { AuthConfig } from "@auth/core/types";
|
|
|
|
const authConfig: AuthConfig = {
|
|
secret: process.env.NEXTAUTH_SECRET || "your-secret-change-in-production",
|
|
providers: [
|
|
{
|
|
id: "github",
|
|
name: "GitHub",
|
|
type: "oauth",
|
|
clientId: process.env.GITHUB_ID || "",
|
|
clientSecret: process.env.GITHUB_SECRET || "",
|
|
},
|
|
],
|
|
callbacks: {
|
|
async session({ session, token }) {
|
|
if (session.user && token.sub) {
|
|
session.user.email = token.email as string;
|
|
}
|
|
return session;
|
|
},
|
|
},
|
|
pages: {
|
|
signIn: "/auth/signin",
|
|
},
|
|
};
|
|
|
|
export default authConfig;
|
|
AUTHEOF
|
|
|
|
log_info "Creating NextAuth API route..."
|
|
cat > 'src/app/api/auth/[...nextauth]/route.ts' << 'NEXTAUTHEOF'
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
import { AuthHandler } from "@auth/core";
|
|
import authConfig from "../../../auth.config";
|
|
|
|
const authHandler = (req: NextRequest) =>
|
|
AuthHandler({
|
|
...authConfig,
|
|
req: req as any,
|
|
resolve(): Promise<any> {
|
|
throw new Error("Function not implemented.");
|
|
},
|
|
secret: authConfig.secret!,
|
|
trustHost: true,
|
|
});
|
|
|
|
export { authHandler as GET, authHandler as POST };
|
|
NEXTAUTHEOF
|
|
|
|
log_info "Creating Tina API route..."
|
|
cat > 'src/app/api/tina/[[...tina]]/route.ts' << 'TINAEOF'
|
|
import { TinaNodeBackend } from "@tinacms/server";
|
|
import authConfig from "../../../../auth.config";
|
|
import { branchName } from "./branch";
|
|
|
|
const tinaBackend = TinaNodeBackend({
|
|
authConfig: authConfig as any,
|
|
branch: branchName,
|
|
});
|
|
|
|
export { tinaBackend as GET, tinaBackend as POST };
|
|
TINAEOF
|
|
|
|
log_info "Creating Tina branch configuration..."
|
|
cat > src/app/api/tina/branch.ts << 'BRANCHEMAP'
|
|
export const branchName = process.env.TINA_BRANCH || "main";
|
|
BRANCHEMAP
|
|
|
|
log_info "Creating database schema..."
|
|
mkdir -p src/lib
|
|
cat > src/lib/schema.ts << 'DBEOF'
|
|
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
|
|
export const users = sqliteTable("users", {
|
|
id: text("id").primaryKey(),
|
|
name: text("name"),
|
|
email: text("email").unique(),
|
|
emailVerified: integer("email_verified", { mode: "boolean" }),
|
|
image: text("image"),
|
|
createdAt: integer("created_at", { mode: "timestamp" }),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" }),
|
|
});
|
|
|
|
export const accounts = sqliteTable("accounts", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id")
|
|
.references(() => users.id)
|
|
.notNull(),
|
|
type: text("type").notNull(),
|
|
provider: text("provider").notNull(),
|
|
providerAccountId: text("provider_account_id").notNull(),
|
|
refresh_token: text("refresh_token"),
|
|
access_token: text("access_token"),
|
|
expires_at: integer("expires_at"),
|
|
token_type: text("token_type"),
|
|
scope: text("scope"),
|
|
id_token: text("id_token"),
|
|
session_state: text("session_state"),
|
|
});
|
|
|
|
export const sessions = sqliteTable("sessions", {
|
|
id: text("id").primaryKey(),
|
|
sessionToken: text("session_token").unique(),
|
|
userId: text("user_id")
|
|
.references(() => users.id)
|
|
.notNull(),
|
|
expires: integer("expires", { mode: "timestamp" }).notNull(),
|
|
});
|
|
|
|
export const verificationTokens = sqliteTable("verification_tokens", {
|
|
identifier: text("identifier").notNull(),
|
|
token: text("token").notNull(),
|
|
expires: integer("expires", { mode: "timestamp" }).notNull(),
|
|
});
|
|
DBEOF
|
|
|
|
log_info "Creating database client..."
|
|
cat > src/lib/db.ts << 'DBCEOF'
|
|
import { createClient } from "@libsql/client";
|
|
import { drizzle } from "drizzle-orm/libsql";
|
|
import * as schema from "./schema";
|
|
|
|
const client = createClient({
|
|
url: process.env.DATABASE_URL || "file:local.db",
|
|
});
|
|
|
|
export const db = drizzle(client, { schema });
|
|
DBCEOF
|
|
|
|
log_info "Creating environment template..."
|
|
cat > .env.example << 'ENVEOF'
|
|
NEXTAUTH_SECRET=generate-a-random-secret-here
|
|
NEXTAUTH_URL=http://localhost:3000
|
|
|
|
GITHUB_ID=your-github-oauth-app-client-id
|
|
GITHUB_SECRET=your-github-oauth-app-client-secret
|
|
|
|
DATABASE_URL=file:local.db
|
|
TINA_BRANCH=main
|
|
ENVEOF
|
|
|
|
log_info "Creating README..."
|
|
cat > README.md << 'READMEEOF'
|
|
# Tina CMS Backend (Self-Hosted)
|
|
|
|
Self-hosted Tina CMS backend with Auth.js authentication and SQLite database.
|
|
|
|
## Setup
|
|
|
|
1. Install dependencies:
|
|
```bash
|
|
npm install
|
|
```
|
|
|
|
2. Configure environment:
|
|
```bash
|
|
cp .env.example .env
|
|
# Edit .env with your settings
|
|
```
|
|
|
|
3. Set up GitHub OAuth App:
|
|
- Go to https://github.com/settings/developers
|
|
- Create a new OAuth App
|
|
- Set callback URL to: `http://your-domain.com/api/auth/callback/github`
|
|
|
|
4. Start development:
|
|
```bash
|
|
npm run dev
|
|
```
|
|
|
|
## Environment Variables
|
|
|
|
| Variable | Description |
|
|
|----------|-------------|
|
|
| NEXTAUTH_SECRET | Random secret for NextAuth |
|
|
| NEXTAUTH_URL | Your site URL |
|
|
| GITHUB_ID | GitHub OAuth Client ID |
|
|
| GITHUB_SECRET | GitHub OAuth Client Secret |
|
|
| DATABASE_URL | SQLite database path |
|
|
| TINA_BRANCH | Git branch for content |
|
|
|
|
## Connecting Frontend
|
|
|
|
In your Astro frontend's `tina/config.ts`:
|
|
|
|
```ts
|
|
import { defineConfig } from "tinacms";
|
|
|
|
export default defineConfig({
|
|
apiUrl: "https://your-tina-backend.com",
|
|
contentApiUrl: "https://your-tina-backend.com",
|
|
});
|
|
```
|
|
READMEEOF
|
|
|
|
log_success "Tina backend created at: $BACKEND_PATH"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. cd $BACKEND_PATH"
|
|
echo " 2. npm install"
|
|
echo " 3. cp .env.example .env"
|
|
echo " 4. Configure GitHub OAuth App"
|
|
echo " 5. npm run dev"
|
|
echo ""
|
|
}
|
|
|
|
main "$@" |