mirror of
https://git.sr.ht/~roxwize/mipilin
synced 2025-01-30 18:53:36 +00:00
mipilin r9... Fimally
Signed-off-by: roxwize <rae@roxwize.xyz>
This commit is contained in:
parent
961d963677
commit
fa8fae4638
9 changed files with 217 additions and 150 deletions
2
TODO.md
2
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?)
|
||||
|
|
|
@ -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 <b>${code}</b>. 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 <b>${code}</b>.`);
|
||||
res.redirect("/mod");
|
||||
res.redirect(req.get("Referrer") || "/");
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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") || "/");
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue