Compare commits
44 Commits
7d973739da
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 66c0506e7e | |||
| 1a7deed5b0 | |||
| 463943abab | |||
| d2b87f6f57 | |||
| f729d24c65 | |||
| 4b1174cb30 | |||
| d6a3bd55a7 | |||
| b843f49399 | |||
| 31dc117fb9 | |||
| c109a2f558 | |||
| 9ccc609522 | |||
| c17bf09df3 | |||
| b01af42d91 | |||
| a5069f922f | |||
| 279ab4bb9d | |||
| 2a33bb81c1 | |||
| 5bb81ba2f2 | |||
| 6164c1ca27 | |||
| ced95a9593 | |||
| 0e3740bb6a | |||
| 3028d5ffb2 | |||
| fbdb963b01 | |||
| e577748914 | |||
| ee0ba92ff6 | |||
| defbe8ce16 | |||
| c7e3417093 | |||
| fcc7f3017c | |||
| 2343af9d2e | |||
| 440ad779fd | |||
| ec724ddebe | |||
| bbcc008317 | |||
| 32ea4de8de | |||
| 517d6e5472 | |||
| 7ad5e5082b | |||
| bee21cffba | |||
| b634decd1f | |||
| ce29a85d61 | |||
| c1c5aabd36 | |||
| 96f28dbba4 | |||
| 742132fbb0 | |||
| b8bb83624d | |||
| c762b81b01 | |||
| 2da0c212ee | |||
| 61196dc325 |
@@ -0,0 +1,61 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
run-name: Build And Deploy (by ${{ gitea.actor }}) 🚀
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and Upload
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install yarn
|
||||||
|
shell: bash
|
||||||
|
run: npm i yarn -g
|
||||||
|
- name: Yarn install (dev)
|
||||||
|
shell: bash
|
||||||
|
run: yarn install
|
||||||
|
- name: Vite build
|
||||||
|
shell: bash
|
||||||
|
run: ./node_modules/.bin/vite build
|
||||||
|
- name: Publish artifacts
|
||||||
|
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifact
|
||||||
|
path: |
|
||||||
|
dist
|
||||||
|
index.html
|
||||||
|
package.json
|
||||||
|
public
|
||||||
|
src
|
||||||
|
tsconfig.json
|
||||||
|
vite.config.ts
|
||||||
|
yarn.lock
|
||||||
|
Upload:
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: https://github.com/christopherHX/gitea-download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifact
|
||||||
|
path: server
|
||||||
|
- name: Install rsync
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get -y install rsync
|
||||||
|
- name: Upload and install dependencies in server
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
chmod 600 ~/.ssh
|
||||||
|
echo "$SSH_KEYSCAN" > ~/.ssh/known_hosts
|
||||||
|
chmod 600 ~/.ssh/known_hosts
|
||||||
|
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
# scp -r -P 44 artifact owofannouncements@foxthesystem.space:~/server
|
||||||
|
rsync -avzxh --delete -e 'ssh -p 44' server owofannouncements@foxthesystem.space:~
|
||||||
|
ssh -p 44 owofannouncements@foxthesystem.space "/bin/bash -c 'cd server ; yarn install --production=true'"
|
||||||
|
env:
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
SSH_KEYSCAN: ${{ secrets.SSH_KEYSCAN }}
|
||||||
@@ -25,7 +25,7 @@ Compile:
|
|||||||
Run:
|
Run:
|
||||||
|
|
||||||
```
|
```
|
||||||
NODE_ENV=production HTTP_PORT=1234 node ./src/server/main.ts
|
NODE_ENV=production HTTP_PORT=19285 DATABASE_PATH=/opt/owofannouncements/data.db UPLOADS_DIR=/opt/owofannouncements/uploads node /opt/owofannouncements/server/src/server/main.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy to server:
|
Copy to server:
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"dev": "nodemon -w src/server -x tsx src/server/main.ts",
|
"dev": "nodemon -w src/server -x tsx src/server/main.ts",
|
||||||
"start": "NODE_ENV=production tsx src/server/main.ts",
|
"start": "NODE_ENV=production tsx src/server/main.ts",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"biome-full": "biome check --write"
|
"biome-full": "biome check --write --unsafe"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { unlink } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import {
|
||||||
|
type NextFunction,
|
||||||
|
type Request,
|
||||||
|
type Response,
|
||||||
|
Router,
|
||||||
|
} from "express";
|
||||||
|
import multer from "multer";
|
||||||
|
import z from "zod";
|
||||||
|
import { baseUploadsFolder } from "./base-uploads-folder.ts";
|
||||||
|
import { db } from "./db/database.ts";
|
||||||
|
|
||||||
|
const PostInput = z.object({
|
||||||
|
file: z.object({ filename: z.string() }),
|
||||||
|
body: z.object({
|
||||||
|
publication_date: z.coerce
|
||||||
|
.date()
|
||||||
|
.transform((x) => new Date(x).toISOString()),
|
||||||
|
title: z.string().nonempty(),
|
||||||
|
text: z.string().nonempty(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const PatchInput = z.object({
|
||||||
|
file: z.object({ filename: z.string().optional() }).optional(),
|
||||||
|
body: z.object({
|
||||||
|
publication_date: z.coerce
|
||||||
|
.date()
|
||||||
|
.optional()
|
||||||
|
.transform((x) => (x ? new Date(x).toISOString() : x)),
|
||||||
|
title: z.string().nonempty().optional(),
|
||||||
|
text: z.string().nonempty().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const AnnouncementIdInput = z.object({
|
||||||
|
announcementId: z.coerce.number().positive(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const GetFromGame = z.object({
|
||||||
|
gameName: z.string().nonempty(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
dest: baseUploadsFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiRouter = Router();
|
||||||
|
|
||||||
|
apiRouter.get("/announcements", async (_, res) => {
|
||||||
|
console.log(`Processing GET /api/announcements`);
|
||||||
|
const announcements = await db
|
||||||
|
.selectFrom("announcements")
|
||||||
|
.select(["id", "title", "text", "publication_date", "image"])
|
||||||
|
.orderBy("announcements.publication_date", "desc")
|
||||||
|
.execute();
|
||||||
|
res.json(announcements);
|
||||||
|
});
|
||||||
|
|
||||||
|
apiRouter.get("/announcements/from-game", async (req, res) => {
|
||||||
|
console.log(`Processing GET /api/announcements/from-game`);
|
||||||
|
const fromGameData = GetFromGame.parse(req.query);
|
||||||
|
console.log(`Request from game ${fromGameData.gameName}`);
|
||||||
|
const announcements = await db
|
||||||
|
.selectFrom("announcements")
|
||||||
|
.select(["id", "title", "text", "publication_date", "image"])
|
||||||
|
.orderBy("announcements.publication_date", "desc")
|
||||||
|
.where("publication_date", "<=", new Date().toISOString())
|
||||||
|
.limit(3)
|
||||||
|
.execute();
|
||||||
|
res.json(announcements);
|
||||||
|
});
|
||||||
|
|
||||||
|
apiRouter.get("/announcements/:announcementId", async (req, res) => {
|
||||||
|
console.log(`Processing GET /api/announcements/:announcementId`);
|
||||||
|
const { announcementId } = AnnouncementIdInput.parse(req.params);
|
||||||
|
const announcement = await db
|
||||||
|
.selectFrom("announcements")
|
||||||
|
.select(["id", "title", "text", "publication_date", "image"])
|
||||||
|
.where("id", "=", announcementId)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
res.json(announcement);
|
||||||
|
});
|
||||||
|
|
||||||
|
apiRouter.post("/announcements", upload.single("image"), async (req, res) => {
|
||||||
|
console.log(`Processing POST /api/announcements`);
|
||||||
|
const postInput = PostInput.parse(req);
|
||||||
|
const announcement = {
|
||||||
|
title: postInput.body.title,
|
||||||
|
text: postInput.body.text,
|
||||||
|
publication_date: postInput.body.publication_date,
|
||||||
|
image: postInput.file.filename,
|
||||||
|
};
|
||||||
|
await db.insertInto("announcements").values(announcement).execute();
|
||||||
|
res.redirect("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
apiRouter.post(
|
||||||
|
"/announcements/:announcementId",
|
||||||
|
upload.single("image"),
|
||||||
|
async (req, res) => {
|
||||||
|
console.log(`Processing POST /api/announcements/:announcementId`);
|
||||||
|
const patchInput = PatchInput.parse(req);
|
||||||
|
const { announcementId } = AnnouncementIdInput.parse(req.params);
|
||||||
|
await db
|
||||||
|
.updateTable("announcements")
|
||||||
|
.set({
|
||||||
|
title: patchInput.body.title,
|
||||||
|
text: patchInput.body.text,
|
||||||
|
publication_date: patchInput.body.publication_date,
|
||||||
|
image: patchInput.file?.filename,
|
||||||
|
})
|
||||||
|
.where("id", "=", announcementId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
res.redirect("/");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
apiRouter.delete("/announcements/:announcementId", async (req, res) => {
|
||||||
|
console.log(`Processing DELETE /api/announcements`);
|
||||||
|
|
||||||
|
const { announcementId } = AnnouncementIdInput.parse(req.params);
|
||||||
|
|
||||||
|
const { image } = await db
|
||||||
|
.selectFrom("announcements")
|
||||||
|
.select("image")
|
||||||
|
.where("id", "=", announcementId)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
await db
|
||||||
|
.deleteFrom("announcements")
|
||||||
|
.where("id", "=", announcementId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await unlink(path.join(baseUploadsFolder, image));
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
});
|
||||||
|
|
||||||
|
apiRouter.use(
|
||||||
|
(_err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||||
|
console.error(_err);
|
||||||
|
res.status(500).send("Internal Server Error.");
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/";
|
||||||
|
console.log(`Using ${baseUploadsFolder} as base uploads folder`);
|
||||||
@@ -2,8 +2,10 @@ import SQLite from "better-sqlite3";
|
|||||||
import { Kysely, SqliteDialect } from "kysely";
|
import { Kysely, SqliteDialect } from "kysely";
|
||||||
import type { Database } from "./types.ts";
|
import type { Database } from "./types.ts";
|
||||||
|
|
||||||
|
const databasePath = process.env.DATABASE_PATH ?? "./data.db";
|
||||||
|
console.log(`Using ${databasePath} as database path`);
|
||||||
const dialect = new SqliteDialect({
|
const dialect = new SqliteDialect({
|
||||||
database: new SQLite("./data.db"),
|
database: new SQLite(databasePath),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const db = new Kysely<Database>({
|
export const db = new Kysely<Database>({
|
||||||
|
|||||||
+6
-123
@@ -1,133 +1,16 @@
|
|||||||
import { unlink } from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import multer from "multer";
|
|
||||||
import ViteExpress from "vite-express";
|
import ViteExpress from "vite-express";
|
||||||
import z from "zod";
|
import { apiRouter } from "./api.ts";
|
||||||
import { db } from "./db/database.ts";
|
import { baseUploadsFolder } from "./base-uploads-folder.ts";
|
||||||
|
|
||||||
const baseUploadsFolder = "uploads/";
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
dest: baseUploadsFolder,
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const PostInput = z.object({
|
app.use((req, _res, next) => {
|
||||||
file: z.object({ filename: z.string() }),
|
console.log(`Received request ${req.url}`);
|
||||||
body: z.object({
|
next();
|
||||||
publication_date: z.coerce
|
|
||||||
.date()
|
|
||||||
.transform((x) => new Date(x).toISOString()),
|
|
||||||
title: z.string().nonempty(),
|
|
||||||
text: z.string().nonempty(),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const PatchInput = z.object({
|
app.use("/api", apiRouter);
|
||||||
file: z.object({ filename: z.string().optional() }).optional(),
|
|
||||||
body: z.object({
|
|
||||||
publication_date: z.coerce
|
|
||||||
.date()
|
|
||||||
.optional()
|
|
||||||
.transform((x) => (x ? new Date(x).toISOString() : x)),
|
|
||||||
title: z.string().nonempty().optional(),
|
|
||||||
text: z.string().nonempty().optional(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const AnnouncementIdInput = z.object({
|
|
||||||
announcementId: z.coerce.number().positive(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const GetFromGame = z.object({
|
|
||||||
gameName: z.string().nonempty(),
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/api/announcements", async (_, res) => {
|
|
||||||
const announcements = await db
|
|
||||||
.selectFrom("announcements")
|
|
||||||
.select(["id", "title", "text", "publication_date", "image"])
|
|
||||||
.orderBy("announcements.publication_date", "desc")
|
|
||||||
.execute();
|
|
||||||
res.json(announcements);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/api/announcements/from-game", async (req, res) => {
|
|
||||||
const fromGameData = GetFromGame.parse(req.query);
|
|
||||||
console.log(`Request from game ${fromGameData.gameName}`);
|
|
||||||
const announcements = await db
|
|
||||||
.selectFrom("announcements")
|
|
||||||
.select(["id", "title", "text", "publication_date", "image"])
|
|
||||||
.orderBy("announcements.publication_date", "desc")
|
|
||||||
.where("publication_date", "<=", new Date().toISOString())
|
|
||||||
.limit(3)
|
|
||||||
.execute();
|
|
||||||
res.json(announcements);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/api/announcements/:announcementId", async (req, res) => {
|
|
||||||
const { announcementId } = AnnouncementIdInput.parse(req.params);
|
|
||||||
const announcement = await db
|
|
||||||
.selectFrom("announcements")
|
|
||||||
.select(["id", "title", "text", "publication_date", "image"])
|
|
||||||
.where("id", "=", announcementId)
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
res.json(announcement);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/announcements", upload.single("image"), async (req, res) => {
|
|
||||||
const postInput = PostInput.parse(req);
|
|
||||||
const announcement = {
|
|
||||||
title: postInput.body.title,
|
|
||||||
text: postInput.body.text,
|
|
||||||
publication_date: postInput.body.publication_date,
|
|
||||||
image: postInput.file.filename,
|
|
||||||
};
|
|
||||||
await db.insertInto("announcements").values(announcement).execute();
|
|
||||||
res.redirect("/");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post(
|
|
||||||
"/api/announcements/:announcementId",
|
|
||||||
upload.single("image"),
|
|
||||||
async (req, res) => {
|
|
||||||
const patchInput = PatchInput.parse(req);
|
|
||||||
const { announcementId } = AnnouncementIdInput.parse(req.params);
|
|
||||||
await db
|
|
||||||
.updateTable("announcements")
|
|
||||||
.set({
|
|
||||||
title: patchInput.body.title,
|
|
||||||
text: patchInput.body.text,
|
|
||||||
publication_date: patchInput.body.publication_date,
|
|
||||||
image: patchInput.file?.filename,
|
|
||||||
})
|
|
||||||
.where("id", "=", announcementId)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
res.redirect("/");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
app.delete("/api/announcements/:announcementId", async (req, res) => {
|
|
||||||
const { announcementId } = AnnouncementIdInput.parse(req.params);
|
|
||||||
|
|
||||||
const { image } = await db
|
|
||||||
.selectFrom("announcements")
|
|
||||||
.select("image")
|
|
||||||
.where("id", "=", announcementId)
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
|
|
||||||
await db
|
|
||||||
.deleteFrom("announcements")
|
|
||||||
.where("id", "=", announcementId)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await unlink(path.join(baseUploadsFolder, image));
|
|
||||||
|
|
||||||
res.status(204).send();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use("/images", express.static(baseUploadsFolder));
|
app.use("/images", express.static(baseUploadsFolder));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user