From b3c9cba6478dc16d135313aa6d0adcc02d67ece6 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Sun, 25 Feb 2024 21:34:33 +0000 Subject: feat: optional public events/groups --- config/config.example.toml | 3 ++ public/css/style.css | 39 ++++++++++++++++- public/js/modules/event-edit.js | 2 + public/js/modules/group-edit.js | 5 +++ public/js/modules/new.js | 7 ++++ src/lib/config.ts | 66 +++++++++++++++++++++++++++-- src/models/Event.ts | 5 +++ src/models/EventGroup.ts | 5 +++ src/routes/event.ts | 2 + src/routes/frontend.ts | 71 ++++++++++++++++++++++++++++++- src/routes/group.ts | 2 + src/util/validation.ts | 31 ++++++++++++-- views/event.handlebars | 3 +- views/eventgroup.handlebars | 1 + views/home.handlebars | 44 +++++++++++++++---- views/layouts/main.handlebars | 2 +- views/partials/eventForm.handlebars | 8 ++++ views/partials/eventGroupForm.handlebars | 11 +++++ views/partials/sidebar.handlebars | 12 ++++-- views/publicEventList.handlebars | 72 ++++++++++++++++++++++++++++++++ 20 files changed, 365 insertions(+), 26 deletions(-) create mode 100644 views/publicEventList.handlebars diff --git a/config/config.example.toml b/config/config.example.toml index e9995de..4e00171 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -16,6 +16,9 @@ email_logo_url = "" # Show a Ko-Fi box to donate money to Raphael (Gathio's creator) on the front # page. show_kofi = false +# Show a list of events and groups on the front page which have been marked as +# 'Display this event/group on the public event/group list'. +show_public_event_list = false # Which mail service to use to send emails to hosts and attendees. Options are # 'nodemailer' or 'sendgrid'. Configure settings for this mail # service below. diff --git a/public/css/style.css b/public/css/style.css index dd59d6b..24d10b8 100755 --- a/public/css/style.css +++ b/public/css/style.css @@ -381,7 +381,7 @@ li.hidden-attendee .attendee-name { } } -@media (min-width: 577px) { +@media (min-width: 576px) { #sidebar { border-right: 2px solid #e0e0e0; min-height: 100vh; @@ -422,7 +422,7 @@ li.hidden-attendee .attendee-name { } .list-group-item-action:hover { - background-color: #d4edda; + background-color: #f2f8ff; } .code { @@ -548,3 +548,38 @@ img.group-preview__image { opacity: 1; pointer-events: auto; } + +ul#sidebar__nav { + padding: 0; + margin: 0; + list-style: none; + display: flex; + flex-direction: row; + gap: 0.5rem; + align-items: center; + justify-content: center; + margin-bottom: 1rem; +} + +ul#sidebar__nav li { + padding: 0 1rem 0.5rem 1rem; + text-align: center; +} + +ul#sidebar__nav a { + display: block; + width: 100%; +} + +@media (min-width: 576px) { + ul#sidebar__nav { + flex-direction: column; + } + ul#sidebar__nav li { + width: 100%; + padding: 0 0 0.5rem 0; + } + ul#sidebar__nav li:has(a:not(.btn)):not(:last-child) { + border-bottom: 1px solid #e0e0e0; + } +} diff --git a/public/js/modules/event-edit.js b/public/js/modules/event-edit.js index 740c861..736547f 100644 --- a/public/js/modules/event-edit.js +++ b/public/js/modules/event-edit.js @@ -32,6 +32,7 @@ function editEventForm() { creatorEmail: window.eventData.creatorEmail, eventGroupID: window.eventData.eventGroupID, eventGroupEditToken: window.eventData.eventGroupEditToken, + publicCheckbox: window.eventData.showOnPublicList, interactionCheckbox: window.eventData.usersCanComment, joinCheckbox: window.eventData.usersCanAttend, maxAttendeesCheckbox: window.eventData.maxAttendees !== null, @@ -53,6 +54,7 @@ function editEventForm() { this.data.joinCheckbox = window.eventData.usersCanAttend; this.data.maxAttendeesCheckbox = window.eventData.maxAttendees !== null; + this.data.publicCheckbox = window.eventData.showOnPublicList; }, async submitForm() { this.submitting = true; diff --git a/public/js/modules/group-edit.js b/public/js/modules/group-edit.js index 1a2c1db..2d55346 100644 --- a/public/js/modules/group-edit.js +++ b/public/js/modules/group-edit.js @@ -27,6 +27,11 @@ function editEventGroupForm() { eventGroupURL: window.groupData.url, hostName: window.groupData.hostName, creatorEmail: window.groupData.creatorEmail, + publicCheckbox: window.groupData.showOnPublicList, + }, + init() { + // Set checkboxes + this.data.publicCheckbox = window.groupData.showOnPublicList; }, errors: [], submitting: false, diff --git a/public/js/modules/new.js b/public/js/modules/new.js index a018087..f7c3e34 100644 --- a/public/js/modules/new.js +++ b/public/js/modules/new.js @@ -87,6 +87,7 @@ function newEventForm() { creatorEmail: "", eventGroupID: "", eventGroupEditToken: "", + publicCheckbox: false, interactionCheckbox: false, joinCheckbox: false, maxAttendeesCheckbox: false, @@ -107,6 +108,7 @@ function newEventForm() { this.data.interactionCheckbox = false; this.data.joinCheckbox = false; this.data.maxAttendeesCheckbox = false; + this.data.publicCheckbox = false; }, async submitForm() { this.submitting = true; @@ -160,6 +162,11 @@ function newEventGroupForm() { eventGroupURL: "", hostName: "", creatorEmail: "", + publicCheckbox: false, + }, + init() { + // Reset checkboxes + this.data.publicCheckbox = false; }, errors: [], submitting: false, diff --git a/src/lib/config.ts b/src/lib/config.ts index 1029be9..b4385ca 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -14,10 +14,11 @@ interface GathioConfig { port: string; email: string; site_name: string; - delete_after_days: number | null; + delete_after_days: number; is_federated: boolean; email_logo_url: string; show_kofi: boolean; + show_public_event_list: boolean; mail_service: "nodemailer" | "sendgrid"; creator_email_addresses: string[]; }; @@ -42,6 +43,7 @@ interface FrontendConfig { isFederated: boolean; emailLogoUrl: string; showKofi: boolean; + showPublicEventList: boolean; showInstanceInformation: boolean; staticPages?: StaticPage[]; version: string; @@ -56,8 +58,10 @@ const defaultConfig: GathioConfig = { is_federated: true, delete_after_days: 7, email_logo_url: "", + show_public_event_list: false, show_kofi: false, mail_service: "nodemailer", + creator_email_addresses: [], }, database: { mongodb_url: "mongodb://localhost:27017/gathio", @@ -69,15 +73,71 @@ export const frontendConfig = (): FrontendConfig => { return { domain: config.general.domain, siteName: config.general.site_name, - isFederated: config.general.is_federated, + isFederated: !!config.general.is_federated, emailLogoUrl: config.general.email_logo_url, - showKofi: config.general.show_kofi, + showPublicEventList: !!config.general.show_public_event_list, + showKofi: !!config.general.show_kofi, showInstanceInformation: !!config.static_pages?.length, staticPages: config.static_pages, version: process.env.npm_package_version || "unknown", }; }; +interface InstanceRule { + icon: string; + text: string; +} + +export const instanceRules = (): InstanceRule[] => { + const config = getConfig(); + const rules = []; + rules.push( + config.general.show_public_event_list + ? { + text: "Public events and groups are displayed on the homepage", + icon: "fas fa-eye", + } + : { + text: "Events and groups can only be accessed by direct link", + icon: "fas fa-eye-slash", + }, + ); + rules.push( + config.general.creator_email_addresses?.length + ? { + text: "Only specific people can create events and groups", + icon: "fas fa-user-check", + } + : { + text: "Anyone can create events and groups", + icon: "fas fa-users", + }, + ); + rules.push( + config.general.delete_after_days > 0 + ? { + text: `Events are automatically deleted ${config.general.delete_after_days} days after they end`, + icon: "far fa-calendar-times", + } + : { + text: "Events are permanent, and are never automatically deleted", + icon: "far fa-calendar-check", + }, + ); + rules.push( + config.general.is_federated + ? { + text: "This instance federates with other instances using ActivityPub", + icon: "fas fa-globe", + } + : { + text: "This instance does not federate with other instances", + icon: "fas fa-globe", + }, + ); + return rules; +}; + // Attempt to load our global config. Will stop the app if the config file // cannot be read (there's no point trying to continue!) export const getConfig = (): GathioConfig => { diff --git a/src/models/Event.ts b/src/models/Event.ts index f67d40b..5731680 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -73,6 +73,7 @@ export interface IEvent extends mongoose.Document { privateKey?: string; followers?: IFollower[]; activityPubMessages?: IActivityPubMessage[]; + showOnPublicList?: boolean; } const Attendees = new mongoose.Schema({ @@ -334,6 +335,10 @@ const EventSchema = new mongoose.Schema({ }, followers: [Followers], activityPubMessages: [ActivityPubMessages], + showOnPublicList: { + type: Boolean, + default: false, + }, }); export default mongoose.model("Event", EventSchema); diff --git a/src/models/EventGroup.ts b/src/models/EventGroup.ts index 2b5c2aa..de7187d 100755 --- a/src/models/EventGroup.ts +++ b/src/models/EventGroup.ts @@ -16,6 +16,7 @@ export interface IEventGroup extends mongoose.Document { firstLoad?: boolean; events?: mongoose.Types.ObjectId[]; subscribers?: ISubscriber[]; + showOnPublicList?: boolean; } const Subscriber = new mongoose.Schema({ @@ -70,6 +71,10 @@ const EventGroupSchema = new mongoose.Schema({ }, events: [{ type: mongoose.Schema.Types.ObjectId, ref: "Event" }], subscribers: [Subscriber], + showOnPublicList: { + type: Boolean, + default: false, + }, }); export default mongoose.model("EventGroup", EventGroupSchema); diff --git a/src/routes/event.ts b/src/routes/event.ts index fb9d8c7..6be5ff8 100644 --- a/src/routes/event.ts +++ b/src/routes/event.ts @@ -140,6 +140,7 @@ router.post( viewPassword: "", // Backwards compatibility editPassword: "", // Backwards compatibility editToken: editToken, + showOnPublicList: eventData?.publicBoolean, eventGroup: isPartOfEventGroup ? eventGroup?._id : null, usersCanAttend: eventData.joinBoolean ? true : false, showUsersList: false, // Backwards compatibility @@ -371,6 +372,7 @@ router.put( url: eventData.eventURL, hostName: eventData.hostName, image: eventImageFilename, + showOnPublicList: eventData.publicBoolean, usersCanAttend: eventData.joinBoolean, showUsersList: false, // Backwards compatibility usersCanComment: eventData.interactionBoolean, diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts index 0d8793a..4cdce8a 100644 --- a/src/routes/frontend.ts +++ b/src/routes/frontend.ts @@ -2,7 +2,7 @@ import { Router, Request, Response } from "express"; import moment from "moment-timezone"; import { marked } from "marked"; import { markdownToSanitizedHTML, renderPlain } from "../util/markdown.js"; -import getConfig, { frontendConfig } from "../lib/config.js"; +import getConfig, { frontendConfig, instanceRules } from "../lib/config.js"; import { addToLog, exportICal } from "../helpers.js"; import Event from "../models/Event.js"; import EventGroup, { IEventGroup } from "../models/EventGroup.js"; @@ -16,7 +16,20 @@ const config = getConfig(); const router = Router(); router.get("/", (_: Request, res: Response) => { - res.render("home", frontendConfig()); + if (config.general.show_public_event_list) { + return res.redirect("/events"); + } + return res.render("home", { + ...frontendConfig(), + instanceRules: instanceRules(), + }); +}); + +router.get("/about", (_: Request, res: Response) => { + return res.render("home", { + ...frontendConfig(), + instanceRules: instanceRules(), + }); }); router.get("/new", (_: Request, res: Response) => { @@ -57,6 +70,51 @@ router.get("/new/:magicLinkToken", async (req: Request, res: Response) => { }); }); +router.get("/events", async (_: Request, res: Response) => { + if (!config.general.show_public_event_list) { + return res.status(404).render("404", frontendConfig()); + } + const events = await Event.find({ showOnPublicList: true }) + .populate("eventGroup") + .lean() + .sort("start"); + const updatedEvents = events.map((event) => { + const startMoment = moment.tz(event.start, event.timezone); + const endMoment = moment.tz(event.end, event.timezone); + const isSameDay = startMoment.isSame(endMoment, "day"); + + return { + id: event.id, + name: event.name, + location: event.location, + displayDate: isSameDay + ? startMoment.format("D MMM YYYY") + : `${startMoment.format("D MMM YYYY")} - ${endMoment.format( + "D MMM YYYY", + )}`, + eventHasConcluded: endMoment.isBefore(moment.tz(event.timezone)), + eventGroup: event.eventGroup, + }; + }); + const upcomingEvents = updatedEvents.filter( + (event) => event.eventHasConcluded === false, + ); + const pastEvents = updatedEvents.filter( + (event) => event.eventHasConcluded === true, + ); + const eventGroups = await EventGroup.find({ + showOnPublicList: true, + }).lean(); + + res.render("publicEventList", { + title: "Public events", + upcomingEvents: upcomingEvents, + pastEvents: pastEvents, + eventGroups: eventGroups, + ...frontendConfig(), + }); +}); + router.get("/:eventID", async (req: Request, res: Response) => { try { const event = await Event.findOne({ @@ -266,6 +324,11 @@ router.get("/:eventID", async (req: Request, res: Response) => { firstLoad: firstLoad, eventHasConcluded: eventHasConcluded, eventHasBegun: eventHasBegun, + eventWillBeDeleted: config.general.delete_after_days > 0, + daysUntilDeletion: moment + .tz(event.end, event.timezone) + .add(config.general.delete_after_days, "days") + .fromNow(), metadata: metadata, jsonData: { name: event.name, @@ -276,6 +339,7 @@ router.get("/:eventID", async (req: Request, res: Response) => { url: event.url, hostName: event.hostName, creatorEmail: event.creatorEmail, + showOnPublicList: event.showOnPublicList, eventGroupID: event.eventGroup ? (event.eventGroup as unknown as IEventGroup).id : null, @@ -337,6 +401,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { return { id: event.id, name: event.name, + location: event.location, displayDate: isSameDay ? startMoment.format("D MMM YYYY") : `${startMoment.format("D MMM YYYY")} - ${endMoment.format( @@ -388,6 +453,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { res.set("X-Robots-Tag", "noindex"); res.render("eventgroup", { + ...frontendConfig(), domain: config.general.domain, title: eventGroup.name, eventGroupData: eventGroup, @@ -409,6 +475,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { creatorEmail: eventGroup.creatorEmail, image: eventGroup.image, editToken: editingEnabled ? eventGroupEditToken : null, + showOnPublicList: eventGroup.showOnPublicList, }, }); } catch (err) { diff --git a/src/routes/group.ts b/src/routes/group.ts index 34377b0..c006a5d 100644 --- a/src/routes/group.ts +++ b/src/routes/group.ts @@ -81,6 +81,7 @@ router.post( hostName: groupData.hostName, editToken: editToken, firstLoad: true, + showOnPublicList: groupData.publicBoolean, }); await eventGroup.save(); @@ -206,6 +207,7 @@ router.put( url: req.body.eventGroupURL, hostName: req.body.hostName, image: eventGroupImageFilename, + showOnPublicList: groupData.publicBoolean, }; await EventGroup.findOneAndUpdate( diff --git a/src/util/validation.ts b/src/util/validation.ts index 732fbf3..b9a0c8a 100644 --- a/src/util/validation.ts +++ b/src/util/validation.ts @@ -5,11 +5,16 @@ type Error = { field?: string; }; -type ValidationResponse = { +type EventValidationResponse = { data?: ValidatedEventData; errors?: Error[]; }; +type EventGroupValidationResponse = { + data?: ValidatedEventGroupData; + errors?: Error[]; +}; + interface EventData { eventName: string; eventLocation: string; @@ -21,6 +26,7 @@ interface EventData { imagePath: string; hostName: string; creatorEmail: string; + publicCheckbox: string; eventGroupCheckbox: string; eventGroupID: string; eventGroupEditToken: string; @@ -33,11 +39,13 @@ interface EventData { // EventData without the 'checkbox' fields export type ValidatedEventData = Omit< EventData, + | "publicCheckbox" | "eventGroupCheckbox" | "interactionCheckbox" | "joinCheckbox" | "maxAttendeesCheckbox" > & { + publicBoolean: boolean; eventGroupBoolean: boolean; interactionBoolean: boolean; joinBoolean: boolean; @@ -50,8 +58,13 @@ interface EventGroupData { eventGroupURL: string; hostName: string; creatorEmail: string; + publicCheckbox: string; } +export type ValidatedEventGroupData = Omit & { + publicBoolean: boolean; +}; + const validateEmail = (email: string) => { if (!email || email.length === 0 || typeof email !== "string") { return false; @@ -89,9 +102,12 @@ export const validateEventTime = (start: Date, end: Date): Error | boolean => { return true; }; -export const validateEventData = (eventData: EventData): ValidationResponse => { +export const validateEventData = ( + eventData: EventData, +): EventValidationResponse => { const validatedData: ValidatedEventData = { ...eventData, + publicBoolean: eventData.publicCheckbox === "true", eventGroupBoolean: eventData.eventGroupCheckbox === "true", interactionBoolean: eventData.interactionCheckbox === "true", joinBoolean: eventData.joinCheckbox === "true", @@ -186,7 +202,9 @@ export const validateEventData = (eventData: EventData): ValidationResponse => { }; }; -export const validateGroupData = (groupData: EventGroupData) => { +export const validateGroupData = ( + groupData: EventGroupData, +): EventGroupValidationResponse => { const errors: Error[] = []; if (!groupData.eventGroupName) { errors.push({ @@ -209,8 +227,13 @@ export const validateGroupData = (groupData: EventGroupData) => { } } + const validatedData: ValidatedEventGroupData = { + ...groupData, + publicBoolean: groupData.publicCheckbox === "true", + }; + return { - data: groupData, + data: validatedData, errors: errors, }; }; diff --git a/views/event.handlebars b/views/event.handlebars index 4402578..44c2f4b 100755 --- a/views/event.handlebars +++ b/views/event.handlebars @@ -109,7 +109,7 @@ {{#if eventHasConcluded}} {{/if}} {{#if firstLoad}} @@ -523,7 +523,6 @@ window.eventData = {{{ json jsonData }}}; $(this).html(' Copied!'); setTimeout(function(){ $("#copyAPLink").html(' Copy');}, 5000); }) - $(".daysToDeletion").html(moment("{{eventEndISO}}").add(7, 'days').fromNow()); if ($("#joinCheckbox").is(':checked')){ $("#maxAttendeesCheckboxContainer").css("display","flex"); } diff --git a/views/eventgroup.handlebars b/views/eventgroup.handlebars index 434f691..7ad3570 100755 --- a/views/eventgroup.handlebars +++ b/views/eventgroup.handlebars @@ -122,6 +122,7 @@ {{this.name}} + {{#if this.location}} {{this.location}}{{/if}} {{this.displayDate}} {{/unless}} diff --git a/views/home.handlebars b/views/home.handlebars index add7eac..bf92724 100755 --- a/views/home.handlebars +++ b/views/home.handlebars @@ -1,25 +1,53 @@ -

