From fa8fae463881a958dc12f0e6b46f9fb3f1e87190 Mon Sep 17 00:00:00 2001 From: roxwize Date: Tue, 28 Jan 2025 17:05:54 -0500 Subject: [PATCH] mipilin r9... Fimally Signed-off-by: roxwize --- TODO.md | 2 + routes/admin.ts | 13 ++- routes/login.ts | 20 +++- routes/updates.ts | 37 +++++-- routes/users.ts | 10 +- static/css/main.css | 256 +++++++++++++++++++++++--------------------- views/admin.pug | 1 + views/dashboard.pug | 24 ++++- views/index.pug | 4 + 9 files changed, 217 insertions(+), 150 deletions(-) diff --git a/TODO.md b/TODO.md index 2615b4b..73ecee5 100644 --- a/TODO.md +++ b/TODO.md @@ -12,3 +12,5 @@ - [ ] Journal entry comments - [ ] A Forum - [ ] Make it easier to view journal entries +- [x] Users can fuck with invite codes by using an invite code and then making an account with it and then having more invite codes SO MAKE IT The case that only users with the trusted status may Do that +- [ ] Private journals are stored in plaintext and can be found if a database breach happens, maybe (optionally?) encrypt them (with PGP keys?) diff --git a/routes/admin.ts b/routes/admin.ts index 11688c0..d7b6162 100644 --- a/routes/admin.ts +++ b/routes/admin.ts @@ -68,6 +68,11 @@ export default function (app: Express, db: NodePgDatabase) { return; } if (!(req.session["status"] & UserStatus.MODERATOR)) { + if (!(req.session["status"] & UserStatus.TRUSTED)) { + req.flash("error", "Only trusted users can perform this action."); + res.redirect(req.get("Referrer") || "/"); + return; + } const { codesUsed } = ( await db .select({ codesUsed: count() }) @@ -87,7 +92,7 @@ export default function (app: Express, db: NodePgDatabase) { "error", "You've generated the maximum of five codes this week. Your counter will reset next month." ); - res.redirect("/dashboard"); + res.redirect(req.get("Referrer") || "/"); return; } @@ -100,19 +105,19 @@ export default function (app: Express, db: NodePgDatabase) { "success", `Your code has been created as ${code}. It expires in a week so use it ASAP!!!` ); - res.redirect("/dashboard"); + res.redirect(req.get("Referrer") || "/"); return; } const expiration = new Date(req.body.expiration || 0); if (req.body.expiration && expiration.getTime() <= Date.now()) { req.flash("error", "Chosen expiration date is in the past."); - res.redirect("/mod"); + res.redirect(req.get("Referrer") || "/"); return; } const code = await createInviteCode(db, req.session["uid"], expiration); req.flash("success", `Your code has been created as ${code}.`); - res.redirect("/mod"); + res.redirect(req.get("Referrer") || "/"); }); } diff --git a/routes/login.ts b/routes/login.ts index 22a98e7..11bb5a9 100644 --- a/routes/login.ts +++ b/routes/login.ts @@ -2,7 +2,7 @@ import { Express } from "express"; import bcrypt from "bcrypt"; import { render } from "./util.js"; import { NodePgDatabase } from "drizzle-orm/node-postgres"; -import { inviteCodes, profiles, users } from "../db/schema.js"; +import { follows, inviteCodes, profiles, users } from "../db/schema.js"; import { eq } from "drizzle-orm"; //! TEMP Also not sanitized like at all @@ -21,6 +21,11 @@ export default function(app: Express, db: NodePgDatabase) { return; } // validation + if (!req.body.name || !req.body.referral || !req.body.email || !req.body.pass) { + req.flash("error", "A required field wasn't filled in."); + res.redirect("/register"); + return; + } if (req.body.referral.length < 22) { req.flash("error", "Invalid invite code! Make sure you pasted it in correctly WITH the hyphens."); res.redirect("/register"); @@ -36,7 +41,11 @@ export default function(app: Express, db: NodePgDatabase) { res.redirect("/register"); return; } - if (!req.body.name.match(/[A-Z0-9_-]/i)) { + + //! dumb + req.body.name = req.body.name.trim(); + const match = req.body.name.match(/[A-Z0-9_-]+/i); + if (match?.[0] !== req.body.name) { req.flash( "error", "Username can only contain letters, numbers, underscores, hyphens, and periods!!" @@ -101,6 +110,13 @@ export default function(app: Express, db: NodePgDatabase) { )[0]; await db.insert(profiles).values({ user: uid }); + // Follow me by default ;w;;; + //! Also this assumes that im at id 1 which might not be true ever + await db.insert(follows).values({ + userId: 1, + followerId: uid + }); + req.session["loggedIn"] = true; req.session["status"] = code.confers; req.session["user"] = req.body.name; diff --git a/routes/updates.ts b/routes/updates.ts index c27a289..0a2be3c 100644 --- a/routes/updates.ts +++ b/routes/updates.ts @@ -112,6 +112,12 @@ export default async function (app: Express, db: NodePgDatabase) { ) )[0]; + const followed = await db + .select({ uname: users.name }) + .from(follows) + .where(eq(follows.followerId, req.session["uid"])) + .innerJoin(users, eq(follows.userId, users.id)); + render(db, "dashboard", "dashboard", res, req, { user, moods, @@ -120,6 +126,8 @@ export default async function (app: Express, db: NodePgDatabase) { recentUpdates, codes, codesUsed, + followed, + isTrusted: req.session["status"] & (UserStatus.MODERATOR | UserStatus.TRUSTED), feed: [] }); }); @@ -128,6 +136,15 @@ export default async function (app: Express, db: NodePgDatabase) { res.redirect("/login"); return; } + // make sure the user isnt updating too fast + //! TODO: also do this for journal entries + const lastUpdate = (await db.select({ date: updates.date }).from(updates).where(eq(updates.user, req.session["uid"])).orderBy(desc(updates.date)).limit(1))?.[0]; + if (Date.now() < lastUpdate?.date?.getTime() + 10 * 1000) { + req.flash("error", "You're updating your mood too fast! Wait ten seconds between updates."); + res.redirect(req.get("Referrer") || "/"); + return; + } + const moodIndex = moods.indexOf(req.body.mood.trim()); if (moodIndex === -1) { req.flash( @@ -205,7 +222,8 @@ export default async function (app: Express, db: NodePgDatabase) { if ( !entry || (entry.visibility === 0 && - entry.uname !== req.session["user"] && !isMod) + entry.uname !== req.session["user"] && + !isMod) ) { render404(db, res, req); return; @@ -247,16 +265,19 @@ export default async function (app: Express, db: NodePgDatabase) { )[0]; const isMod = req.session["status"] & UserStatus.MODERATOR; - if ( - !entry || - (entry?.uid !== req.session["uid"] && - !isMod) - ) { + 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."); + 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; } diff --git a/routes/users.ts b/routes/users.ts index 9e5cfdc..977543a 100644 --- a/routes/users.ts +++ b/routes/users.ts @@ -149,7 +149,7 @@ export default async function (app: Express, db: NodePgDatabase) { (uname || "") !== req.session["user"] && !(req.session["status"] & UserStatus.MODERATOR) ) { - res.redirect("back"); + res.redirect(req.get("Referrer") || "/"); return; } @@ -182,11 +182,7 @@ export default async function (app: Express, db: NodePgDatabase) { .where(eq(users.name, req.params.user)) )[0]; if (!uid) { - req.flash( - "error", - "It looks like you're trying to follow a user that doesn't exist anymore." - ); - res.redirect("/"); + render404(db, res, req); return; } const isFollowing = !!( @@ -217,6 +213,6 @@ export default async function (app: Express, db: NodePgDatabase) { followerId: req.session["uid"] }); } - res.redirect(`/users/${req.params.user}`); + res.redirect(req.get("Referrer") || "/"); }); } diff --git a/static/css/main.css b/static/css/main.css index 4dcf33a..9e08b42 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,231 +1,239 @@ * { - box-sizing: border-box; + box-sizing: border-box; } body { - display: flex; - align-items: center; - justify-content: center; - margin: 0; - background-color: #21211d; - background-image: url("/img/bg.png"); - background-size: cover; - width: 100vw; - height: 100vh; - font-family: "Gohufont 14", sans-serif; - font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + background-color: #21211d; + background-image: url("/img/bg.png"); + background-size: cover; + width: 100vw; + height: 100vh; + font-family: "Gohufont 14", sans-serif; + font-size: 14px; } main { - background-color: #faf8da; - width: 95%; - max-width: 953px; - height: 95%; + background-color: #faf8da; + width: 95%; + max-width: 953px; + height: 95%; } a { - color: #4630df; + color: #4630df; } h1, h2, h3 { - margin: 0; + margin: 0; } header { - display: flex; - flex-direction: row; - justify-content: space-between; - background-color: #e0d665; - background-image: linear-gradient(to bottom, #f6ef98, #c7c07d); - padding: 16px; - width: 100%; - overflow: hidden; + display: flex; + flex-direction: row; + justify-content: space-between; + background-color: #e0d665; + background-image: linear-gradient(to bottom, #f6ef98, #c7c07d); + padding: 16px; + width: 100%; + overflow: hidden; } #header-logo { - filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5)); - height: 40px; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5)); + height: 40px; } header nav { - display: flex; - align-items: center; - gap: 2em; + display: flex; + align-items: center; + gap: 2em; } header nav a { - text-decoration: none; - text-shadow: 0 0px 4px rgba(0, 0, 0, 0.8); - color: #faf8da; - font-size: 24px; - font-weight: bold; + text-decoration: none; + text-shadow: 0 0px 4px rgba(0, 0, 0, 0.8); + color: #faf8da; + font-size: 24px; + font-weight: bold; } header nav a:visited { - color: #faf8da; + color: #faf8da; } header nav a:hover { - text-decoration: underline; + text-decoration: underline; } summary { - cursor: pointer; + cursor: pointer; } #ticker { - position: relative; - border: 1px solid #25251d; - background-color: #454542; - height: 1.5em; - color: #faf8da; + position: relative; + border: 1px solid #25251d; + background-color: #454542; + height: 1.5em; + color: #faf8da; } #ticker > div { - display: flex; - float: left; - align-items: center; - justify-content: center; - border-right: 1px solid #25251d; - background-color: #3b3b33; - padding: 0 4px; - min-width: 255px; - height: 100%; - text-align: center; + display: flex; + float: left; + align-items: center; + justify-content: center; + border-right: 1px solid #25251d; + background-color: #3b3b33; + padding: 0 4px; + min-width: 255px; + height: 100%; + text-align: center; } #ticker div a { - color: #faf8da; + color: #faf8da; } #ticker #ticker-marquee { - width: auto; - height: 100%; - display: flex; - align-items: center; + width: auto; + height: 100%; + display: flex; + align-items: center; } #page { - position: relative; - height: calc(100% - 1.5em - 76px); - overflow-y: auto; + position: relative; + height: calc(100% - 1.5em - 76px); + overflow-y: auto; } #sidebar { - position: sticky; - top: 0; - float: left; - border-right: 1px solid #afa870; - padding: 1em; - width: 256px; - height: 100%; + position: sticky; + top: 0; + float: left; + border-right: 1px solid #afa870; + padding: 1em; + width: 256px; + height: 100%; } #content { - padding: 1em; + padding: 1em; } #content > *:nth-child(2) { - margin-top: 0; - margin-inline-start: 0; + margin-top: 0; + margin-inline-start: 0; } #content > *:last-child { - margin-bottom: 0; - margin-inline-end: 0; + margin-bottom: 0; + margin-inline-end: 0; } #flashes div { - margin-bottom: 8px; - padding: 8px; - border-width: 1px; - border-style: solid; + margin-bottom: 8px; + padding: 8px; + border-width: 1px; + border-style: solid; } #flashes div:first-child { - margin-top: 0; + margin-top: 0; } .flash-error { - background-color: #e06583; - border-color: #911533; + background-color: #e06583; + border-color: #911533; } .flash-info { - background-color: #8ca3f2; - border-color: #6371a1; + background-color: #8ca3f2; + border-color: #6371a1; } .flash-success { - background-color: #8cf2a6; - border-color: #3da758; + background-color: #8cf2a6; + border-color: #3da758; } .error { - color: #c51e48; + color: #c51e48; } .subtle { - opacity: 0.5; + opacity: 0.5; } button, textarea, select, input { - border: 1px solid #757462; - background-color: white; - padding: 4px; - font-family: inherit; - font-size: 14px; + border: 1px solid #757462; + background-color: white; + padding: 4px; + font-family: inherit; + font-size: 14px; } button:focus, textarea:focus, select:focus, input:focus { - outline: 0; - border: 1px solid #000000; + outline: 0; + border: 1px solid #000000; } button, select { - cursor: pointer; + cursor: pointer; } table button { - padding: 0 4px; + padding: 0 4px; } -th, td { - text-align: left; - padding-right: 1em; +th, +td { + text-align: left; + padding-right: 1em; +} +tr:nth-child(odd) td { + background-color: rgba(0, 0, 0, 0.1); +} +tr:hover td { + background-color: rgba(0, 0, 0, 0.2); + cursor: default; } .feed-update { - background-color: #d5d4bb; - height: fit-content; - word-wrap: break-word; + background-color: #d5d4bb; + height: fit-content; + word-wrap: break-word; } .feed-update div:first-child { - display: flex; - justify-content: space-between; + display: flex; + justify-content: space-between; } .feed-update div:last-child { - text-align: right; - color: #3f3f38; - font-style: italic; + 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; + 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; - } + #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; - } + .feed-update { + border-bottom: 1px solid #afa870; + padding: 8px; + } } diff --git a/views/admin.pug b/views/admin.pug index 485e869..cf2121b 100644 --- a/views/admin.pug +++ b/views/admin.pug @@ -18,6 +18,7 @@ block content a(href=`/users/${user.uname}`)= user.uname td= user.status.toString(2).padStart(4, "0") h2 Invite codes + p Well Rae. You've made quite a The Closed Beta. form(action="/codes/delete", method="post") table tbody diff --git a/views/dashboard.pug b/views/dashboard.pug index b25a838..8bc8911 100644 --- a/views/dashboard.pug +++ b/views/dashboard.pug @@ -15,11 +15,23 @@ block page button(type="submit") Edit h1(style="margin-top:1em;") Mood history ul#dashboard-mood-history + if moodHistory.length < 1 + span [no updates] for mood of moodHistory li strong= mood.mood | | #{mood.date} + h1(style="margin-top:1em;") Follows + table + tbody + for follow of followed + tr + td + a(href=`/users/${follow.uname}`)= follow.uname + td + form(action=`/users/${follow.uname}/follow`, method="post") + button Unfollow block content p @@ -42,7 +54,7 @@ block content +feed(recentUpdates) h1#invite-codes(style="margin-top:1em;") Invite codes - p Invite your friends to the mipilin beta! You can create up to five invite codes every month, and they all expire within a week. + p If you're a trusted user, you can invite your friends to the mipilin beta! You can create up to five invite codes every month, and they all expire within a week. p | Your current invite codes ( strong= codesUsed @@ -61,13 +73,15 @@ block content .subtle You have no currently active invite codes. br form(action="/codes/create", method="post") - button(type="submit", disabled=codesUsed>=5) Generate + button(type="submit", disabled=codesUsed>=5 || !isTrusted) Generate + if codesUsed >= 5 + p You've generated the maximum amount of codes this month. + if !isTrusted + p You need be manually marked as "trusted" to generate codes. script(nonce=nonce). function disable(form) { - form.preventDefault(); const btn = form.querySelector("button"); - btn.preventDefault(); - btn.setAttribute("disabled", true); + btn.setAttribute("disabled", ""); setTimeout(() => { btn.removeAttribute("disabled"); diff --git a/views/index.pug b/views/index.pug index 39a2405..296f700 100644 --- a/views/index.pug +++ b/views/index.pug @@ -36,6 +36,10 @@ block content | Hi, this is mipilin (pronounced /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 a(href="https://www.imood.com/") imood |, from which I borrowed many ideas and basically all of the moods. + p + | If you're wondering how to get started, look down. There is a lot of people! Also on the sidebar to your left is important information, including another list of users. Click on them! Follow them! Interact with them! Following someone will make their updates appear on your + a(href="/dashboard/#feed") dashboard's local feed + | . No one likes an empty local feed!! p | mipilin is free and open source forevur. If you want to CHECK OUT TEH CODE or FILE AN ISSUE then you may do so at the respective links provided on the a(href="https://sr.ht/~roxwize/mipilin") project page