Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM node:22-alpine
WORKDIR /app
RUN apk add --no-cache python3 build-base
ADD package.json pnpm-lock.yaml /app/
RUN npm install -g pnpm
RUN pnpm install
COPY . /app/
# Dev: do not perform full build fail-stop; allow partial TS
CMD ["pnpm", "run", "dev"]
26 changes: 25 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ volumes:
services:
gathio:
container_name: gathio-app
image: ghcr.io/lowercasename/gathio:latest
build:
context: .
dockerfile: Dockerfile
links:
- mongo
environment:
- NODE_ENV=production
ports:
- 3000:3000
volumes:
Expand All @@ -17,6 +21,26 @@ services:
- ./gathio-docker/static:/app/static
# The path to Gathio's user-uploaded event images folder - change to match your system
- ./gathio-docker/images:/app/public/events
# Use Dockerfile default CMD (npm run start); build already performed in multi-stage.
gathio-dev:
container_name: gathio-app-dev
build:
context: .
dockerfile: Dockerfile.dev
links:
- mongo
ports:
- 3001:3000
environment:
- NODE_ENV=development
volumes:
- ./gathio-docker/config:/app/config
- ./gathio-docker/static:/app/static
- ./gathio-docker/images:/app/public/events
- ./src:/app/src
- ./views:/app/views
- ./locales:/app/locales
command: pnpm run dev
mongo:
container_name: gathio-db
image: mongo:latest
Expand Down
7 changes: 6 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@
"views.event.started": "Started",
"views.event.welcome": "Welcome to your event!",
"views.event.currentlyediting": "You are currently editing this event. Do not share this link!",
"views.event.approveattendee": "Approve attendee",
"views.event.pending": "Pending",
"views.event.requireapproval": "Require host approvals for attendees to see location",
"views.eventgroup.about": "About",
"views.eventgroup.addevent": "To link an existing event to this group, copy and paste the two codes below into the 'Event Group' box when creating a new event or editing an existing event.",
"views.eventgroup.del": "Delete this event group",
Expand Down Expand Up @@ -307,5 +310,7 @@
"views.publiceventlist.numoevents_zero": "No event",
"views.publiceventlist.pastevents": "Past events",
"views.publiceventlist.upcomingevents": "Upcoming events",
"views.right": "Get it right!"
"views.right": "Get it right!",
"views.event.location_hidden": "Location hidden until approved",
"views.event.location_hidden_desc": "Your registration is pending approval. Once approved, revisit this page using your secret link (?p= parameter) to view the event address."
}
2 changes: 2 additions & 0 deletions public/js/modules/event-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function editEventForm() {
joinCheckbox: window.eventData.usersCanAttend,
maxAttendeesCheckbox: window.eventData.maxAttendees !== null,
maxAttendees: window.eventData.maxAttendees,
approveRegistrationsCheckbox: window.eventData.approveRegistrations,
},
errors: [],
submitting: false,
Expand All @@ -64,6 +65,7 @@ function editEventForm() {
this.data.maxAttendeesCheckbox =
window.eventData.maxAttendees !== null;
this.data.publicCheckbox = window.eventData.showOnPublicList;
this.data.approveRegistrationsCheckbox = window.eventData.approveRegistrations;
},
updateEventEnd() {
if (this.data.eventEnd === "" || this.data.eventEnd < this.data.eventStart) {
Expand Down
2 changes: 2 additions & 0 deletions public/js/modules/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function newEventForm() {
joinCheckbox: false,
maxAttendeesCheckbox: false,
maxAttendees: "",
approveRegistrationsCheckbox: false,
},
errors: [],
submitting: false,
Expand All @@ -67,6 +68,7 @@ function newEventForm() {
this.data.joinCheckbox = false;
this.data.maxAttendeesCheckbox = false;
this.data.publicCheckbox = false;
this.data.approveRegistrationsCheckbox = false;
},
updateEventEnd() {
if (this.data.eventEnd === "" || this.data.eventEnd < this.data.eventStart) {
Expand Down
8 changes: 4 additions & 4 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,16 +200,16 @@ export const getConfig = (): GathioConfig => {
const config = toml.parse(
fs.readFileSync("./config/config.toml", "utf-8"),
) as GathioConfig;
const resolvedConfig = {
const resolvedConfig: GathioConfig = {
...defaultConfig,
...config,
}
};
if (process.env.CYPRESS || process.env.CI) {
config.general.mail_service = "none";
resolvedConfig.general.mail_service = "none";
console.log(
"Running in Cypress or CI, not initializing email service.",
);
} else if (config.general.mail_service === "none") {
} else if (resolvedConfig.general.mail_service === "none") {
console.warn(
"You have not configured this Gathio instance to send emails! This means that event creators will not receive emails when their events are created, which means they may end up locked out of editing events. Consider setting up an email service.",
);
Expand Down
10 changes: 10 additions & 0 deletions src/models/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface IAttendee {
created?: Date;
_id: string;
visibility?: "public" | "private";
approved?: boolean; // Host has approved this attendee to view protected info
}

export interface IReply {
Expand Down Expand Up @@ -74,6 +75,7 @@ export interface IEvent extends mongoose.Document {
followers?: IFollower[];
activityPubMessages?: IActivityPubMessage[];
showOnPublicList?: boolean;
approveRegistrations?: boolean; // Per-event: hide location until attendee approved
}

const Attendees = new mongoose.Schema({
Expand Down Expand Up @@ -113,6 +115,10 @@ const Attendees = new mongoose.Schema({
default: "public",
},
created: Date,
approved: {
type: Boolean,
default: false,
},
});

const Followers = new mongoose.Schema(
Expand Down Expand Up @@ -339,6 +345,10 @@ const EventSchema = new mongoose.Schema({
type: Boolean,
default: false,
},
approveRegistrations: {
type: Boolean,
default: false,
},
});

export default mongoose.model<IEvent>("Event", EventSchema);
56 changes: 49 additions & 7 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,12 +506,6 @@ router.post("/deleteeventgroup/:eventGroupID/:editToken", (req, res) => {

router.post("/attendee/provision", async (req, res) => {
const removalPassword = niceware.generatePassphrase(6).join("-");
const newAttendee = {
status: "provisioned",
removalPassword,
created: Date.now(),
};

const event = await Event.findOne({ id: req.query.eventID }).catch((e) => {
addToLog(
"provisionEventAttendee",
Expand All @@ -528,6 +522,13 @@ router.post("/attendee/provision", async (req, res) => {
return res.sendStatus(404);
}

const newAttendee = {
status: "provisioned",
removalPassword,
created: Date.now(),
approved: !event.approveRegistrations, // Auto approve if this event does not require approvals
};

event.attendees.push(newAttendee);
await event.save().catch((e) => {
console.log(e);
Expand Down Expand Up @@ -617,6 +618,7 @@ router.post("/attendevent/:eventID", async (req, res) => {
"attendees.$.visibility": req.body.attendeeVisible
? "public"
: "private",
// Do not modify approval status here
},
},
)
Expand Down Expand Up @@ -654,7 +656,8 @@ router.post("/attendevent/:eventID", async (req, res) => {
res.status(500).end();
});
}
res.redirect(`/${req.params.eventID}`);
// Redirect with ?p so attendee immediately has credential in URL
res.redirect(`/${req.params.eventID}?p=${encodeURIComponent(req.body.removalPassword)}`);
})
.catch((error) => {
res.send("Database error, please try again :(");
Expand Down Expand Up @@ -782,6 +785,45 @@ router.post("/removeattendee/:eventID/:attendeeID", (req, res) => {
});
});

// Approve an attendee (allow them to view hidden location) when instance requires approval
router.post("/approveattendee/:eventID/:attendeeID", async (req, res) => {
try {
const event = await Event.findOne({ id: req.params.eventID });
if (!event) {
return res.sendStatus(404);
}
// Require edit token in query (?e=)
const editToken = req.query.e;
if (!editToken || editToken !== event.editToken) {
return res.sendStatus(403);
}
// Find attendee
const attendee = event.attendees?.find((a) => a._id.toString() === req.params.attendeeID);
if (!attendee) {
return res.sendStatus(404);
}
attendee.approved = true;
await event.save();
addToLog(
"approveEventAttendee",
"success",
"Attendee approved in event " + req.params.eventID,
);
return res.redirect(`/${req.params.eventID}?e=${event.editToken}&m=approved`);
} catch (error) {
console.error(error);
addToLog(
"approveEventAttendee",
"error",
"Attempt to approve attendee in event " +
req.params.eventID +
" failed with error: " +
error,
);
return res.sendStatus(500);
}
});

/*
* Create an email subscription on an event group.
*/
Expand Down
3 changes: 3 additions & 0 deletions src/routes/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ router.post(
],
publicKey,
privateKey,
approveRegistrations: eventData.approveRegistrationsBoolean && eventData.joinBoolean,
});
try {
await event.save();
Expand Down Expand Up @@ -396,6 +397,8 @@ router.put(
eventData.timezone,
)
: undefined,
approveRegistrations:
eventData.approveRegistrationsBoolean && eventData.joinBoolean,
};
let diffText =
"<p>" + i18next.t("routes.event.difftext") + "</p><ul>";
Expand Down
Loading