1
0
Fork 0
mirror of https://git.sr.ht/~roxwize/mipilin synced 2025-01-30 18:53:36 +00:00

Rae from website

Signed-off-by: roxwize <rae@roxwize.xyz>
This commit is contained in:
Rae 5e 2025-01-22 20:34:25 -05:00
parent 453a143bfa
commit a3e6df6ce8
Signed by: rae
GPG key ID: 5B1A0FAB9BAB81EE
13 changed files with 691 additions and 35 deletions

View file

@ -9,3 +9,5 @@
- [ ] Visibility indicator on journal entries
- [ ] Hide journal entries from feed that are hidden unless current user is a moderator
- [ ] Edit/delete journal entries?
- [ ] Journal entry comments
- [ ] A Forum

View file

@ -38,6 +38,18 @@ export const journalEntries = pgTable("journal_entries", {
date: timestamp().notNull()
});
export const journalComments = pgTable("journal_comments", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
user: integer()
.references(() => users.id, { onDelete: "cascade" })
.notNull(),
entry: integer()
.references(() => journalEntries.id, { onDelete: "cascade" })
.notNull(),
content: varchar({ length: 512 }).notNull(),
date: timestamp().notNull()
});
export const profiles = pgTable("profiles", {
user: integer()
.references(() => users.id, { onDelete: "cascade" })

View file

@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS "journal_comments" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "journal_comments_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"user" integer NOT NULL,
"entry" integer NOT NULL,
"content" varchar(512) NOT NULL,
"date" timestamp NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "journal_comments" ADD CONSTRAINT "journal_comments_user_users_id_fk" FOREIGN KEY ("user") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "journal_comments" ADD CONSTRAINT "journal_comments_entry_journal_entries_id_fk" FOREIGN KEY ("entry") REFERENCES "public"."journal_entries"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,496 @@
{
"id": "0c1bcfed-899c-4ec9-9a7d-c987aa50dfb8",
"prevId": "f8e1fcdc-dfa2-41cd-886a-5f29a1c091df",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.follows": {
"name": "follows",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"follower_id": {
"name": "follower_id",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"follows_user_id_users_id_fk": {
"name": "follows_user_id_users_id_fk",
"tableFrom": "follows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"follows_follower_id_users_id_fk": {
"name": "follows_follower_id_users_id_fk",
"tableFrom": "follows",
"tableTo": "users",
"columnsFrom": [
"follower_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"follows_user_id_follower_id_pk": {
"name": "follows_user_id_follower_id_pk",
"columns": [
"user_id",
"follower_id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.invite_codes": {
"name": "invite_codes",
"schema": "",
"columns": {
"token": {
"name": "token",
"type": "varchar(22)",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"granted": {
"name": "granted",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"expires": {
"name": "expires",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "'1970-01-01 00:00:00.000'"
},
"confers": {
"name": "confers",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"invite_codes_user_id_users_id_fk": {
"name": "invite_codes_user_id_users_id_fk",
"tableFrom": "invite_codes",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.journal_comments": {
"name": "journal_comments",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "journal_comments_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"user": {
"name": "user",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"entry": {
"name": "entry",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "varchar(512)",
"primaryKey": false,
"notNull": true
},
"date": {
"name": "date",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"journal_comments_user_users_id_fk": {
"name": "journal_comments_user_users_id_fk",
"tableFrom": "journal_comments",
"tableTo": "users",
"columnsFrom": [
"user"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"journal_comments_entry_journal_entries_id_fk": {
"name": "journal_comments_entry_journal_entries_id_fk",
"tableFrom": "journal_comments",
"tableTo": "journal_entries",
"columnsFrom": [
"entry"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.journal_entries": {
"name": "journal_entries",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "journal_entries_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"user": {
"name": "user",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"mood-change": {
"name": "mood-change",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"title": {
"name": "title",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"entry": {
"name": "entry",
"type": "varchar(4096)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"visibility": {
"name": "visibility",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"date": {
"name": "date",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"journal_entries_user_users_id_fk": {
"name": "journal_entries_user_users_id_fk",
"tableFrom": "journal_entries",
"tableTo": "users",
"columnsFrom": [
"user"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.profiles": {
"name": "profiles",
"schema": "",
"columns": {
"user": {
"name": "user",
"type": "integer",
"primaryKey": true,
"notNull": true
},
"bio": {
"name": "bio",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"website": {
"name": "website",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {
"profiles_user_users_id_fk": {
"name": "profiles_user_users_id_fk",
"tableFrom": "profiles",
"tableTo": "users",
"columnsFrom": [
"user"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.updates": {
"name": "updates",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "updates_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"user": {
"name": "user",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"mood": {
"name": "mood",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"description": {
"name": "description",
"type": "varchar(512)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"date": {
"name": "date",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"updates_user_users_id_fk": {
"name": "updates_user_users_id_fk",
"tableFrom": "updates",
"tableTo": "users",
"columnsFrom": [
"user"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "users_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"email": {
"name": "email",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(26)",
"primaryKey": false,
"notNull": true
},
"pass": {
"name": "pass",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"registered": {
"name": "registered",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
},
"users_name_unique": {
"name": "users_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -22,6 +22,13 @@
"when": 1735185254730,
"tag": "0002_special_ben_parker",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1737088748932,
"tag": "0003_closed_alex_power",
"breakpoints": true
}
]
}

View file

@ -58,6 +58,10 @@ object-src 'none'; base-uri 'none';"
);
return next();
});
app.use((req, res, next) => {
console.log(`${req.ip.padEnd(24)} ${req.method.padStart(8)} ${req.path}`);
return next();
})
await init(app, db);

View file

@ -10,7 +10,14 @@ import {
} from "../db/schema.js";
import { and, count, desc, eq, sql } from "drizzle-orm";
import dayjs from "dayjs";
import { getMoods, render, render404, UserStatus } from "./util.js";
import {
confirm,
getMoods,
journalMoodString,
render,
render404,
UserStatus
} from "./util.js";
export default async function (app: Express, db: NodePgDatabase) {
const { moods, moodsSorted } = await getMoods();
@ -155,17 +162,28 @@ export default async function (app: Express, db: NodePgDatabase) {
render(db, "journal", "your journal", res, req);
});
app.get("/journal/:id", async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
req.flash("error", "Invalid ID");
render404(db, res, req);
return;
}
const entry: {
uname: string,
title: string,
content: string,
moodChange: number,
moodString?: string,
date: Date,
visibility: number
id: number;
uid: number;
uname: string;
title: string;
content: string;
moodChange: number;
moodString?: string;
date: Date;
visibility: number;
} = (
await db
.select({
id: journalEntries.id,
uid: users.id,
uname: users.name,
title: journalEntries.title,
content: journalEntries.entry,
@ -174,45 +192,88 @@ export default async function (app: Express, db: NodePgDatabase) {
visibility: journalEntries.visibility
})
.from(journalEntries)
.where(eq(journalEntries.id, parseInt(req.params.id)))
.where(eq(journalEntries.id, id))
.leftJoin(users, eq(journalEntries.user, users.id))
)[0];
//? put into util function?
//? also GOD
switch (entry.moodChange) {
case -2:
entry.moodString = "much worse"
break;
case -1:
entry.moodString = "worse";
break;
default:
case 0:
entry.moodString = "about the same";
break;
case 1:
entry.moodString = "better";
break;
case 2:
entry.moodString = "much better";
break;
}
entry.moodString = journalMoodString(entry.moodChange);
const isMod = req.session["status"] & UserStatus.MODERATOR;
if (
!entry ||
(entry.visibility === 0 && entry.uname !== req.session["user"] && !(req.session["status"] & UserStatus.MODERATOR))
(entry.visibility === 0 &&
entry.uname !== req.session["user"] && !isMod)
) {
render404(db, res, req);
return;
}
// maybe turn ([x] !== req.session["user"] && !(req.session["status"] & UserStatus.MODERATOR)) into util function
entry.content =
entry.visibility === 2 &&
entry.uid !== req.session["uid"] &&
!(req.session["status"] & UserStatus.MODERATOR)
? "This journal entry's contents have been made private."
: entry.content;
const entryTimestamp = dayjs(entry.date).fromNow();
const isSelf = entry.uid === req.session["uid"];
render(db, "journal_view", entry.title, res, req, {
entry,
entryTimestamp
entryTimestamp,
isSelf,
isMod
});
});
app.post("/journal/:id/edit", async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
req.flash("error", "Invalid ID");
render404(db, res, req);
return;
}
const entry = (
await db
.select({
uid: journalEntries.user
})
.from(journalEntries)
.where(eq(journalEntries.id, id))
.limit(1)
)[0];
const isMod = req.session["status"] & UserStatus.MODERATOR;
if (
!entry ||
(entry?.uid !== req.session["uid"] &&
!isMod)
) {
render404(db, res, req);
return;
}
if (isMod && entry.uid !== req.session["uid"] && req.body.action !== "delete") {
req.flash("error", "Moderators can only delete other users' posts.");
res.redirect(`/journal/${req.params.id}`);
return;
}
if (req.body.action === "edit") {
// TODO!!
} else if (req.body.action === "delete") {
if (!req.body.confirm) {
confirm(db, res, req);
return;
}
await db.delete(journalEntries).where(eq(journalEntries.id, id));
req.flash("success", "Journal entry deleted ;w;");
res.redirect("/");
} else {
render404(db, res, req);
}
});
app.post("/update/journal", async (req, res) => {
if (!req.session["loggedIn"]) {
res.redirect("/login");

View file

@ -7,7 +7,7 @@ import {
updates,
users
} from "../db/schema.js";
import { and, desc, eq } from "drizzle-orm";
import { and, desc, eq, ne } from "drizzle-orm";
import { getMoods, render, render404, UserStatus } from "./util.js";
import { PgColumn } from "drizzle-orm/pg-core";
import dayjs from "dayjs";
@ -79,10 +79,15 @@ export default async function (app: Express, db: NodePgDatabase) {
.select({
id: journalEntries.id,
title: journalEntries.title,
date: journalEntries.date
date: journalEntries.date,
visibility: journalEntries.visibility
})
.from(journalEntries)
.where(eq(journalEntries.user, user.id))
.where(
user.id === req.session["uid"] || req.session["status"] & UserStatus.MODERATOR
? eq(journalEntries.user, user.id)
: and(eq(journalEntries.user, user.id), ne(journalEntries.visibility, 0))
)
.orderBy(desc(journalEntries.date))
.limit(5)
).map((e) => {
@ -90,6 +95,7 @@ export default async function (app: Express, db: NodePgDatabase) {
id: e.id,
title: e.title,
date: e.date,
visibility: e.visibility,
relativeDate: now.to(e.date)
};
});
@ -115,7 +121,7 @@ export default async function (app: Express, db: NodePgDatabase) {
};
});
if (!isSelf) {
if (!isSelf && userMood) {
userMood.mood = moods[userMood.mood as number];
}

View file

@ -115,3 +115,27 @@ export async function createInviteCode(
});
return token;
}
export function journalMoodString(mood: number) {
switch (mood) {
case -2:
return "much worse";
case -1:
return "worse";
default:
case 0:
return "about the same";
case 1:
return "better";
case 2:
return "much better";
}
}
export function confirm(
db: NodePgDatabase,
res: Response,
req: Request
) {
render(db, "confirm", "Confirm action", res, req, { body: req.body, url: req.url });
}

View file

@ -166,6 +166,7 @@ input {
background-color: white;
padding: 4px;
font-family: inherit;
font-size: 14px;
}
button:focus,
textarea:focus,

9
views/confirm.pug Normal file
View file

@ -0,0 +1,9 @@
extends site.pug
block content
form(action=url, method="post")
for [k, v] of Object.entries(body)
input(type="text", name=k, value=v, hidden)
p Sure you want to do this??
button(name="confirm", value="y") Yes
button(onclick="history.back();") NO!!!!

View file

@ -1,7 +1,14 @@
extends site.pug
block content
h1= entry.title
form(action=`/journal/${entry.id}/edit`, method="post")
h1(style="display:flex;align-items:center;gap:1em;")
| #{entry.title}
if isSelf || isMod
div(style="display:flex;")
if isSelf
button(name="action", value="edit") edit
button(name="action", value="delete") delete
span
| by
a(href=`/users/${entry.uname}`)= entry.uname

View file

@ -34,7 +34,15 @@ block content
for entry of userJournalEntries
li
a(href=`/journal/${entry.id}`)= entry.title
span.subtle(title=entry.date.toLocaleString()) (#{entry.relativeDate})
span.subtle(title=entry.date.toLocaleString())
| (#{entry.relativeDate}
if entry.visibility !== 1
| ,
if entry.visibility === 0
| private
if entry.visibility === 2
| mood-only
| )
else
div [no entries]
br