@@ -40,14 +40,16 @@ nvm use 20
40
40
## Step 1: Create a new AdminForth project
41
41
42
42
``` 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
49
44
```
50
45
46
+ Add modules:
47
+
48
+ ``` bash
49
+ npm i @adminforth/upload @adminforth/rich-editor @adminforth/text-complete
50
+ ```
51
+
52
+
51
53
## Step 2: Prepare environment
52
54
53
55
### OpenAI
@@ -95,24 +97,35 @@ Go to bucket settings, Permissions, Object ownership and select "ACLs Enabled" a
95
97
6 . Go to Security credentials and create a new access key. Save `Access key ID` and `Secret access key`.
96
98
97
99
98
- ### Create .env file in project directory
100
+ ### Edit .env file in project directory
99
101
100
102
Create `.env` file with the following content:
101
103
102
104
```bash title=".env"
103
- DATABASE_URL=file:./db/db.sqlite
104
- ADMINFORTH_SECRET=<some random string>
105
105
OPENAI_API_KEY=...
106
106
AWS_ACCESS_KEY_ID=your_access_key_id
107
107
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"
108
120
AWS_S3_BUCKET=my-ai-blog-bucket
109
121
AWS_S3_REGION=us-east-1
110
122
```
111
123
112
124
113
- ## Step 3: Initialize database
114
125
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:
116
129
117
130
118
131
``` yaml title="./schema.prisma"
@@ -122,78 +135,85 @@ generator client {
122
135
123
136
datasource db {
124
137
provider = "sqlite"
125
- url = env("DATABASE_URL ")
138
+ url = env("PRISMA_DATABASE_URL ")
126
139
}
127
140
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
132
148
avatar String?
133
- publicName String?
134
- passwordHash String
149
+ //diff-add
150
+ public_name String?
151
+ //diff-add
135
152
posts Post[]
136
153
}
137
154
155
+ //diff-add
138
156
model Post {
157
+ //diff-add
139
158
id String @id
159
+ //diff-add
140
160
createdAt DateTime
161
+ //diff-add
141
162
title String
163
+ //diff-add
142
164
slug String
165
+ //diff-add
143
166
picture String?
167
+ //diff-add
144
168
content String
169
+ //diff-add
145
170
published Boolean
171
+ //diff-add
146
172
author User? @relation(fields : [authorId], references: [id])
173
+ //diff-add
147
174
authorId String?
175
+ //diff-add
148
176
contentImages ContentImage[]
177
+ //diff-add
149
178
}
150
179
180
+ //diff-add
151
181
model ContentImage {
182
+ //diff-add
152
183
id String @id
184
+ //diff-add
153
185
createdAt DateTime
186
+ //diff-add
154
187
img String
188
+ //diff-add
155
189
postId String
190
+ //diff-add
156
191
resourceId String
192
+ //diff-add
157
193
post Post @relation(fields : [postId], references: [id])
194
+ //diff-add
158
195
}
159
196
```
160
197
161
- Create database using ` prisma migrate ` :
198
+ Create a migration :
162
199
163
200
``` bash
164
- npx -y prisma migrate dev --name init
201
+ npm run makemigration -- --name add-posts
165
202
```
166
203
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.
168
204
169
205
## Step 4: Setting up AdminForth
170
206
171
207
172
- Open ` package.json ` , set ` type ` to ` module ` and add ` start ` script:
173
208
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:
190
210
191
211
``` ts title="./index.ts"
192
212
import express from ' express' ;
193
213
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' ;
197
217
import httpProxy from ' http-proxy' ;
198
218
199
219
declare var process : {
@@ -264,11 +284,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
264
284
app .use (express .json ());
265
285
const port = 3500 ;
266
286
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.' );
272
289
273
290
// api to server recent posts
274
291
app .get (' /api/posts' , async (req , res ) => {
@@ -325,11 +342,11 @@ if (import.meta.url === `file://${process.argv[1]}`) {
325
342
}
326
343
```
327
344
328
- ## Step 5: Create resources
345
+ ## Step 5: Edit resources
329
346
330
- Create ` res ` folder. Create ` ./res /adminuser.ts` file with following content:
347
+ Open ` ./resources /adminuser.ts` file with following content:
331
348
332
- ``` ts title="./res /adminuser.ts"
349
+ ``` ts title="./resources /adminuser.ts"
333
350
import AdminForth , { AdminForthDataTypes } from ' adminforth' ;
334
351
import { randomUUID } from ' crypto' ;
335
352
import UploadPlugin from ' @adminforth/upload' ;
@@ -439,7 +456,7 @@ export default {
439
456
440
457
Create ` posts.ts ` file in res directory with following content:
441
458
442
- ``` ts title="./res /post.ts"
459
+ ``` ts title="./resources /post.ts"
443
460
import { AdminUser , AdminForthDataTypes } from ' adminforth' ;
444
461
import { randomUUID } from ' crypto' ;
445
462
import UploadPlugin from ' @adminforth/upload' ;
@@ -588,7 +605,7 @@ export default {
588
605
589
606
Also create ` content-image.ts ` file in ` res ` directory with following content:
590
607
591
- ``` ts title="./res /content-image.ts"
608
+ ``` ts title="./resources /content-image.ts"
592
609
593
610
import { AdminForthDataTypes } from ' adminforth' ;
594
611
import { randomUUID } from ' crypto' ;
@@ -650,7 +667,7 @@ export default {
650
667
Now you can start your admin panel:
651
668
652
669
``` bash
653
- npm start
670
+ npm run dev
654
671
```
655
672
656
673
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
660
677
661
678
## Step 5: Create Nuxt project
662
679
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 :
665
682
666
683
``` bash
667
684
npx nuxi@latest init seo
@@ -921,28 +938,19 @@ Go to `http://localhost:3500/admin` to add new posts.
921
938
922
939
## Step 6: Deploy
923
940
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.
925
942
926
943
Please note that in this demo example we routing requests to Nuxt.js app from AdminForth app using http-proxy.
927
944
While this will work fine, it might give slower serving then if you would route traffik using dedicated reverse proxies like traefik or nginx.
928
945
929
946
930
- ### Dockerize in single container
931
-
932
- Create ` bundleNow.ts ` file in root project directory:
947
+ ### Dockerize AdminForth and Nuxt in single container
933
948
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
- ```
940
949
941
-
942
- Create ` Dockerfile ` in root project directory:
950
+ Open ` Dockerfile ` in root project directory (` ai-blog ` ) and put in the following content:
943
951
944
952
``` dockerfile title="./Dockerfile"
945
- FROM node:20-alpine
953
+ FROM node:20-slim
946
954
EXPOSE 3500
947
955
WORKDIR /app
948
956
RUN apk add --no-cache supervisor
@@ -952,15 +960,15 @@ COPY seo/package.json seo/package-lock.json seo/
952
960
RUN cd seo && npm ci
953
961
COPY . .
954
962
955
- RUN npx tsx bundleNow.ts
963
+ RUN npx adminforth bundle
956
964
RUN cd seo && npm run build
957
965
958
966
RUN cat > /etc/supervisord.conf <<EOF
959
967
[supervisord]
960
968
nodaemon=true
961
969
962
970
[program:app]
963
- command=npm run startLive
971
+ command=npm run prod
964
972
directory=/app
965
973
autostart=true
966
974
autorestart=true
@@ -976,7 +984,7 @@ stdout_logfile=/dev/stdout
976
984
stderr_logfile=/dev/stderr
977
985
978
986
[program:prisma]
979
- command=npx --yes prisma migrate deploy
987
+ command=npm run migrate:prod
980
988
directory=/app
981
989
autostart=true
982
990
stdout_logfile=/dev/stdout
987
995
CMD ["supervisord" , "-c" , "/etc/supervisord.conf" ]
988
996
```
989
997
990
- Create ` .dockerignore ` file in root project directory:
998
+ Open ` .dockerignore ` file in root project directory ( ` ai-blog ` ) and put in the following content :
991
999
992
1000
``` bash title=".dockerignore"
993
1001
.env
@@ -1005,7 +1013,8 @@ terraform*
1005
1013
Build and run your docker container locally:
1006
1014
1007
1015
``` 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
1009
1018
```
1010
1019
1011
1020
Now you can open ` http://localhost ` in your browser and see your blog.
@@ -1138,13 +1147,35 @@ resource "aws_instance" "docker_instance" {
1138
1147
systemctl start docker
1139
1148
systemctl enable docker
1140
1149
usermod -a -G docker ec2-user
1150
+
1151
+ echo "done" > /home/ec2-user/user-data-done
1141
1152
EOF
1142
1153
1143
1154
tags = {
1144
1155
Name = "my-ai-blog-instance"
1145
1156
}
1146
1157
}
1147
1158
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
+
1148
1179
resource "null_resource" "build_image" {
1149
1180
provisioner "local-exec" {
1150
1181
command = "docker build -t blogapp . && docker save blogapp:latest -o blogapp_image.tar"
@@ -1155,7 +1186,7 @@ resource "null_resource" "build_image" {
1155
1186
}
1156
1187
1157
1188
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]
1159
1190
1160
1191
triggers = {
1161
1192
always_run = timestamp()
@@ -1188,8 +1219,6 @@ resource "null_resource" "remote_commands" {
1188
1219
1189
1220
provisioner "remote-exec" {
1190
1221
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",
1193
1222
"sudo docker system prune -af",
1194
1223
"docker load -i /home/ec2-user/blogapp_image.tar",
1195
1224
"sudo docker rm -f blogapp || true",
0 commit comments