Skip to content

Commit 772f22c

Browse files
authored
Merge pull request #203 from devforth/next
Next
2 parents 9478779 + f5b9b0d commit 772f22c

File tree

6 files changed

+181
-265
lines changed

6 files changed

+181
-265
lines changed

adminforth/documentation/blog/2024-10-01-ai-blog/index.md

Lines changed: 104 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,16 @@ nvm use 20
4040
## Step 1: Create a new AdminForth project
4141

4242
```bash
43-
mkdir ai-blog
44-
cd ai-blog
45-
npm init -y
46-
npm i adminforth @adminforth/upload @adminforth/rich-editor @adminforth/chat-gpt \
47-
express slugify http-proxy @types/express typescript tsx @types/node -D
48-
npx --yes tsc --init --module NodeNext --target ESNext
43+
npx adminforth create-app ai-blog
4944
```
5045

46+
Add modules:
47+
48+
```bash
49+
npm i @adminforth/upload @adminforth/rich-editor @adminforth/text-complete
50+
```
51+
52+
5153
## Step 2: Prepare environment
5254

5355
### OpenAI
@@ -95,24 +97,35 @@ Go to bucket settings, Permissions, Object ownership and select "ACLs Enabled" a
9597
6. Go to Security credentials and create a new access key. Save `Access key ID` and `Secret access key`.
9698
9799
98-
### Create .env file in project directory
100+
### Edit .env file in project directory
99101
100102
Create `.env` file with the following content:
101103
102104
```bash title=".env"
103-
DATABASE_URL=file:./db/db.sqlite
104-
ADMINFORTH_SECRET=<some random string>
105105
OPENAI_API_KEY=...
106106
AWS_ACCESS_KEY_ID=your_access_key_id
107107
AWS_SECRET_ACCESS_KEY=your_secret_access_key
108+
```
109+
110+
Edit `.env.local` file and add:
111+
112+
```bash title=".env.local"
113+
AWS_S3_BUCKET=my-ai-blog-bucket
114+
AWS_S3_REGION=us-east-1
115+
```
116+
117+
In same way edit `.env.prod` file and add:
118+
119+
```bash title=".env.prod"
108120
AWS_S3_BUCKET=my-ai-blog-bucket
109121
AWS_S3_REGION=us-east-1
110122
```
111123

112124

113-
## Step 3: Initialize database
114125

115-
Create `./schema.prisma` and put next content there:
126+
## Step 3: Add prisma models
127+
128+
Open `./schema.prisma` and put next content there:
116129

117130

118131
```yaml title="./schema.prisma"
@@ -122,78 +135,85 @@ generator client {
122135

123136
datasource db {
124137
provider = "sqlite"
125-
url = env("DATABASE_URL")
138+
url = env("PRISMA_DATABASE_URL")
126139
}
127140

128-
model User {
129-
id String @id
130-
createdAt DateTime
131-
email String @unique
141+
model adminuser {
142+
id String @id
143+
email String @unique
144+
password_hash String
145+
role String
146+
created_at DateTime
147+
//diff-add
132148
avatar String?
133-
publicName String?
134-
passwordHash String
149+
//diff-add
150+
public_name String?
151+
//diff-add
135152
posts Post[]
136153
}
137154

155+
//diff-add
138156
model Post {
157+
//diff-add
139158
id String @id
159+
//diff-add
140160
createdAt DateTime
161+
//diff-add
141162
title String
163+
//diff-add
142164
slug String
165+
//diff-add
143166
picture String?
167+
//diff-add
144168
content String
169+
//diff-add
145170
published Boolean
171+
//diff-add
146172
author User? @relation(fields: [authorId], references: [id])
173+
//diff-add
147174
authorId String?
175+
//diff-add
148176
contentImages ContentImage[]
177+
//diff-add
149178
}
150179

180+
//diff-add
151181
model ContentImage {
182+
//diff-add
152183
id String @id
184+
//diff-add
153185
createdAt DateTime
186+
//diff-add
154187
img String
188+
//diff-add
155189
postId String
190+
//diff-add
156191
resourceId String
192+
//diff-add
157193
post Post @relation(fields: [postId], references: [id])
194+
//diff-add
158195
}
159196
```
160197

161-
Create database using `prisma migrate`:
198+
Create a migration:
162199

163200
```bash
164-
npx -y prisma migrate dev --name init
201+
npm run makemigration -- --name add-posts
165202
```
166203

167-
> in future if you will need to update schema, you can run `npx prisma migrate dev --name <name>` where `<name>` is a name of migration.
168204

169205
## Step 4: Setting up AdminForth
170206

171207

172-
Open `package.json`, set `type` to `module` and add `start` script:
173208

