Compare commits
40 Commits
b8bb83624d
...
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 |
@@ -7,11 +7,55 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
checkout:
|
||||
name: Build
|
||||
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
|
||||
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:
|
||||
|
||||
```
|
||||
NODE_ENV=production HTTP_PORT=1234 DATABASE_PATH=/opt/owof-announcements/data.db UPLOADS_DIR=/opt/owof-announcements/uploads node /opt/owof-announcements/code/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:
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"dev": "nodemon -w src/server -x tsx src/server/main.ts",
|
||||
"start": "NODE_ENV=production tsx src/server/main.ts",
|
||||
"build": "vite build",
|
||||
"biome-full": "biome check --write"
|
||||
"biome-full": "biome check --write --unsafe"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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 type { Database } from "./types.ts";
|
||||
|
||||
const databasePath = process.env.DATABASE_PATH ?? "./data.db";
|
||||
console.log(`Using ${databasePath} as database path`);
|
||||
const dialect = new SqliteDialect({
|
||||
database: new SQLite(process.env.DATABASE_PATH ?? "./data.db"),
|
||||
database: new SQLite(databasePath),
|
||||
});
|
||||
|
||||
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 multer from "multer";
|
||||
import ViteExpress from "vite-express";
|
||||
import z from "zod";
|
||||
import { db } from "./db/database.ts";
|
||||
|
||||
const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/";
|
||||
|
||||
const upload = multer({
|
||||
dest: baseUploadsFolder,
|
||||
});
|
||||
import { apiRouter } from "./api.ts";
|
||||
import { baseUploadsFolder } from "./base-uploads-folder.ts";
|
||||
|
||||
const app = express();
|
||||
|
||||
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(),
|
||||
}),
|
||||
app.use((req, _res, next) => {
|
||||
console.log(`Received request ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
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("/api", apiRouter);
|
||||
|
||||
app.use("/images", express.static(baseUploadsFolder));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user