#!/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 { 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 "$@"