174-
```json title="./package.json"
175-
{
176-
...
177-
//diff-add
178-
"type": "module",
179-
"scripts": {
180-
...
181-
//diff-add
182-
"start": "NODE_ENV=development tsx watch --env-file=.env index.ts",
183-
//diff-add
184-
"startLive": "NODE_ENV=production APP_PORT=80 tsx index.ts"
185-
},
186-
}
187-
```
188-
189-
Create `index.ts` file in root directory with following content:
209+
Open `index.ts` file in root directory and update it with the following content:
190210

191211
```ts title="./index.ts"
192212
import express from 'express';
193213
import AdminForth, { Filters, Sorts } from 'adminforth';
194-
import userResource from './res/user.js';
195-
import postResource from './res/posts.js';
196-
import contentImageResource from './res/content-image.js';
214+
import userResource from './resources/user.js';
215+
import postResource from './resources/posts.js';
216+
import contentImageResource from './resources/content-image.js';
197217
import httpProxy from 'http-proxy';
198218

199219
declare var process : {
@@ -264,11 +284,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
264284
app.use(express.json());
265285
const port = 3500;
266286

267-
// needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime
268-
if (process.env.NODE_ENV === 'development') {
269-
await admin.bundleNow({ hotReload: true });
270-
}
271-
console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');
287+
await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development' });
288+
console.log('Bundling AdminForth SPA done.');
272289

273290
// api to server recent posts
274291
app.get('/api/posts', async (req, res) => {
@@ -325,11 +342,11 @@ if (import.meta.url === `file://${process.argv[1]}`) {
325342
}
326343
```
327344

328-
## Step 5: Create resources
345+
## Step 5: Edit resources
329346

330-
Create `res` folder. Create `./res/adminuser.ts` file with following content:
347+
Open `./resources/adminuser.ts` file with following content:
331348

332-
```ts title="./res/adminuser.ts"
349+
```ts title="./resources/adminuser.ts"
333350
import AdminForth, { AdminForthDataTypes } from 'adminforth';
334351
import { randomUUID } from 'crypto';
335352
import UploadPlugin from '@adminforth/upload';
@@ -439,7 +456,7 @@ export default {
439456

440457
Create `posts.ts` file in res directory with following content:
441458

442-
```ts title="./res/post.ts"
459+
```ts title="./resources/post.ts"
443460
import { AdminUser, AdminForthDataTypes } from 'adminforth';
444461
import { randomUUID } from 'crypto';
445462
import UploadPlugin from '@adminforth/upload';
@@ -588,7 +605,7 @@ export default {
588605

589606
Also create `content-image.ts` file in `res` directory with following content:
590607

591-
```ts title="./res/content-image.ts"
608+
```ts title="./resources/content-image.ts"
592609

593610
import { AdminForthDataTypes } from 'adminforth';
594611
import { randomUUID } from 'crypto';
@@ -650,7 +667,7 @@ export default {
650667
Now you can start your admin panel:
651668

652669
```bash
653-
npm start
670+
npm run dev
654671
```
655672

656673
Open `http://localhost:3500/admin` in your browser and login with `[email protected]` and `adminforth` credentials.
@@ -660,8 +677,8 @@ Set up your avatar (you can generate it with AI) and public name in user setting
660677

661678
## Step 5: Create Nuxt project
662679

663-
664-
Now let's initialize our seo-facing frontend:
680+
Now let's initialize our seo-facing frontend.
681+
In the root directory of your admin app (`ai-blog`) and create a new folder `seo` and run:
665682

666683
```bash
667684
npx nuxi@latest init seo
@@ -921,28 +938,19 @@ Go to `http://localhost:3500/admin` to add new posts.
921938

922939
## Step 6: Deploy
923940

924-
We will dockerize app to make it easy to deploy with many ways. We will wrap both Node.js adminforth app and Nuxt.js app into single container for simplicity using supervisor. However you can split them into two containers and deploy them separately e.g. using docker compose.
941+
We will use Docker to make it easy to deploy with many ways. We will wrap both Node.js adminforth app and Nuxt.js app into single container for simplicity using supervisor. However you can split them into two containers and deploy them separately e.g. using docker compose.
925942

926943
Please note that in this demo example we routing requests to Nuxt.js app from AdminForth app using http-proxy.
927944
While this will work fine, it might give slower serving then if you would route traffik using dedicated reverse proxies like traefik or nginx.
928945

929946

930-
### Dockerize in single container
931-
932-
Create `bundleNow.ts` file in root project directory:
947+
### Dockerize AdminForth and Nuxt in single container
933948