A quick and easy way to make and share events which respects your privacy.

+

Gathio is a simple, federated, privacy-first event hosting platform.

-
+
+
+ This instance, {{siteName}}, has the following features: +
-

You don't need to sign up for an account. When you create an event, we generate a password which allows you to edit the event. Send all your guests the public link, and all your co-hosts the secret editing link containing the password. A week after the event finishes, it's deleted from our servers for ever, and your email goes with it.

+
+
    + {{#each instanceRules}} +
  • {{this.text}}
  • + {{/each}} +
+
+
An example event page for a picnic. The page shows the event's location, host, date and time, and description, as well as buttons to save the event to Google Calendar, export it, and open the location in OpenStreetMap and Google Maps.
-

Also, we don't show you ads, don't sell your data, and never send you unnecessary emails.

+

Privacy-first

+ +

There are no accounts on Gathio. When you create an event, we generate a password which allows you to edit the event. Send all your guests the public link, and all your co-hosts the secret editing link containing the password.

+ +

If you supply your email, we'll send you the editing password so you don't lose it - but supplying your email is optional!

+ +

If this instance automatically deletes its events, sometime after the event finishes, it's deleted from the database for ever, and your data goes with it.

+ +

Also, Gathio doesn't show you ads, doesn't sell your data, and never sends you unnecessary emails.

+ +

But remember: all events are visible to anyone who knows the link, so probably don't use Gathio to plot your surprise birthday party or revolution. Or whatever, you do you.

+ +

Configurable

+ +

The flagship Gathio instance at gath.io is designed for anyone to create ephemeral, hidden events. Anyone can create an event; events are never displayed anywhere public; and they're deleted 7 days after they end.

+ +

But if your community sets up their own instance, you can limit event creation to a specific list of people, display events on a handy list on the homepage, and disable event deletion entirely!

+ +

Federation and self-hosting

-

But remember: all events are visible to anyone who knows the link, so probably don't use gathio to plot your surprise birthday party or revolution. Or whatever, you do you.

+

Gathio can easily be self-hosted, and supports ActivityPub services like Mastodon, Pleroma, and Friendica, allowing you to access events from anywhere on the Fediverse. We encourage you to spin up your own instance for your community. Detailed instructions on ActivityPub access and self-hosted installation live on our GitHub wiki. -

Federation and self-hosting

+

Open source

-

gathio can easily be self-hosted, and supports ActivityPub services like Mastodon, Pleroma, and Friendica, allowing you to access events from anywhere on the Fediverse. We encourage you to spin up your own instance for your community. Detailed instructions on ActivityPub access and self-hosted installation live on our GitHub wiki. Leave a question in our tracker if you encounter any issues.

+

Gathio is delighted to be open source, and is built by a lovely group of people. Leave a question in our tracker if you encounter any issues.

{{#if showKofi}}
-

If you find yourself using and enjoying gathio, consider buying me a coffee. It'll help keep the site running!

+

If you find yourself using and enjoying Gathio, consider buying Raphael a coffee. It'll help keep the project and main site running!

diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars index d45d596..996d35f 100755 --- a/views/layouts/main.handlebars +++ b/views/layouts/main.handlebars @@ -70,7 +70,7 @@
{{#if showInstanceInformation}}

- {{domain}} + {{siteName}} {{#each staticPages}} {{#if @first}} · diff --git a/views/partials/eventForm.handlebars b/views/partials/eventForm.handlebars index c2eebc3..9227300 100755 --- a/views/partials/eventForm.handlebars +++ b/views/partials/eventForm.handlebars @@ -73,6 +73,14 @@

+ {{#if showPublicEventList}} +
+ + +
+ {{/if}}
Recommended dimensions (w x h): 920px by 300px.
+{{#if showPublicEventList}} +
+ +
+ + +
+
+{{/if}}
-

gathio

+

gathio

-

Nicer events

- - New +
diff --git a/views/publicEventList.handlebars b/views/publicEventList.handlebars new file mode 100644 index 0000000..6d85ca7 --- /dev/null +++ b/views/publicEventList.handlebars @@ -0,0 +1,72 @@ +
+

{{siteName}}

+

{{siteName}} is an instance of Gathio, a simple, federated, privacy-first event hosting platform.

+ + +
+
+
Upcoming events
+ +
+ +
+
Past events
+
+ {{#if pastEvents}} + {{#each pastEvents}} + + + {{this.name}} + {{this.displayDate}} + {{#if this.eventGroup}} + In group: {{this.eventGroup.name}} + {{/if}} + + {{/each}} + {{else}} +
No events!
+ {{/if}} +
+
+
+ +
+
+
Event groups
+
+ {{#if eventGroups}} + {{#each eventGroups}} + + + {{this.name}} + + {{/each}} + {{else}} +
No groups!
+ {{/if}} +
+ +
\ No newline at end of file -- cgit v1.2.3 From 1ff8eb315f964380599f411656dc6039d7961ee6 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Sun, 25 Feb 2024 23:43:21 +0000 Subject: version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 92c6f34..9605180 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gathio", - "version": "1.3.1", + "version": "1.4.0", "description": "", "main": "index.js", "type": "module", -- cgit v1.2.3 From 7741c9230df0b908a6db287144c321f911ec246d Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Sun, 25 Feb 2024 23:59:19 +0000 Subject: Add description --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9605180..c2e5a81 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gathio", "version": "1.4.0", - "description": "", + "description": "A simple, federated, privacy-first event hosting platform", "main": "index.js", "type": "module", "scripts": { -- cgit v1.2.3 From 363cfbae076f4494ddd5fddcf03de622f6247051 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Mon, 26 Feb 2024 12:06:18 +0000 Subject: fix: change wording and header tag --- views/createEventMagicLink.handlebars | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/createEventMagicLink.handlebars b/views/createEventMagicLink.handlebars index 563af82..ab00dc5 100644 --- a/views/createEventMagicLink.handlebars +++ b/views/createEventMagicLink.handlebars @@ -1,6 +1,6 @@
-

Request a link to create a new event

+

Request a link to create a new event

- The administrator of this instance has limited event creation to a set of specific email addresses. If your email address is allowed to create events, you will be sent a magic link to create an event. If not, you won't receive anything. + The administrator of this instance has limited event creation to a set of specific email addresses. If your email address is allowed to create events, you will be sent a magic link. If not, you won't receive anything.

If you run into any issues, please contact the instance administrator. -- cgit v1.2.3 From c93fd6e2d455ea4208f9e5ca6bfbd1c0e9fd1ad9 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Mon, 26 Feb 2024 12:09:46 +0000 Subject: refactor: allow Cypress to override config --- package.json | 2 ++ pnpm-lock.yaml | 36 +++++++++++++------ src/app.ts | 4 +++ src/index.d.ts | 14 ++++++++ src/lib/config.ts | 20 +++++++++-- src/lib/middleware.ts | 29 +++++++++++++--- src/routes.js | 2 +- src/routes/activitypub.ts | 59 +++++++++++++++++-------------- src/routes/event.ts | 42 +++++++++++----------- src/routes/frontend.ts | 88 +++++++++++++++++++++++++++++------------------ src/routes/group.ts | 13 ++++--- src/routes/magicLink.ts | 20 ++++++----- src/routes/static.ts | 6 ++-- src/util/object.ts | 30 ++++++++++++++++ 14 files changed, 246 insertions(+), 119 deletions(-) create mode 100644 src/index.d.ts create mode 100644 src/util/object.ts diff --git a/package.json b/package.json index c2e5a81..f96ef07 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "license": "GPL-3.0-or-later", "dependencies": { "@sendgrid/mail": "^6.5.5", + "@types/cookie-parser": "^1.4.6", "activitypub-types": "^1.0.3", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dompurify": "^3.0.6", "express": "^4.18.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51126fe..831f278 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,15 @@ dependencies: '@sendgrid/mail': specifier: ^6.5.5 version: 6.5.5 + '@types/cookie-parser': + specifier: ^1.4.6 + version: 1.4.6 activitypub-types: specifier: ^1.0.3 version: 1.0.3 + cookie-parser: + specifier: ^1.4.6 + version: 1.4.6 cors: specifier: ^2.8.5 version: 2.8.5 @@ -695,7 +701,6 @@ packages: dependencies: '@types/connect': 3.4.36 '@types/node': 20.8.2 - dev: true /@types/bson@4.0.5: resolution: {integrity: sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==} @@ -711,7 +716,12 @@ packages: resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} dependencies: '@types/node': 20.8.2 - dev: true + + /@types/cookie-parser@1.4.6: + resolution: {integrity: sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==} + dependencies: + '@types/express': 4.17.18 + dev: false /@types/dompurify@3.0.3: resolution: {integrity: sha512-odiGr/9/qMqjcBOe5UhcNLOFHSYmKFOyr+bJ/Xu3Qp4k1pNPAlNLUVNNLcLfjQI7+W7ObX58EdD3H+3p3voOvA==} @@ -726,7 +736,6 @@ packages: '@types/qs': 6.9.8 '@types/range-parser': 1.2.5 '@types/send': 0.17.2 - dev: true /@types/express@4.17.18: resolution: {integrity: sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==} @@ -735,11 +744,9 @@ packages: '@types/express-serve-static-core': 4.17.37 '@types/qs': 6.9.8 '@types/serve-static': 1.15.3 - dev: true /@types/http-errors@2.0.2: resolution: {integrity: sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==} - dev: true /@types/ical@0.8.1: resolution: {integrity: sha512-JQyqcdMGEa0aUaZPablO5okXvrAspGMzQYriYUV0C5RjDOk/7dqFklvl9yA1uidc0qtrZu4VBFgF0LXhPGPAJw==} @@ -757,11 +764,9 @@ packages: /@types/mime@1.3.3: resolution: {integrity: sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==} - dev: true /@types/mime@3.0.2: resolution: {integrity: sha512-Wj+fqpTLtTbG7c0tH47dkahefpLKEbB+xAZuLq7b4/IDHPl/n6VoXcyUQ2bypFlbSwvCr0y+bD4euTTqTJsPxQ==} - dev: true /@types/mongodb@3.6.20: resolution: {integrity: sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==} @@ -795,11 +800,9 @@ packages: /@types/qs@6.9.8: resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==} - dev: true /@types/range-parser@1.2.5: resolution: {integrity: sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==} - dev: true /@types/request@2.48.9: resolution: {integrity: sha512-4mi2hYsvPAhe8RXjk5DKB09sAUzbK68T2XjORehHdWyxFoX2zUnfi1VQ5wU4Md28H/5+uB4DkxY9BS4B87N/0A==} @@ -815,7 +818,6 @@ packages: dependencies: '@types/mime': 1.3.3 '@types/node': 20.8.2 - dev: true /@types/serve-static@1.15.3: resolution: {integrity: sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==} @@ -823,7 +825,6 @@ packages: '@types/http-errors': 2.0.2 '@types/mime': 3.0.2 '@types/node': 20.8.2 - dev: true /@types/sinonjs__fake-timers@8.1.1: resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} @@ -1283,10 +1284,23 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookie-parser@1.4.6: + resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + dev: false + /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false + /cookie@0.4.1: + resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} + engines: {node: '>= 0.6'} + dev: false + /cookie@0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} diff --git a/src/app.ts b/src/app.ts index 40425a8..0708081 100755 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,6 @@ import express from "express"; import hbs from "express-handlebars"; +import cookieParser from "cookie-parser"; import routes from "./routes.js"; import frontend from "./routes/frontend.js"; @@ -58,6 +59,9 @@ app.use(express.json({ type: activityPubContentType })); app.use(express.json({ type: "application/json" })); app.use(express.urlencoded({ extended: true })); +// Cookies // +app.use(cookieParser()); + // Router // app.use("/", staticPages); app.use("/", frontend); diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..292e5d3 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,14 @@ +import "express"; +import { GathioConfig } from "./lib/config.js"; + +interface Locals { + config: GathioConfig; +} + +declare module "express" { + export interface Response { + locals: { + config?: GathioConfig; + }; + } +} diff --git a/src/lib/config.ts b/src/lib/config.ts index b4385ca..4bc43bd 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,6 +1,7 @@ import fs from "fs"; import toml from "toml"; import { exitWithError } from "./process.js"; +import { Response } from "express"; interface StaticPage { title: string; @@ -8,7 +9,7 @@ interface StaticPage { filename: string; } -interface GathioConfig { +export interface GathioConfig { general: { domain: string; port: string; @@ -68,8 +69,21 @@ const defaultConfig: GathioConfig = { }, }; -export const frontendConfig = (): FrontendConfig => { - const config = getConfig(); +export const frontendConfig = (res: Response): FrontendConfig => { + const config = res.locals.config; + if (!config) { + return { + domain: defaultConfig.general.domain, + siteName: defaultConfig.general.site_name, + isFederated: defaultConfig.general.is_federated, + emailLogoUrl: defaultConfig.general.email_logo_url, + showPublicEventList: defaultConfig.general.show_public_event_list, + showKofi: defaultConfig.general.show_kofi, + showInstanceInformation: false, + staticPages: [], + version: process.env.npm_package_version || "unknown", + }; + } return { domain: config.general.domain, siteName: config.general.site_name, diff --git a/src/lib/middleware.ts b/src/lib/middleware.ts index 0594e90..5073137 100644 --- a/src/lib/middleware.ts +++ b/src/lib/middleware.ts @@ -1,14 +1,14 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import MagicLink from "../models/MagicLink.js"; -import getConfig from "../lib/config.js"; - -const config = getConfig(); +import getConfig, { GathioConfig } from "../lib/config.js"; +import { deepMerge } from "../util/object.js"; export const checkMagicLink = async ( req: Request, res: Response, - next: any, + next: NextFunction, ) => { + const config = getConfig(); if (!config.general.creator_email_addresses?.length) { // No creator email addresses are configured, so skip the magic link check return next(); @@ -49,3 +49,22 @@ export const checkMagicLink = async ( } next(); }; + +// Route-specific middleware which injects the config into the request object +// It can also be used to modify the config based on the request, which +// we use for Cypress testing. +export const getConfigMiddleware = ( + req: Request, + res: Response, + next: NextFunction, +) => { + const config = getConfig(); + if (process.env.CYPRESS === "true" && req.cookies?.cypressConfigOverride) { + console.log("Overriding config with Cypress config"); + const override = JSON.parse(req.cookies.cypressConfigOverride); + res.locals.config = deepMerge(config, override); + return next(); + } + res.locals.config = config; + return next(); +}; diff --git a/src/routes.js b/src/routes.js index 8ea7e05..9eedfb5 100755 --- a/src/routes.js +++ b/src/routes.js @@ -1511,7 +1511,7 @@ router.post("/activitypub/inbox", (req, res) => { }); router.use(function (req, res, next) { - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); }); addToLog("startup", "success", "Started up successfully"); diff --git a/src/routes/activitypub.ts b/src/routes/activitypub.ts index 667a44f..fc61dd7 100644 --- a/src/routes/activitypub.ts +++ b/src/routes/activitypub.ts @@ -1,21 +1,22 @@ import { Router, Request, Response, NextFunction } from "express"; import { createFeaturedPost, createWebfinger } from "../activitypub.js"; import { acceptsActivityPub } from "../lib/activitypub.js"; -import getConfig, { frontendConfig } from "../lib/config.js"; +import { frontendConfig } from "../lib/config.js"; import Event from "../models/Event.js"; import { addToLog } from "../helpers.js"; - -const config = getConfig(); +import { getConfigMiddleware } from "../lib/middleware.js"; const router = Router(); +router.use(getConfigMiddleware); + const send404IfNotFederated = ( req: Request, res: Response, next: NextFunction, ) => { - if (!config.general.is_federated) { - return res.status(404).render("404", frontendConfig()); + if (!res.locals.config?.general.is_federated) { + return res.status(404).render("404", frontendConfig(res)); } next(); }; @@ -27,7 +28,7 @@ router.get("/:eventID/featured", (req: Request, res: Response) => { const { eventID } = req.params; const featured = { "@context": "https://www.w3.org/ns/activitystreams", - id: `https://${config.general.domain}/${eventID}/featured`, + id: `https://${res.locals.config?.general.domain}/${eventID}/featured`, type: "OrderedCollection", orderedItems: [createFeaturedPost(eventID)], }; @@ -41,17 +42,17 @@ router.get("/:eventID/featured", (req: Request, res: Response) => { // return the JSON for a given activitypub message router.get("/:eventID/m/:hash", async (req: Request, res: Response) => { const { hash, eventID } = req.params; - const id = `https://${config.general.domain}/${eventID}/m/${hash}`; + const id = `https://${res.locals.config?.general.domain}/${eventID}/m/${hash}`; try { const event = await Event.findOne({ id: eventID, }); if (!event) { - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } else { if (!event.activityPubMessages) { - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } const message = event.activityPubMessages.find( (el) => el.id === id, @@ -68,7 +69,7 @@ router.get("/:eventID/m/:hash", async (req: Request, res: Response) => { ); } } else { - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } } } catch (err) { @@ -80,19 +81,19 @@ router.get("/:eventID/m/:hash", async (req: Request, res: Response) => { " failed with error: " + err, ); - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } }); router.get("/.well-known/nodeinfo", (req, res) => { - if (!config.general.is_federated) { - return res.status(404).render("404", frontendConfig()); + if (!res.locals.config?.general.is_federated) { + return res.status(404).render("404", frontendConfig(res)); } const nodeInfo = { links: [ { rel: "http://nodeinfo.diaspora.software/ns/schema/2.2", - href: `https://${config.general.domain}/.well-known/nodeinfo/2.2`, + href: `https://${res.locals.config?.general.domain}/.well-known/nodeinfo/2.2`, }, ], }; @@ -105,13 +106,13 @@ router.get("/.well-known/nodeinfo", (req, res) => { router.get("/.well-known/nodeinfo/2.2", async (req, res) => { const eventCount = await Event.countDocuments(); - if (!config.general.is_federated) { - return res.status(404).render("404", frontendConfig()); + if (!res.locals.config?.general.is_federated) { + return res.status(404).render("404", frontendConfig(res)); } const nodeInfo = { version: "2.2", instance: { - name: config.general.site_name, + name: res.locals.config?.general.site_name, description: "Federated, no-registration, privacy-respecting event hosting.", }, @@ -157,16 +158,24 @@ router.get("/.well-known/webfinger", async (req, res) => { const event = await Event.findOne({ id: eventID }); if (!event) { - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } else { if (acceptsActivityPub(req)) { res.header( "Content-Type", "application/activity+json", - ).send(createWebfinger(eventID, config.general.domain)); + ).send( + createWebfinger( + eventID, + res.locals.config?.general.domain, + ), + ); } else { res.header("Content-Type", "application/json").send( - createWebfinger(eventID, config.general.domain), + createWebfinger( + eventID, + res.locals.config?.general.domain, + ), ); } } @@ -176,7 +185,7 @@ router.get("/.well-known/webfinger", async (req, res) => { "error", `Attempt to render webfinger for ${resource} failed with error: ${err}`, ); - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } } }); @@ -192,13 +201,13 @@ router.get("/:eventID/followers", async (req, res) => { let followersCollection = { type: "OrderedCollection", totalItems: followers.length, - id: `https://${config.general.domain}/${eventID}/followers`, + id: `https://${res.locals.config?.general.domain}/${eventID}/followers`, first: { type: "OrderedCollectionPage", totalItems: followers.length, - partOf: `https://${config.general.domain}/${eventID}/followers`, + partOf: `https://${res.locals.config?.general.domain}/${eventID}/followers`, orderedItems: followers, - id: `https://${config.general.domain}/${eventID}/followers?page=1`, + id: `https://${res.locals.config?.general.domain}/${eventID}/followers?page=1`, }, "@context": ["https://www.w3.org/ns/activitystreams"], }; @@ -221,7 +230,7 @@ router.get("/:eventID/followers", async (req, res) => { "error", `Attempt to render followers for ${eventID} failed with error: ${err}`, ); - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } }); diff --git a/src/routes/event.ts b/src/routes/event.ts index 6be5ff8..ad77052 100644 --- a/src/routes/event.ts +++ b/src/routes/event.ts @@ -21,14 +21,11 @@ import { updateActivityPubActor, updateActivityPubEvent, } from "../activitypub.js"; -import getConfig from "../lib/config.js"; import { sendEmailFromTemplate } from "../lib/email.js"; import crypto from "crypto"; import ical from "ical"; import { markdownToSanitizedHTML } from "../util/markdown.js"; -import { checkMagicLink } from "../lib/middleware.js"; - -const config = getConfig(); +import { checkMagicLink, getConfigMiddleware } from "../lib/middleware.js"; const storage = multer.memoryStorage(); // Accept only JPEG, GIF or PNG images, up to 10MB @@ -58,6 +55,8 @@ const icsUpload = multer({ const router = Router(); +router.use(getConfigMiddleware); + router.post( "/event", upload.single("imageUpload"), @@ -149,7 +148,7 @@ router.post( firstLoad: true, activityPubActor: createActivityPubActor( eventID, - config.general.domain, + res.locals.config?.general.domain, publicKey, markdownToSanitizedHTML(eventData.eventDescription), eventData.eventName, @@ -169,7 +168,7 @@ router.post( ), activityPubMessages: [ { - id: `https://${config.general.domain}/${eventID}/m/featuredPost`, + id: `https://${res.locals.config?.general.domain}/${eventID}/m/featuredPost`, content: JSON.stringify( createFeaturedPost( eventID, @@ -198,9 +197,9 @@ router.post( { eventID, editToken, - siteName: config.general.site_name, - siteLogo: config.general.email_logo_url, - domain: config.general.domain, + siteName: res.locals.config?.general.site_name, + siteLogo: res.locals.config?.general.email_logo_url, + domain: res.locals.config?.general.domain, }, req, ); @@ -232,9 +231,10 @@ router.post( `New event in ${eventGroup.name}`, "eventGroupUpdated", { - siteName: config.general.site_name, - siteLogo: config.general.email_logo_url, - domain: config.general.domain, + siteName: res.locals.config?.general.site_name, + siteLogo: + res.locals.config?.general.email_logo_url, + domain: res.locals.config?.general.domain, eventGroupName: eventGroup.name, eventName: event.name, eventID: event.id, @@ -451,11 +451,11 @@ router.put( const guidObject = crypto.randomBytes(16).toString("hex"); const jsonObject = { "@context": "https://www.w3.org/ns/activitystreams", - id: `https://${config.general.domain}/${req.params.eventID}/m/${guidObject}`, + id: `https://${res.locals.config?.general.domain}/${req.params.eventID}/m/${guidObject}`, name: `RSVP to ${event.name}`, type: "Note", cc: "https://www.w3.org/ns/activitystreams#Public", - content: `${diffText} See here: https://${config.general.domain}/${req.params.eventID}`, + content: `${diffText} See here: https://${res.locals.config?.general.domain}/${req.params.eventID}`, }; broadcastCreateMessage(jsonObject, event.followers, eventID); // also broadcast an Update profile message to all followers so that at least Mastodon servers will update the local profile information @@ -472,7 +472,7 @@ router.put( "@context": "https://www.w3.org/ns/activitystreams", name: `RSVP to ${event.name}`, type: "Note", - content: `@${attendee.name} ${diffText} See here: https://${config.general.domain}/${req.params.eventID}`, + content: `@${attendee.name} ${diffText} See here: https://${res.locals.config?.general.domain}/${req.params.eventID}`, tag: [ { type: "Mention", @@ -498,9 +498,9 @@ router.put( { diffText, eventID: req.params.eventID, - siteName: config.general.site_name, - siteLogo: config.general.email_logo_url, - domain: config.general.domain, + siteName: res.locals.config?.general.site_name, + siteLogo: res.locals.config?.general.email_logo_url, + domain: res.locals.config?.general.domain, }, req, ); @@ -612,9 +612,9 @@ router.post( { eventID, editToken, - siteName: config.general.site_name, - siteLogo: config.general.email_logo_url, - domain: config.general.domain, + siteName: res.locals.config?.general.site_name, + siteLogo: res.locals.config?.general.email_logo_url, + domain: res.locals.config?.general.domain, }, req, ); diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts index 4cdce8a..240aff0 100644 --- a/src/routes/frontend.ts +++ b/src/routes/frontend.ts @@ -2,7 +2,7 @@ import { Router, Request, Response } from "express"; import moment from "moment-timezone"; import { marked } from "marked"; import { markdownToSanitizedHTML, renderPlain } from "../util/markdown.js"; -import getConfig, { frontendConfig, instanceRules } from "../lib/config.js"; +import { frontendConfig, instanceRules } from "../lib/config.js"; import { addToLog, exportICal } from "../helpers.js"; import Event from "../models/Event.js"; import EventGroup, { IEventGroup } from "../models/EventGroup.js"; @@ -11,41 +11,44 @@ import { activityPubContentType, } from "../lib/activitypub.js"; import MagicLink from "../models/MagicLink.js"; - -const config = getConfig(); +import { getConfigMiddleware } from "../lib/middleware.js"; const router = Router(); + +// Add config middleware to all routes +router.use(getConfigMiddleware); + router.get("/", (_: Request, res: Response) => { - if (config.general.show_public_event_list) { + if (res.locals.config?.general.show_public_event_list) { return res.redirect("/events"); } return res.render("home", { - ...frontendConfig(), + ...frontendConfig(res), instanceRules: instanceRules(), }); }); router.get("/about", (_: Request, res: Response) => { return res.render("home", { - ...frontendConfig(), + ...frontendConfig(res), instanceRules: instanceRules(), }); }); -router.get("/new", (_: Request, res: Response) => { - if (config.general.creator_email_addresses?.length) { - return res.render("createEventMagicLink", frontendConfig()); +router.get("/new", (req: Request, res: Response) => { + if (res.locals.config?.general.creator_email_addresses?.length) { + return res.render("createEventMagicLink", frontendConfig(res)); } return res.render("newevent", { title: "New event", - ...frontendConfig(), + ...frontendConfig(res), }); }); router.get("/new/:magicLinkToken", async (req: Request, res: Response) => { // If we don't have any creator email addresses, we don't need to check the magic link // so we can just redirect to the new event page - if (!config.general.creator_email_addresses?.length) { + if (!res.locals.config?.general.creator_email_addresses?.length) { return res.redirect("/new"); } const magicLink = await MagicLink.findOne({ @@ -55,7 +58,7 @@ router.get("/new/:magicLinkToken", async (req: Request, res: Response) => { }); if (!magicLink) { return res.render("createEventMagicLink", { - ...frontendConfig(), + ...frontendConfig(res), message: { type: "danger", text: "This magic link is invalid or has expired. Please request a new one here.", @@ -64,15 +67,15 @@ router.get("/new/:magicLinkToken", async (req: Request, res: Response) => { } res.render("newevent", { title: "New event", - ...frontendConfig(), + ...frontendConfig(res), magicLinkToken: req.params.magicLinkToken, creatorEmail: magicLink.email, }); }); router.get("/events", async (_: Request, res: Response) => { - if (!config.general.show_public_event_list) { - return res.status(404).render("404", frontendConfig()); + if (!res.locals.config?.general.show_public_event_list) { + return res.status(404).render("404", frontendConfig(res)); } const events = await Event.find({ showOnPublicList: true }) .populate("eventGroup") @@ -93,7 +96,7 @@ router.get("/events", async (_: Request, res: Response) => { "D MMM YYYY", )}`, eventHasConcluded: endMoment.isBefore(moment.tz(event.timezone)), - eventGroup: event.eventGroup, + eventGroup: event.eventGroup as any as IEventGroup, }; }); const upcomingEvents = updatedEvents.filter( @@ -105,13 +108,23 @@ router.get("/events", async (_: Request, res: Response) => { const eventGroups = await EventGroup.find({ showOnPublicList: true, }).lean(); + const updatedEventGroups = eventGroups.map((eventGroup) => { + return { + name: eventGroup.name, + numberOfEvents: updatedEvents.filter( + (event) => + event.eventGroup?._id.toString() === + eventGroup._id.toString(), + ).length, + }; + }); res.render("publicEventList", { title: "Public events", upcomingEvents: upcomingEvents, pastEvents: pastEvents, - eventGroups: eventGroups, - ...frontendConfig(), + eventGroups: updatedEventGroups, + ...frontendConfig(res), }); }); @@ -123,7 +136,7 @@ router.get("/:eventID", async (req: Request, res: Response) => { .lean() // Required, see: https://stackoverflow.com/questions/59690923/handlebars-access-has-been-denied-to-resolve-the-property-from-because-it-is .populate("eventGroup"); if (!event) { - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } const parsedLocation = event.location.replace(/\s+/g, "+"); let displayDate; @@ -286,9 +299,12 @@ router.get("/:eventID", async (req: Request, res: Response) => { .join(" ") .trim(), image: eventHasCoverImage - ? `https://${config.general.domain}/events/` + event.image + ? `https://${res.locals.config?.general.domain}/events/` + + event.image : null, - url: `https://${config.general.domain}/` + req.params.eventID, + url: + `https://${res.locals.config?.general.domain}/` + + req.params.eventID, }; if (acceptsActivityPub(req)) { res.header("Content-Type", activityPubContentType).send( @@ -297,7 +313,7 @@ router.get("/:eventID", async (req: Request, res: Response) => { } else { res.set("X-Robots-Tag", "noindex"); res.render("event", { - ...frontendConfig(), + ...frontendConfig(res), title: event.name, escapedName: escapedName, eventData: event, @@ -324,10 +340,11 @@ router.get("/:eventID", async (req: Request, res: Response) => { firstLoad: firstLoad, eventHasConcluded: eventHasConcluded, eventHasBegun: eventHasBegun, - eventWillBeDeleted: config.general.delete_after_days > 0, + eventWillBeDeleted: + (res.locals.config?.general.delete_after_days || 0) > 0, daysUntilDeletion: moment .tz(event.end, event.timezone) - .add(config.general.delete_after_days, "days") + .add(res.locals.config?.general.delete_after_days, "days") .fromNow(), metadata: metadata, jsonData: { @@ -368,7 +385,7 @@ router.get("/:eventID", async (req: Request, res: Response) => { err, ); console.log(err); - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } }); @@ -379,7 +396,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { }).lean(); if (!eventGroup) { - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } const parsedDescription = markdownToSanitizedHTML( eventGroup.description, @@ -446,15 +463,18 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { .join(" ") .trim(), image: eventGroupHasCoverImage - ? `https://${config.general.domain}/events/` + eventGroup.image + ? `https://${res.locals.config?.general.domain}/events/` + + eventGroup.image : null, - url: `https://${config.general.domain}/` + req.params.eventID, + url: + `https://${res.locals.config?.general.domain}/` + + req.params.eventID, }; res.set("X-Robots-Tag", "noindex"); res.render("eventgroup", { - ...frontendConfig(), - domain: config.general.domain, + ...frontendConfig(res), + domain: res.locals.config?.general.domain, title: eventGroup.name, eventGroupData: eventGroup, escapedName: escapedName, @@ -485,7 +505,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { `Attempt to display event group ${req.params.eventGroupID} failed with error: ${err}`, ); console.log(err); - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } }); @@ -512,7 +532,7 @@ router.get( `Attempt to display event group feed for ${req.params.eventGroupID} failed with error: ${err}`, ); console.log(err); - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } }, ); @@ -534,7 +554,7 @@ router.get("/export/event/:eventID", async (req: Request, res: Response) => { `Attempt to export event ${req.params.eventID} failed with error: ${err}`, ); console.log(err); - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } }); @@ -560,7 +580,7 @@ router.get( `Attempt to export event group ${req.params.eventGroupID} failed with error: ${err}`, ); console.log(err); - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } }, ); diff --git a/src/routes/group.ts b/src/routes/group.ts index c006a5d..8afd766 100644 --- a/src/routes/group.ts +++ b/src/routes/group.ts @@ -1,5 +1,4 @@ import { Router, Response, Request } from "express"; -import getConfig from "../lib/config.js"; import multer from "multer"; import { generateEditToken, generateEventID } from "../util/generator.js"; import { validateGroupData } from "../util/validation.js"; @@ -9,9 +8,7 @@ import EventGroup from "../models/EventGroup.js"; import { sendEmailFromTemplate } from "../lib/email.js"; import { marked } from "marked"; import { renderPlain } from "../util/markdown.js"; -import { checkMagicLink } from "../lib/middleware.js"; - -const config = getConfig(); +import { checkMagicLink, getConfigMiddleware } from "../lib/middleware.js"; const storage = multer.memoryStorage(); // Accept only JPEG, GIF or PNG images, up to 10MB @@ -30,6 +27,8 @@ const upload = multer({ const router = Router(); +router.use(getConfigMiddleware); + router.post( "/group", upload.single("imageUpload"), @@ -101,9 +100,9 @@ router.post( { eventGroupID: eventGroup.id, editToken: eventGroup.editToken, - siteName: config.general.site_name, - siteLogo: config.general.email_logo_url, - domain: config.general.domain, + siteName: res.locals.config?.general.site_name, + siteLogo: res.locals.config?.general.email_logo_url, + domain: res.locals.config?.general.domain, }, req, ); diff --git a/src/routes/magicLink.ts b/src/routes/magicLink.ts index 24f0667..499d0a4 100644 --- a/src/routes/magicLink.ts +++ b/src/routes/magicLink.ts @@ -1,17 +1,19 @@ import { Router, Request, Response } from "express"; -import getConfig, { frontendConfig } from "../lib/config.js"; +import { frontendConfig } from "../lib/config.js"; import { sendEmailFromTemplate } from "../lib/email.js"; import { generateMagicLinkToken } from "../util/generator.js"; import MagicLink from "../models/MagicLink.js"; +import { getConfigMiddleware } from "../lib/middleware.js"; const router = Router(); -const config = getConfig(); + +router.use(getConfigMiddleware); router.post("/magic-link/event/create", async (req: Request, res: Response) => { const { email } = req.body; if (!email) { res.render("createEventMagicLink", { - ...frontendConfig(), + ...frontendConfig(res), message: { type: "danger", text: "Please provide an email address.", @@ -19,14 +21,14 @@ router.post("/magic-link/event/create", async (req: Request, res: Response) => { }); return; } - const allowedEmails = config.general.creator_email_addresses; + const allowedEmails = res.locals.config?.general.creator_email_addresses; if (!allowedEmails?.length) { // No creator email addresses are configured, so skip the magic link check return res.redirect("/new"); } if (!allowedEmails.includes(email)) { res.render("createEventMagicLink", { - ...frontendConfig(), + ...frontendConfig(res), message: { type: "success", text: "Thanks! If this email address can create events, you should receive an email with a magic link.", @@ -52,14 +54,14 @@ router.post("/magic-link/event/create", async (req: Request, res: Response) => { "createEventMagicLink", { token, - siteName: config.general.site_name, - siteLogo: config.general.email_logo_url, - domain: config.general.domain, + siteName: res.locals.config?.general.site_name, + siteLogo: res.locals.config?.general.email_logo_url, + domain: res.locals.config?.general.domain, }, req, ); res.render("createEventMagicLink", { - ...frontendConfig(), + ...frontendConfig(res), message: { type: "success", text: "Thanks! If this email address can create events, you should receive an email with a magic link.", diff --git a/src/routes/static.ts b/src/routes/static.ts index 33f0225..6fab98d 100644 --- a/src/routes/static.ts +++ b/src/routes/static.ts @@ -21,13 +21,13 @@ if (config.static_pages?.length) { return res.render("static", { title: page.title, content: parsed, - ...frontendConfig(), + ...frontendConfig(res), }); } - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } catch (err) { console.error(err); - return res.status(404).render("404", frontendConfig()); + return res.status(404).render("404", frontendConfig(res)); } }); }); diff --git a/src/util/object.ts b/src/util/object.ts new file mode 100644 index 0000000..1ecc89b --- /dev/null +++ b/src/util/object.ts @@ -0,0 +1,30 @@ +/** + * Simple object check. + */ +export function isObject(item: any) { + return item && typeof item === "object" && !Array.isArray(item); +} + +/** + * Deep merge two objects. + */ +export function deepMerge( + target: Record, + ...sources: Record[] +): T { + if (!sources.length) return target; + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + deepMerge(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return deepMerge(target, ...sources) as T; +} -- cgit v1.2.3 From 9e11c3667e027f805fca37b5dffe9d8a52303a14 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Mon, 26 Feb 2024 12:10:32 +0000 Subject: testing: E2E tests for public and restricted events --- cypress/e2e/event.cy.ts | 24 +------------ cypress/e2e/group.cy.ts | 10 ++---- cypress/e2e/magicLink.cy.ts | 14 ++++++++ cypress/e2e/publicEvent.cy.ts | 75 +++++++++++++++++++++++++++++++++++++++++ cypress/e2e/publicGroup.cy.ts | 28 +++++++++++++++ cypress/fixtures/eventData.json | 16 +++++++++ cypress/fixtures/example.json | 5 --- cypress/fixtures/groupData.json | 7 ++++ cypress/support/commands.ts | 23 ++++++++----- cypress/tsconfig.json | 4 ++- 10 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 cypress/e2e/magicLink.cy.ts create mode 100644 cypress/e2e/publicEvent.cy.ts create mode 100644 cypress/e2e/publicGroup.cy.ts create mode 100644 cypress/fixtures/eventData.json delete mode 100644 cypress/fixtures/example.json create mode 100644 cypress/fixtures/groupData.json diff --git a/cypress/e2e/event.cy.ts b/cypress/e2e/event.cy.ts index 8870164..c49c518 100644 --- a/cypress/e2e/event.cy.ts +++ b/cypress/e2e/event.cy.ts @@ -1,19 +1,4 @@ -const eventData = { - eventName: "Your Event Name", - eventLocation: "Event Location", - timezone: "America/New York", - eventDescription: "Event Description", - eventURL: "https://example.com", - hostName: "Your Name", - creatorEmail: "test@example.com", - eventGroupCheckbox: false, - interactionCheckbox: true, - joinCheckbox: true, - maxAttendeesCheckbox: true, - maxAttendees: 10, - eventStart: "2030-01-01T00:00", - eventEnd: "2030-01-01T01:00", -}; +import eventData from "../fixtures/eventData.json"; describe("Events", () => { beforeEach(() => { @@ -37,13 +22,6 @@ describe("Events", () => { cy.get("#hostName").type(eventData.hostName); cy.get("#creatorEmail").type(eventData.creatorEmail); - // Check checkboxes based on eventData - if (eventData.eventGroupCheckbox) { - cy.get("#eventGroupCheckbox").check(); - cy.get("#eventGroupID").type(eventData.eventGroupID); - cy.get("#eventGroupEditToken").type(eventData.eventGroupEditToken); - } - if (eventData.interactionCheckbox) { cy.get("#interactionCheckbox").check(); } diff --git a/cypress/e2e/group.cy.ts b/cypress/e2e/group.cy.ts index 279cb6c..69c722a 100644 --- a/cypress/e2e/group.cy.ts +++ b/cypress/e2e/group.cy.ts @@ -1,14 +1,8 @@ -const groupData = { - eventGroupName: "Test Group", - eventGroupDescription: "Test Group Description", - eventGroupURL: "https://example.com", - hostName: "Test Host", - creatorEmail: "test@example.com", -}; +import groupData from "../fixtures/groupData.json"; describe("Groups", () => { beforeEach(() => { - cy.createGroup(groupData); + cy.createGroup(groupData, false); }); it("creates a new group", function () { cy.get("#eventGroupName").should("have.text", groupData.eventGroupName); diff --git a/cypress/e2e/magicLink.cy.ts b/cypress/e2e/magicLink.cy.ts new file mode 100644 index 0000000..5540415 --- /dev/null +++ b/cypress/e2e/magicLink.cy.ts @@ -0,0 +1,14 @@ +describe("Restricted Event Creation", () => { + it("should redirect to the magic link form", () => { + cy.setCookie( + "cypressConfigOverride", + JSON.stringify({ + general: { + creator_email_addresses: ["test@test.com"], + }, + }), + ); + cy.visit("/new"); + cy.get("h2").should("contain", "Request a link to create a new event"); + }); +}); diff --git a/cypress/e2e/publicEvent.cy.ts b/cypress/e2e/publicEvent.cy.ts new file mode 100644 index 0000000..f110c02 --- /dev/null +++ b/cypress/e2e/publicEvent.cy.ts @@ -0,0 +1,75 @@ +import eventData from "../fixtures/eventData.json"; + +describe("Events", () => { + beforeEach(() => { + cy.setCookie( + "cypressConfigOverride", + JSON.stringify({ + general: { + show_public_event_list: true, + }, + }), + ); + cy.visit("/new"); + cy.get("#showNewEventFormButton").click(); + + cy.get("#eventName").type(eventData.eventName); + cy.get("#eventLocation").type(eventData.eventLocation); + // These are datetime-local inputs + cy.get("#eventStart").type(eventData.eventStart); + cy.get("#eventEnd").type(eventData.eventEnd); + + cy.get("select#timezone + span.select2").click(); + cy.get(".select2-results__option") + .contains(eventData.timezone) + .click({ force: true }); + + cy.get("#eventDescription").type(eventData.eventDescription); + cy.get("#eventURL").type(eventData.eventURL); + + cy.get("#hostName").type(eventData.hostName); + cy.get("#creatorEmail").type(eventData.creatorEmail); + + // Check checkboxes based on eventData + if (eventData.interactionCheckbox) { + cy.get("#interactionCheckbox").check(); + } + + if (eventData.joinCheckbox) { + cy.get("#joinCheckbox").check(); + } + + if (eventData.maxAttendeesCheckbox) { + cy.get("#maxAttendeesCheckbox").check(); + cy.get("#maxAttendees").type(eventData.maxAttendees.toString()); + } + + cy.get("#publicEventCheckbox").check(); + + // Submit the form + cy.get("#newEventFormSubmit").click(); + + // Wait for the new page to load + cy.url({ timeout: 10000 }).should("not.include", "/new"); + + // Get the new event ID from the URL + cy.url().then((url) => { + const [eventID, editToken] = url.split("/").pop().split("?"); + cy.wrap(eventID).as("eventID"); + cy.wrap(editToken).as("editToken"); + }); + }); + + it("should be visible in the public event list", function () { + cy.setCookie( + "cypressConfigOverride", + JSON.stringify({ + general: { + show_public_event_list: true, + }, + }), + ); + cy.visit("/"); + cy.get("#upcomingEvents").should("contain", eventData.eventName); + }); +}); diff --git a/cypress/e2e/publicGroup.cy.ts b/cypress/e2e/publicGroup.cy.ts new file mode 100644 index 0000000..4536195 --- /dev/null +++ b/cypress/e2e/publicGroup.cy.ts @@ -0,0 +1,28 @@ +import groupData from "../fixtures/groupData.json"; + +describe("Groups", () => { + beforeEach(() => { + cy.setCookie( + "cypressConfigOverride", + JSON.stringify({ + general: { + show_public_event_list: true, + }, + }), + ); + cy.createGroup(groupData, true); + }); + it("should be visible in the public group list", function () { + cy.setCookie( + "cypressConfigOverride", + JSON.stringify({ + general: { + show_public_event_list: true, + }, + }), + ); + cy.visit("/"); + cy.get("#groupsTab").click(); + cy.get("#eventGroups").should("contain", groupData.eventGroupName); + }); +}); diff --git a/cypress/fixtures/eventData.json b/cypress/fixtures/eventData.json new file mode 100644 index 0000000..a38ccf2 --- /dev/null +++ b/cypress/fixtures/eventData.json @@ -0,0 +1,16 @@ +{ + "eventName": "Your Event Name", + "eventLocation": "Event Location", + "timezone": "America/New York", + "eventDescription": "Event Description", + "eventURL": "https://example.com", + "hostName": "Your Name", + "creatorEmail": "test@example.com", + "eventGroupCheckbox": false, + "interactionCheckbox": true, + "joinCheckbox": true, + "maxAttendeesCheckbox": true, + "maxAttendees": 10, + "eventStart": "2030-01-01T00:00", + "eventEnd": "2030-01-01T01:00" +} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json deleted file mode 100644 index 519902d..0000000 --- a/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/cypress/fixtures/groupData.json b/cypress/fixtures/groupData.json new file mode 100644 index 0000000..907c3b2 --- /dev/null +++ b/cypress/fixtures/groupData.json @@ -0,0 +1,7 @@ +{ + "eventGroupName": "Test Group", + "eventGroupDescription": "Test Group Description", + "eventGroupURL": "https://example.com", + "hostName": "Test Host", + "creatorEmail": "test@example.com" +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 7535103..eadcd20 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -38,17 +38,20 @@ declare namespace Cypress { interface Chainable { - createGroup(groupData: { - eventGroupName: string; - eventGroupDescription: string; - eventGroupURL: string; - hostName: string; - creatorEmail: string; - }): Chainable; + createGroup( + groupData: { + eventGroupName: string; + eventGroupDescription: string; + eventGroupURL: string; + hostName: string; + creatorEmail: string; + }, + isPublic: boolean, + ): Chainable; } } -Cypress.Commands.add("createGroup", (groupData) => { +Cypress.Commands.add("createGroup", (groupData, isPublic) => { cy.visit("/new"); cy.get("#showNewEventGroupFormButton").click(); @@ -59,6 +62,10 @@ Cypress.Commands.add("createGroup", (groupData) => { cy.get("#eventGroupHostName").type(groupData.hostName); cy.get("#eventGroupCreatorEmail").type(groupData.creatorEmail); + if (isPublic) { + cy.get("#publicGroupCheckbox").check(); + } + // Submit the form cy.get("#newEventGroupForm").submit(); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index dc61836..9a8b8a0 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -2,7 +2,9 @@ "compilerOptions": { "target": "es5", "lib": ["es5", "dom"], - "types": ["cypress", "node"] + "types": ["cypress", "node"], + "resolveJsonModule": true, + "esModuleInterop": true }, "include": ["**/*.ts"] } -- cgit v1.2.3 From 13e94921bd7942628a246f8c613cd898c45d9280 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Mon, 26 Feb 2024 12:10:46 +0000 Subject: fix: remove unused imports --- src/helpers.ts | 2 +- src/lib/email.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 6eda3d0..47b380f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,6 @@ import moment from "moment-timezone"; import icalGenerator from "ical-generator"; -import Log, { ILog } from "./models/Log.js"; +import Log from "./models/Log.js"; import { getConfig } from "./lib/config.js"; import { IEvent } from "./models/Event.js"; diff --git a/src/lib/email.ts b/src/lib/email.ts index 8a215a9..9b8162b 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -1,6 +1,6 @@ import { Request } from "express"; import sgMail from "@sendgrid/mail"; -import nodemailer, { TransportOptions } from "nodemailer"; +import nodemailer from "nodemailer"; import { getConfig } from "./config.js"; import SMTPTransport from "nodemailer/lib/smtp-transport/index.js"; import { exitWithError } from "./process.js"; -- cgit v1.2.3 From 1eb794de5d121d090811919bc4addd63ea2fd321 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Mon, 26 Feb 2024 12:11:10 +0000 Subject: text/styling tweaks for public event list --- views/publicEventList.handlebars | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/views/publicEventList.handlebars b/views/publicEventList.handlebars index 6d85ca7..bceae58 100644 --- a/views/publicEventList.handlebars +++ b/views/publicEventList.handlebars @@ -1,17 +1,17 @@

{{siteName}}

-

{{siteName}} is an instance of Gathio, a simple, federated, privacy-first event hosting platform.

+

{{siteName}} runs on Gathio — a simple, federated, privacy-first event hosting platform.

-
+
Upcoming events
{{#if upcomingEvents}} @@ -32,7 +32,7 @@
-
+
Past events
{{#if pastEvents}} @@ -42,7 +42,7 @@ {{this.name}} {{this.displayDate}} {{#if this.eventGroup}} - In group: {{this.eventGroup.name}} + {{this.eventGroup.name}} {{/if}} {{/each}} @@ -54,7 +54,7 @@
-
+
Event groups
{{#if eventGroups}} @@ -62,6 +62,7 @@ {{this.name}} + {{this.numberOfEvents}} {{plural this.numberOfEvents "event(s)"}} {{/each}} {{else}} -- cgit v1.2.3 From 1275280a9e3a31f6080079d564a8fb9e1847db8b Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Mon, 26 Feb 2024 13:10:20 +0000 Subject: fix: tests in CI --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ee429c0..e4f0e14 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,6 +65,8 @@ jobs: with: start: pnpm start browser: chrome + env: + CYPRESS: true - name: Upload screenshots uses: actions/upload-artifact@v3 -- cgit v1.2.3