i dont really know what i added
Signed-off-by: roxwize <rae@roxwize.xyz>
|
@ -27,6 +27,15 @@ export const updates = pgTable("updates", {
|
|||
date: timestamp().notNull()
|
||||
});
|
||||
|
||||
export const journalEntries = pgTable("journal_entries", {
|
||||
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||
user: integer().references(() => users.id, { onDelete: "cascade" }).notNull(),
|
||||
moodChange: integer("mood-change").default(0).notNull(),
|
||||
entry: varchar({ length: 4096 }).default("").notNull(),
|
||||
visibility: integer().default(1).notNull(),
|
||||
date: timestamp().notNull()
|
||||
});
|
||||
|
||||
export const profiles = pgTable("profiles", {
|
||||
user: integer()
|
||||
.references(() => users.id, { onDelete: "cascade" })
|
||||
|
|
14
drizzle/0006_lame_grey_gargoyle.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE IF NOT EXISTS "journal_entries" (
|
||||
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "journal_entries_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||
"user" integer NOT NULL,
|
||||
"mood-change" integer DEFAULT 0 NOT NULL,
|
||||
"entry" varchar(4096) DEFAULT '' NOT NULL,
|
||||
"visibility" integer DEFAULT 1 NOT NULL,
|
||||
"date" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "journal_entries" ADD CONSTRAINT "journal_entries_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 $$;
|
356
drizzle/meta/0006_snapshot.json
Normal file
|
@ -0,0 +1,356 @@
|
|||
{
|
||||
"id": "73a239ee-a945-48e0-a021-d415815a830b",
|
||||
"prevId": "f8ae31ec-68e9-4857-93b2-abd7f834e580",
|
||||
"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.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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"moderator": {
|
||||
"name": "moderator",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"banned": {
|
||||
"name": "banned",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
|
@ -43,6 +43,13 @@
|
|||
"when": 1731393095652,
|
||||
"tag": "0005_magical_swarm",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1731868543611,
|
||||
"tag": "0006_lame_grey_gargoyle",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
133
main.ts
|
@ -1,5 +1,5 @@
|
|||
//! TODO: There is like no error checking in queries at all
|
||||
import type { Response, Request } from "express";
|
||||
//! TODO: Also no API? seriously? also when a form errors it just redirects with a 301 like everything is okay when it ISNT and it NEVER HAS BEEN
|
||||
|
||||
import "dotenv/config";
|
||||
import dayjs from "dayjs";
|
||||
|
@ -12,19 +12,20 @@ import connectPgSimple from "connect-pg-simple";
|
|||
import flash from "connect-flash";
|
||||
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { follows, profiles, updates, users } from "./db/schema.js";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { updates, users } from "./db/schema.js";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
// routes
|
||||
import loginRoutes from "./routes/login.js";
|
||||
import userRoutes from "./routes/users.js";
|
||||
import { getMoods, render } from "./routes/util.js";
|
||||
import updateRoutes from "./routes/updates.js";
|
||||
import { getMoods, render, setNonce } from "./routes/util.js";
|
||||
|
||||
const db = drizzle(process.env.DATABASE_URL!);
|
||||
|
||||
//! TODO: Make sure SQL queries arent being repeated too much
|
||||
(async () => {
|
||||
const { moods, moodsSorted } = await getMoods();
|
||||
const { moods } = await getMoods();
|
||||
|
||||
// setup dayjs
|
||||
dayjs.extend(relativeTime);
|
||||
|
@ -43,6 +44,19 @@ const db = drizzle(process.env.DATABASE_URL!);
|
|||
})
|
||||
);
|
||||
app.use(flash());
|
||||
//== Content Security Policy
|
||||
app.use((_, res, next) => {
|
||||
res.setHeader("X-Powered-By", "Lots and lots of milk");
|
||||
res.setHeader(
|
||||
"Content-Security-Policy",
|
||||
"\
|
||||
script-src 'nonce-" +
|
||||
setNonce() +
|
||||
"';\
|
||||
object-src 'none'; base-uri 'none';"
|
||||
);
|
||||
return next();
|
||||
});
|
||||
|
||||
app.get("/", async (req, res) => {
|
||||
const upd = db
|
||||
|
@ -57,121 +71,38 @@ const db = drizzle(process.env.DATABASE_URL!);
|
|||
.as("upd");
|
||||
const recentUpdates = await db.select().from(upd).orderBy(desc(upd.date));
|
||||
|
||||
render(db, "index", "Home", res, req, {
|
||||
users: (await db.select().from(users)).length,
|
||||
recentUpdates
|
||||
});
|
||||
});
|
||||
|
||||
await userRoutes(app, db);
|
||||
|
||||
//! -- TEMP DASHBOARD START --
|
||||
app.get("/dashboard", async (req, res) => {
|
||||
if (!req.session["loggedIn"]) {
|
||||
res.redirect("/login");
|
||||
return;
|
||||
}
|
||||
const user = (
|
||||
await db
|
||||
.select({
|
||||
name: users.name,
|
||||
bio: profiles.bio,
|
||||
website: profiles.website //! validate this
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.name, req.session["user"]))
|
||||
.leftJoin(profiles, eq(users.id, profiles.user))
|
||||
)[0];
|
||||
|
||||
const now = dayjs();
|
||||
const moodHistory = (
|
||||
await db
|
||||
.select({ mood: updates.mood, date: updates.date })
|
||||
.from(updates)
|
||||
.where(eq(updates.user, req.session["uid"]))
|
||||
.orderBy(desc(updates.date))
|
||||
.limit(10)
|
||||
).map((e) => {
|
||||
return { mood: moods[e.mood], date: now.to(dayjs(e.date)) };
|
||||
});
|
||||
|
||||
const recentUpdates = (
|
||||
const date = dayjs();
|
||||
const feedUpdates = (
|
||||
await db
|
||||
.select({
|
||||
user: users.name,
|
||||
mood: updates.mood,
|
||||
desc: updates.description,
|
||||
date: updates.date
|
||||
date: updates.date,
|
||||
desc: updates.description
|
||||
})
|
||||
.from(updates)
|
||||
.innerJoin(
|
||||
follows,
|
||||
and(
|
||||
eq(follows.userId, updates.user),
|
||||
eq(follows.followerId, req.session["uid"])
|
||||
)
|
||||
)
|
||||
.leftJoin(users, eq(updates.user, users.id))
|
||||
.innerJoin(users, eq(updates.user, users.id))
|
||||
.limit(50)
|
||||
.orderBy(desc(updates.date))
|
||||
.limit(25)
|
||||
).map((e) => {
|
||||
return {
|
||||
user: e.user,
|
||||
mood: moods[e.mood],
|
||||
desc: e.desc,
|
||||
date: dayjs().to(dayjs(e.date))
|
||||
date: date.to(dayjs(e.date)),
|
||||
desc: e.desc
|
||||
};
|
||||
});
|
||||
|
||||
render(db, "dashboard", "Dashboard", res, req, {
|
||||
user,
|
||||
moods,
|
||||
moodsSorted,
|
||||
moodHistory,
|
||||
render(db, "index", "Home", res, req, {
|
||||
users: (await db.select().from(users)).length,
|
||||
recentUpdates,
|
||||
feed: []
|
||||
feedUpdates
|
||||
});
|
||||
});
|
||||
app.post("/update", async (req, res) => {
|
||||
if (!req.session["loggedIn"]) {
|
||||
res.redirect("/login");
|
||||
return;
|
||||
}
|
||||
const moodIndex = moods.indexOf(req.body.mood.trim());
|
||||
if (moodIndex === -1) {
|
||||
req.flash(
|
||||
"error",
|
||||
"That mood doesn't exist in the database, WTF are you trying to do??"
|
||||
);
|
||||
res.redirect("/dashboard");
|
||||
return;
|
||||
}
|
||||
if (req.body.desc.length > 512) {
|
||||
req.flash(
|
||||
"error",
|
||||
"Mood description can't be longer than 512 characters"
|
||||
);
|
||||
res.redirect("/dashboard");
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(updates)
|
||||
// @ts-expect-error
|
||||
.values({
|
||||
user: req.session["uid"],
|
||||
mood: moodIndex,
|
||||
description: req.body.desc,
|
||||
date: new Date(Date.now())
|
||||
});
|
||||
req.flash("success", "Mood updated!");
|
||||
res.redirect("/dashboard");
|
||||
});
|
||||
//! -- TEMP DASHBOARD END --
|
||||
|
||||
//! TEMP Also not sanitized like at all
|
||||
//! Also make sure user isnt logged in before doing this
|
||||
await userRoutes(app, db);
|
||||
await updateRoutes(app, db);
|
||||
loginRoutes(app, db);
|
||||
//! TEMP done
|
||||
|
||||
app.listen(1337, () => {
|
||||
console.log("Listening on http://127.0.0.1:1337/");
|
||||
|
|
|
@ -5,6 +5,8 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|||
import { profiles, users } from "../db/schema.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
//! TEMP Also not sanitized like at all
|
||||
//! Also make sure user isnt logged in before doing this
|
||||
export default function(app: Express, db: NodePgDatabase) {
|
||||
app.get("/register", (req, res) => {
|
||||
if (req.session["loggedIn"]) {
|
||||
|
|
161
routes/updates.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
||||
import { Express } from "express";
|
||||
import {
|
||||
follows,
|
||||
journalEntries,
|
||||
profiles,
|
||||
updates,
|
||||
users
|
||||
} from "../db/schema.js";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { getMoods, render } from "./util.js";
|
||||
|
||||
export default async function (app: Express, db: NodePgDatabase) {
|
||||
const { moods, moodsSorted } = await getMoods();
|
||||
|
||||
// DASHBOARD
|
||||
app.get("/dashboard", async (req, res) => {
|
||||
if (!req.session["loggedIn"]) {
|
||||
res.redirect("/login");
|
||||
return;
|
||||
}
|
||||
const user = (
|
||||
await db
|
||||
.select({
|
||||
name: users.name,
|
||||
bio: profiles.bio,
|
||||
website: profiles.website //! validate this
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.name, req.session["user"]))
|
||||
.leftJoin(profiles, eq(users.id, profiles.user))
|
||||
)[0];
|
||||
|
||||
const now = dayjs();
|
||||
const moodHistory = (
|
||||
await db
|
||||
.select({ mood: updates.mood, date: updates.date })
|
||||
.from(updates)
|
||||
.where(eq(updates.user, req.session["uid"]))
|
||||
.orderBy(desc(updates.date))
|
||||
.limit(10)
|
||||
).map((e) => {
|
||||
return { mood: moods[e.mood], date: now.to(dayjs(e.date)) };
|
||||
});
|
||||
|
||||
const recentUpdates = (
|
||||
await db
|
||||
.select({
|
||||
user: users.name,
|
||||
mood: updates.mood,
|
||||
desc: updates.description,
|
||||
date: updates.date
|
||||
})
|
||||
.from(updates)
|
||||
.innerJoin(
|
||||
follows,
|
||||
and(
|
||||
eq(follows.userId, updates.user),
|
||||
eq(follows.followerId, req.session["uid"])
|
||||
)
|
||||
)
|
||||
.leftJoin(users, eq(updates.user, users.id))
|
||||
.orderBy(desc(updates.date))
|
||||
.limit(25)
|
||||
).map((e) => {
|
||||
return {
|
||||
user: e.user,
|
||||
mood: moods[e.mood],
|
||||
desc: e.desc,
|
||||
date: now.to(dayjs(e.date))
|
||||
};
|
||||
});
|
||||
|
||||
render(db, "dashboard", "Dashboard", res, req, {
|
||||
user,
|
||||
moods,
|
||||
moodsSorted,
|
||||
moodHistory,
|
||||
recentUpdates,
|
||||
feed: []
|
||||
});
|
||||
});
|
||||
app.post("/update/mood", async (req, res) => {
|
||||
if (!req.session["loggedIn"]) {
|
||||
res.redirect("/login");
|
||||
return;
|
||||
}
|
||||
const moodIndex = moods.indexOf(req.body.mood.trim());
|
||||
if (moodIndex === -1) {
|
||||
req.flash(
|
||||
"error",
|
||||
"That mood doesn't exist in the database, WTF are you trying to do??"
|
||||
);
|
||||
res.redirect("/dashboard");
|
||||
return;
|
||||
}
|
||||
if (req.body.desc.length > 512) {
|
||||
req.flash(
|
||||
"error",
|
||||
"Mood description can't be longer than 512 characters"
|
||||
);
|
||||
res.redirect("/dashboard");
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(updates)
|
||||
// @ts-expect-error
|
||||
.values({
|
||||
user: req.session["uid"],
|
||||
mood: moodIndex,
|
||||
description: req.body.desc,
|
||||
date: new Date(Date.now())
|
||||
});
|
||||
req.flash("success", "Mood updated!");
|
||||
res.redirect("/dashboard");
|
||||
});
|
||||
|
||||
// JOURNAL
|
||||
app.get("/journal", async (req, res) => {
|
||||
render(db, "journal", "Journal", res, req);
|
||||
});
|
||||
app.post("/update/journal", async (req, res) => {
|
||||
if (!req.session["loggedIn"]) {
|
||||
res.redirect("/login");
|
||||
return;
|
||||
}
|
||||
if (req.body.description.length > 4096) {
|
||||
req.flash("error", "Entry too long!");
|
||||
res.redirect("/journal");
|
||||
return;
|
||||
}
|
||||
|
||||
const moodChange = parseInt(req.body.moodDelta);
|
||||
const visibility = parseInt(req.body.visibility);
|
||||
if (isNaN(moodChange) || isNaN(visibility)) {
|
||||
req.flash("error", "One of the values was improperly specified.");
|
||||
res.redirect("/journal");
|
||||
return;
|
||||
}
|
||||
|
||||
let id: number;
|
||||
try {
|
||||
// @ts-expect-error
|
||||
const entry = await db.insert(journalEntries).values({
|
||||
user: req.session["uid"],
|
||||
moodChange,
|
||||
visibility,
|
||||
entry: req.body.description,
|
||||
date: new Date(Date.now())
|
||||
}).returning({ id: journalEntries.id });
|
||||
id = entry[0].id;
|
||||
} catch (err) {
|
||||
req.flash("error", "Failed to create your entry. Try again later or send these logs to roxwize so she can know what's up:<br><br>"+err);
|
||||
res.redirect("/journal");
|
||||
return;
|
||||
}
|
||||
req.flash("success", `Your journal entry is now available as <a href="/journal/view?id=${id}">#${id}</a>!`);
|
||||
res.redirect("/journal");
|
||||
});
|
||||
}
|
|
@ -4,6 +4,7 @@ import { follows, profiles, updates, users } from "../db/schema.js";
|
|||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { getMoods, render } from "./util.js";
|
||||
import { PgColumn } from "drizzle-orm/pg-core";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default async function (app: Express, db: NodePgDatabase) {
|
||||
const { moods } = await getMoods();
|
||||
|
@ -57,6 +58,19 @@ export default async function (app: Express, db: NodePgDatabase) {
|
|||
.limit(1)
|
||||
)[0];
|
||||
|
||||
// feed
|
||||
const now = dayjs();
|
||||
const userMoodFeed = (await db
|
||||
.select({
|
||||
mood: updates.mood,
|
||||
date: updates.date,
|
||||
desc: updates.description
|
||||
})
|
||||
.from(updates)
|
||||
.where(eq(updates.user, user.id))).map((e) => {
|
||||
return { user: user.name, mood: moods[e.mood], date: now.to(dayjs(e.date)), desc: e.desc }
|
||||
});
|
||||
|
||||
if (!isSelf) {
|
||||
userMood.mood = moods[userMood.mood as number];
|
||||
}
|
||||
|
@ -65,6 +79,7 @@ export default async function (app: Express, db: NodePgDatabase) {
|
|||
user,
|
||||
isSelf,
|
||||
userMood,
|
||||
userMoodFeed,
|
||||
isFollowing
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,21 @@ import { updates } from "../db/schema.js";
|
|||
import { desc, eq } from "drizzle-orm";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
const nonceChars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_";
|
||||
let nonce: string;
|
||||
export function setNonce() {
|
||||
nonce = "";
|
||||
for (let i = 0; i < 32; i++)
|
||||
nonce += nonceChars[Math.floor(Math.random() * nonceChars.length)];
|
||||
return nonce;
|
||||
}
|
||||
export function getNonce() {
|
||||
if (!nonce)
|
||||
throw new Error("Nonce doesn't exist");
|
||||
return nonce;
|
||||
}
|
||||
|
||||
let moods: string[], moodsSorted: string[];
|
||||
export async function getMoods() {
|
||||
if (!moods)
|
||||
|
@ -42,7 +57,8 @@ export async function render(
|
|||
session: req.session,
|
||||
flashes: req.flash(),
|
||||
moods,
|
||||
currentMood
|
||||
currentMood,
|
||||
nonce
|
||||
};
|
||||
res.render(page, { ...o, ...stuff });
|
||||
}
|
||||
|
|
|
@ -9,21 +9,6 @@ form *:last-child {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.feed-update {
|
||||
background-color: #d5d4bb;
|
||||
height: fit-content;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.feed-update div:first-child {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.feed-update div:last-child {
|
||||
text-align: right;
|
||||
color: #3f3f38;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#dashboard-update-form * {
|
||||
width: 50%;
|
||||
min-width: 128px;
|
||||
|
@ -43,20 +28,51 @@ form *:last-child {
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 900px) {
|
||||
#dashboard-feed {
|
||||
#journal-update {
|
||||
max-width: 880px;
|
||||
}
|
||||
|
||||
#journal-update .input {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.feed-update {
|
||||
padding: 4px;
|
||||
width: 280px;
|
||||
|
||||
#journal-update .input span {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#ovm {
|
||||
display: flex;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.feed-update {
|
||||
border-bottom: 1px solid #afa870;
|
||||
#visibility-control * {
|
||||
display: inline;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
input.ovm-input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
input.ovm-input:first-child {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
label.ovm-input > img {
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: #afa870;
|
||||
background-color: #d5d4bb;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
width: 64px;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
input.ovm-input:checked+label > img {
|
||||
background-color: #999883;
|
||||
filter: invert(1);
|
||||
}
|
||||
label.ovm-input:last-child > img {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
|
|
@ -169,8 +169,42 @@ select {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.feed-update {
|
||||
background-color: #d5d4bb;
|
||||
height: fit-content;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.feed-update div:first-child {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.feed-update div:last-child {
|
||||
text-align: right;
|
||||
color: #3f3f38;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Gohufont 14";
|
||||
src: url("/fonts/gohufont-14.ttf") format("truetype"),
|
||||
url("/fonts/gohufont-14.woff") format("woff");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 900px) {
|
||||
#feed {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.feed-update {
|
||||
padding: 4px;
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
.feed-update {
|
||||
border-bottom: 1px solid #afa870;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
|
3
static/img/down.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -6 6 6">
|
||||
<path d="M1-6v2H3L0 0-3-4h2V-6Z" fill="#000000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 119 B |
3
static/img/downdown.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -6 6 7.5">
|
||||
<path d="M-3-2.5h.75L0 .5l2.25-3H3l-3 4ZM1-6v2H3L0 0-3-4h2V-6Z" fill="#000000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 152 B |
3
static/img/line.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -0.5 6 1">
|
||||
<path d="M-3-.5H3v1H-3Z" fill="#000000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 113 B |
|
@ -1 +1,5 @@
|
|||
<svg width="88.8" height="41.8" style="fill:none" viewBox="0 0 444 209" xmlns="http://www.w3.org/2000/svg"><path style="fill:none;stroke:#faf8da;stroke-width:24;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" d="M342 192v0l-90-90V42l30-30h30l30 30 30-30h30l30 30v60l-90 90"/><path style="fill:none;stroke:#faf8da;stroke-width:24.2904;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:1" d="M12 197v0V12h180M12 102v0h150H12m180 0v0-90m0 90v0h-30"/></svg>
|
||||
<svg width="88.8" height="41.8" style="fill:none" viewBox="0 0 444 209"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path style="fill:none;stroke:#faf8da;stroke-width:24;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1" d="M342 192v0l-90-90V42l30-30h30l30 30 30-30h30l30 30v60l-90 90"/>
|
||||
<path style="fill:none;stroke:#faf8da;stroke-width:24.2904;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:1" d="M12 197v0V12h180M12 102v0h150H12m180 0v0-90m0 90v0h-30"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 486 B |
3
static/img/up.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -6 6 6">
|
||||
<path d="M1 0V-2H3L0-6-3-2h2V0Z" fill="#000000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 119 B |
3
static/img/upup.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -7.5 6 7.5">
|
||||
<path d="M1 0V-2H3L0-6-3-2h2V0ZM0-7.5l-3 4h.75L0-6.5l2.25 3H3Z" fill="#000000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 154 B |
14
views/_feed.pug
Normal file
|
@ -0,0 +1,14 @@
|
|||
mixin feed(feed, hideUser)
|
||||
if feed.length > 0
|
||||
#feed
|
||||
for update of feed
|
||||
.feed-update
|
||||
div
|
||||
a(href=`/users/${update.user}`)
|
||||
if !hideUser
|
||||
| #{update.user}
|
||||
strong= update.mood
|
||||
div= update.desc || "[no mood description provided]"
|
||||
div= update.date
|
||||
else
|
||||
span [no updates]
|
|
@ -25,8 +25,8 @@ block content
|
|||
details(style="margin-bottom:1em;")
|
||||
summary Moderation
|
||||
p If you're seeing this, you are part of the exclusive club of moderators! You are now entitled to pizza for the rest of your life! And the ability to ban people and create new moods! For FREE!
|
||||
form#dashboard-update-form(action="/update", method="post", onsubmit="disable(this);")
|
||||
select(name="mood")
|
||||
form#dashboard-update-form(action="/update/mood", method="post", onsubmit="disable(this);")
|
||||
select(name="mood", required)
|
||||
//- Maybe put the index of the mood in the value of the option element
|
||||
//- so that indexOf (slow) wont have to be used everytime mood is updated
|
||||
for mood of moodsSorted
|
||||
|
@ -34,17 +34,8 @@ block content
|
|||
textarea(name="desc", placeholder="mood description (max 512 chars)", rows="5", maxlength="512")
|
||||
button(type="submit") Update
|
||||
h1(style="margin-top:0.5em;") Feed
|
||||
if recentUpdates.length > 0
|
||||
#dashboard-feed
|
||||
for update of recentUpdates
|
||||
.feed-update
|
||||
div
|
||||
a(href=`/users/${update.user}`)= update.user
|
||||
strong= update.mood
|
||||
div= update.desc || "[no mood description provided]"
|
||||
div= update.date
|
||||
else
|
||||
span [no updates]
|
||||
include _feed.pug
|
||||
+feed(recentUpdates)
|
||||
script.
|
||||
function disable(form) {
|
||||
const btn = form.querySelector("button");
|
||||
|
|
|
@ -15,6 +15,12 @@ block page
|
|||
|
|
||||
a(href="/dashboard") the dashboard
|
||||
| !
|
||||
if session.loggedIn
|
||||
p
|
||||
| You can record your daily thoughts and feelings, public or private, at your
|
||||
|
|
||||
a(href="/journal/") personal journal
|
||||
| .
|
||||
p
|
||||
strong #{users}
|
||||
|
|
||||
|
@ -28,3 +34,7 @@ block page
|
|||
block content
|
||||
p
|
||||
| Hi, this is MiPilin! It lets you tell your friends how you're feeling, as well as keep a journal of your current mood and their trends over time! Due respect goes to imood, from which I borrowed many ideas and basically all of the moods.
|
||||
h1 Global Feed
|
||||
p Look at how all these people are doing!!!
|
||||
include _feed.pug
|
||||
+feed(feedUpdates)
|
||||
|
|
48
views/journal.pug
Normal file
|
@ -0,0 +1,48 @@
|
|||
extends site.pug
|
||||
|
||||
block head
|
||||
link(rel="stylesheet", href="/css/dashboard.css")
|
||||
script(type="module", src="https://cdn.jsdelivr.net/npm/chart.js@4.4.6/dist/chart.umd.min.js", nonce=nonce)
|
||||
|
||||
block content
|
||||
h1 Your Journal
|
||||
p This is where you can log your overall mood every day, and get a glimpse at how your life is going so far!
|
||||
form#journal-update(action="/update/journal", method="post")
|
||||
.input
|
||||
span Overall mood change (how do you feel compared to yesterday?)
|
||||
#ovm
|
||||
input.ovm-input(type="radio", name="moodDelta", id="moodDelta-mb", value="2", required)
|
||||
label.ovm-input(for="moodDelta-mb", title="Much better")
|
||||
img(src="/img/upup.svg", alt="Much better")
|
||||
|
||||
input.ovm-input(type="radio", name="moodDelta", id="moodDelta-b", value="1", required)
|
||||
label.ovm-input(for="moodDelta-b", title="Better")
|
||||
img(src="/img/up.svg", alt="Better")
|
||||
|
||||
input.ovm-input(type="radio", name="moodDelta", id="moodDelta-nc", value="0", required checked)
|
||||
label.ovm-input(for="moodDelta-nc", title="About the same")
|
||||
img(src="/img/line.svg", alt="About the same")
|
||||
|
||||
input.ovm-input(type="radio", name="moodDelta", id="moodDelta-w", value="-1", required)
|
||||
label.ovm-input(for="moodDelta-w", title="Worse")
|
||||
img(src="/img/down.svg", alt="Worse")
|
||||
|
||||
input.ovm-input(type="radio", name="moodDelta", id="moodDelta-mw", value="-2", required)
|
||||
label.ovm-input(for="moodDelta-mw", title="Much worse")
|
||||
img(src="/img/downdown.svg", alt="Much worse")
|
||||
.input
|
||||
label(for="description") Journal entry for today
|
||||
textarea(name="description", id="description", placeholder="max 4096 chars", maxlength="4096", cols="60", rows="12")
|
||||
.input
|
||||
span Visibility
|
||||
div#visibility-control
|
||||
input(type="radio", name="visibility", id="visibility-public", value="1", checked)
|
||||
label(for="visibility-public") Public
|
||||
br
|
||||
input(type="radio", name="visibility", id="visibility-private", value="0")
|
||||
label(for="visibility-private") Private
|
||||
br
|
||||
input(type="radio", name="visibility", id="visibility-moodChange-only", value="2")
|
||||
label(for="visibility-moodChange-only") Mood only
|
||||
|
||||
button(type="submit") Submit
|
|
@ -3,6 +3,7 @@ html(lang="en")
|
|||
head
|
||||
meta(charset="UTF-8")
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1.0")
|
||||
meta(name="description", content="Show your mood to the world!")
|
||||
link(rel="stylesheet", href="/css/main.css")
|
||||
block head
|
||||
title #{title} > mipilin
|
||||
|
@ -14,10 +15,9 @@ html(lang="en")
|
|||
nav
|
||||
if session.loggedIn
|
||||
a(href="/dashboard/") Dashboard
|
||||
a(href="/feed/") Feed
|
||||
a(href="/journal/") Journal
|
||||
a(href="/logout/") Log out
|
||||
else
|
||||
a(href="/feed/") Feed
|
||||
a(href="/register/") Sign up
|
||||
a(href="/login") Log in
|
||||
#ticker
|
||||
|
@ -41,6 +41,6 @@ html(lang="en")
|
|||
#flashes
|
||||
for [type, flashGroup] of Object.entries(flashes)
|
||||
for flash of flashGroup
|
||||
div(class=`flash-${type}`)= flash
|
||||
div(class=`flash-${type}`)!= flash
|
||||
block content
|
||||
|
||||
|
|
|
@ -27,4 +27,7 @@ block content
|
|||
div(style="margin-left:2ch;word-wrap:break-word;")= userMood.desc || "[no mood description]"
|
||||
else
|
||||
span User has not yet set a mood!
|
||||
|
||||
br
|
||||
h2 Recent updates
|
||||
include _feed.pug
|
||||
+feed(userMoodFeed, true)
|
||||
|
|