934-
```ts title="./bundleNow.ts"
935-
import { admin } from './index.js';
936-
937-
await admin.bundleNow({ hotReload: false});
938-
console.log('Bundling AdminForth done.');
939-
```
940949

941-
942-
Create `Dockerfile` in root project directory:
950+
Open `Dockerfile` in root project directory (`ai-blog`) and put in the following content:
943951

944952
```dockerfile title="./Dockerfile"
945-
FROM node:20-alpine
953+
FROM node:20-slim
946954
EXPOSE 3500
947955
WORKDIR /app
948956
RUN apk add --no-cache supervisor
@@ -952,15 +960,15 @@ COPY seo/package.json seo/package-lock.json seo/
952960
RUN cd seo && npm ci
953961
COPY . .
954962

955-
RUN npx tsx bundleNow.ts
963+
RUN npx adminforth bundle
956964
RUN cd seo && npm run build
957965

958966
RUN cat > /etc/supervisord.conf <<EOF
959967
[supervisord]
960968
nodaemon=true
961969

962970
[program:app]
963-
command=npm run startLive
971+
command=npm run prod
964972
directory=/app
965973
autostart=true
966974
autorestart=true
@@ -976,7 +984,7 @@ stdout_logfile=/dev/stdout
976984
stderr_logfile=/dev/stderr
977985

978986
[program:prisma]
979-
command=npx --yes prisma migrate deploy
987+
command=npm run migrate:prod
980988
directory=/app
981989
autostart=true
982990
stdout_logfile=/dev/stdout
@@ -987,7 +995,7 @@ EOF
987995
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
988996
```
989997

990-
Create `.dockerignore` file in root project directory:
998+
Open `.dockerignore` file in root project directory (`ai-blog`) and put in the following content:
991999

9921000
```bash title=".dockerignore"
9931001
.env
@@ -1005,7 +1013,8 @@ terraform*
10051013
Build and run your docker container locally:
10061014

10071015
```bash
1008-
sudo docker run -p80:3500 -v ./prodDb:/app/db --env-file .env -it $(docker build -q .)
1016+
sudo docker build -t my-ai-blog .
1017+
sudo docker run -p80:3500 -v ./prodDb:/app/db --env-file .env -it --name my-ai-blog -d my-ai-blog
10091018
```
10101019

10111020
Now you can open `http://localhost` in your browser and see your blog.
@@ -1138,13 +1147,35 @@ resource "aws_instance" "docker_instance" {
11381147
systemctl start docker
11391148
systemctl enable docker
11401149
usermod -a -G docker ec2-user
1150+
1151+
echo "done" > /home/ec2-user/user-data-done
11411152
EOF
11421153
11431154
tags = {
11441155
Name = "my-ai-blog-instance"
11451156
}
11461157
}
11471158
1159+
resource "null_resource" "wait_for_user_data" {
1160+
provisioner "remote-exec" {
1161+
inline = [
1162+
"echo 'Waiting for EC2 software install to finish...'",
1163+
"while [ ! -f /home/ec2-user/user-data-done ]; do sleep 2; done",
1164+
"echo 'EC2 software install finished.'"
1165+
]
1166+
1167+
connection {
1168+
type = "ssh"
1169+
user = "ubuntu"
1170+
private_key = file("~/.ssh/id_rsa")
1171+
host = aws_instance.docker_instance.public_ip
1172+
}
1173+
}
1174+
1175+
depends_on = [aws_instance.app_instance]
1176+
}
1177+
1178+
11481179
resource "null_resource" "build_image" {
11491180
provisioner "local-exec" {
11501181
command = "docker build -t blogapp . && docker save blogapp:latest -o blogapp_image.tar"
@@ -1155,7 +1186,7 @@ resource "null_resource" "build_image" {
11551186
}
11561187
11571188
resource "null_resource" "remote_commands" {
1158-
depends_on = [aws_instance.docker_instance, null_resource.build_image]
1189+
depends_on = [null_resource.wait_for_user_data, null_resource.build_image]
11591190
11601191
triggers = {
11611192
always_run = timestamp()
@@ -1188,8 +1219,6 @@ resource "null_resource" "remote_commands" {
11881219
11891220
provisioner "remote-exec" {
11901221
inline = [
1191-
"while ! command -v docker &> /dev/null; do echo 'Waiting for Docker to be installed...'; sleep 1; done",
1192-
"while ! sudo docker info &> /dev/null; do echo 'Waiting for Docker to start...'; sleep 1; done",
11931222
"sudo docker system prune -af",
11941223
"docker load -i /home/ec2-user/blogapp_image.tar",
11951224
"sudo docker rm -f blogapp || true",

0 commit comments

Comments
 (0)