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
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
golang 1.20.14
nodejs 18.20.2
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ Built with [Vue 2](https://github.com/vuejs/vue), [MongoDB](https://github.com/m
- Export availability as CSV
- Only show responses to event creator

## Local development
- Prereqs: Node 18+, Go 1.20+, MongoDB on `localhost:27017`, GCP service account key JSON.
- Backend: create `server/.env` (includes `SERVICE_ACCOUNT_KEY_PATH` and any Stripe/OAuth/email keys), start Mongo, then `cd server && air` (or `go run main.go`) to run `http://localhost:3002/api`. Cloud Tasks is skipped if `SERVICE_ACCOUNT_KEY_PATH` is unset or the file is missing.
- Frontend: `cd frontend && npm install && npm run serve` to run `http://localhost:8080` against the local API; `npm run build` to serve from Go.
- Detailed steps and env samples: `docs/local-dev.md`.
- Docker options:
- Hybrid (Mongo only): `docker compose up -d mongo`, then run Go/Vue locally after installing deps.
- Full stack: `docker compose up --build` spins up Mongo, the Go API, and the Vue dev server (see `docs/local-dev.md` for env/secrets prep).
- Tool versions: `.tool-versions` pins `golang 1.20.14` and `nodejs 18.20.2` (works with asdf or as a reference) to avoid mismatched runtimes with the Go module and Vue CLI 5 stack.
- asdf + zsh: `asdf plugin add golang nodejs` (if not present), `asdf install`, ensure `source "$HOME/.asdf/asdf.sh"` is in `~/.zshrc`, and add `export PATH="$PATH:$(go env GOPATH)/bin"` so Go-installed tools (e.g., `air`) are found.
- Seed demo data (optional): `cd server/scripts/seed_demo && MONGODB_URI=mongodb://localhost:27017 go run main.go` creates a demo user/event for local testing. Avoid running other scripts in `server/scripts/*` unless you know the migration you need.

## Self-hosting

Coming soon...
44 changes: 44 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
version: "3.9"

services:
mongo:
image: mongo:6
restart: unless-stopped
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db

server:
image: golang:1.20
working_dir: /app/server
command: sh -c "go mod download && go run main.go"
env_file:
- server/.env
environment:
- MONGODB_URI=mongodb://mongo:27017
- SERVICE_ACCOUNT_KEY_PATH=/secrets/service_account_key.json
volumes:
- ./:/app
# Mount your GCP service account key JSON here
- ./secrets/service_account_key.json:/secrets/service_account_key.json:ro
ports:
- "3002:3002"
depends_on:
- mongo

frontend:
image: node:18
working_dir: /app/frontend
command: sh -c "npm install && npm run serve -- --host 0.0.0.0 --port 8080"
environment:
- CHOKIDAR_USEPOLLING=true
volumes:
- ./:/app
ports:
- "8080:8080"
depends_on:
- server

volumes:
mongo_data:
31 changes: 13 additions & 18 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
# timeful
# Timeful frontend

## Project setup
Vue 2 app for Timeful.

```
npm install
```
## Quick start
- Install: `npm install`
- Dev server: `npm run serve` (http://localhost:8080 or 8081 if 8080 is busy)
- API base (dev): `http://localhost:3002/api` (see `src/constants.js`)
- Build for production: `npm run build` (Go API serves `frontend/dist`)

### Compiles and hot-reloads for development
## Backend API docs
- http://localhost:3002/swagger/index.html (server must be running)

```
npm run serve
```
## Optional demo data
- From repo root: `cd server/scripts/seed_demo && MONGODB_URI=mongodb://localhost:27017 go run main.go` (adds demo user + event)

### Compiles and minifies for production

```
npm run build
```

### Customize configuration

See [Configuration Reference](https://cli.vuejs.org/config/).
## Customize configuration
- Vue CLI reference: https://cli.vuejs.org/config/
44 changes: 44 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Core
SERVICE_ACCOUNT_KEY_PATH=/secrets/service_account_key.json
MONGODB_URI=mongodb://mongo:27017
ENCRYPTION_KEY=32_char_encryption_key_here

# OAuth / clients
CLIENT_ID=google_oauth_client_id
CLIENT_SECRET=google_oauth_client_secret
MICROSOFT_CLIENT_ID=microsoft_client_id
MICROSOFT_CLIENT_SECRET=microsoft_client_secret
IOS_CLIENT_ID=ios_client_id_optional
ANDROID_CLIENT_ID=android_client_id_optional

# Stripe (optional unless you hit billing paths)
STRIPE_API_KEY=sk_test_xxx
STRIPE_MONTHLY_PRICE_ID=price_xxx
STRIPE_MONTHLY_STUDENT_PRICE_ID=price_xxx
STRIPE_LIFETIME_STUDENT_PRICE_ID=price_xxx
STRIPE_YEARLY_PRICE_ID=price_xxx
STRIPE_LIFETIME_PRICE_ID=price_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

# Email / notifications (optional; set LISTMONK_ENABLED=false to skip)
LISTMONK_ENABLED=false
LISTMONK_URL=
LISTMONK_USERNAME=
LISTMONK_PASSWORD=
LISTMONK_LIST_ID=
LISTMONK_INITIAL_EMAIL_REMINDER_ID=
LISTMONK_SECOND_EMAIL_REMINDER_ID=
LISTMONK_FINAL_EMAIL_REMINDER_ID=
SCHEJ_EMAIL_ADDRESS=
GMAIL_APP_PASSWORD=
MAILCHIMP_API_KEY=
MAILJET_API_KEY=
MAILJET_API_SECRET=
MAILJET_LIST_ID=

# Slack / Discord (optional)
SLACK_DEV_WEBHOOK_URL=
SLACK_PROD_WEBHOOK_URL=
SLACK_MONETIZATION_WEBHOOK_URL=
DISCORD_BOT_TOKEN=
GUILD_ID=
23 changes: 14 additions & 9 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
# Schej.it API

API docs (available when the server is running): http://localhost:3002/swagger/index.html
Swagger (when running): http://localhost:3002/swagger/index.html

## Debug
## Quick start
- Prereqs: MongoDB, Go 1.20+
- Env: copy `.env.example` to `.env` and fill required keys; optional integrations can stay blank for local dev
- Live reload: `go install github.com/cosmtrek/[email protected]`
- Run: `air` (or `go run main.go`)
- Mongo URI: `mongodb://localhost:27017/schej-it` (or `mongodb://mongo:27017/schej-it` in docker-compose)

- Install mongodb
- Install `air`, a package that facilitates live reload for Go apps
- `go install github.com/cosmtrek/air@latest`
- To run the server, simply run `air` in the root directory of the server
## Seeding (optional)
- `cd scripts/seed_demo && MONGODB_URI=mongodb://localhost:27017 go run main.go` (creates demo user + event)

## Make a backup of the mongodb database
## Migrations
- Scripts in `scripts/*` are one-off data migrations. Only run them if you need that specific migration on existing data.

- Run `mongodump --host="localhost:27017" --db=schej-it` to make a backup
- Run `mongorestore --uri mongodb://localhost:27017 ./dump --drop` to restore
## Backups
- Backup: `mongodump --host="localhost:27017" --db=schej-it`
- Restore: `mongorestore --uri mongodb://localhost:27017 ./dump --drop`
33 changes: 30 additions & 3 deletions server/db/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package db

import (
"context"
"os"
"strings"
"time"

"go.mongodb.org/mongo-driver/mongo"
Expand All @@ -22,11 +24,36 @@ var FolderEventsCollection *mongo.Collection

func Init() func() {
// Establish mongodb connection
var ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
mongoURI := os.Getenv("MONGODB_URI")
if mongoURI == "" {
mongoURI = "mongodb://localhost:27017"
}
// Guard against malformed URIs (e.g., accidental extra slashes). If parsing fails, fall back to localhost.
if !strings.HasPrefix(mongoURI, "mongodb://") {
logger.StdErr.Printf("MONGODB_URI malformed (%s); falling back to mongodb://localhost:27017\n", mongoURI)
mongoURI = "mongodb://localhost:27017"
}

var (
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
err error
)
defer cancel()
Client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost"))

// Try primary URI, then fall back to localhost if the host "mongo" is unreachable (common when running API outside compose).
connectURI := mongoURI
Client, err = mongo.Connect(ctx, options.Client().ApplyURI(connectURI))
if err != nil {
logger.StdErr.Panicln(err)
logger.StdErr.Printf("failed to connect to Mongo at %s: %v\n", connectURI, err)
// Always try a localhost fallback on any error (covers parse errors or host resolution failures)
fallback := "mongodb://localhost:27017"
if connectURI != fallback {
logger.StdErr.Printf("retrying Mongo connection with fallback %s\n", fallback)
Client, err = mongo.Connect(ctx, options.Client().ApplyURI(fallback))
}
if err != nil {
logger.StdErr.Panicln(err)
}
}

// Define mongodb database + collections
Expand Down
34 changes: 23 additions & 11 deletions server/scripts/20230812_add_calendar_accounts/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
"schej.it/server/db"
"schej.it/server/models"
"schej.it/server/utils"
)

func main() {
Expand All @@ -28,17 +29,21 @@ func main() {
if user.CalendarAccounts == nil {
user.CalendarAccounts = make(map[string]models.CalendarAccount)
}
if _, ok := user.CalendarAccounts[user.Email]; !ok {
user.CalendarAccounts[user.Email] = models.CalendarAccount{
Email: user.Email,
Picture: user.Picture,
Enabled: &[]bool{true}[0], // Workaround to pass a boolean pointer

AccessToken: user.AccessToken,
AccessTokenExpireDate: user.AccessTokenExpireDate,
RefreshToken: user.RefreshToken,
accountKey := utils.GetCalendarAccountKey(user.Email, models.GoogleCalendarType)
if _, ok := user.CalendarAccounts[accountKey]; !ok {
user.CalendarAccounts[accountKey] = models.CalendarAccount{
CalendarType: models.GoogleCalendarType,
Email: user.Email,
Picture: user.Picture,
Enabled: utils.TruePtr(),
OAuth2CalendarAuth: &models.OAuth2CalendarAuth{
AccessToken: user.AccessToken,
AccessTokenExpireDate: user.AccessTokenExpireDate,
RefreshToken: user.RefreshToken,
},
}
_, err := db.UsersCollection.UpdateByID(context.Background(), user.Id, bson.M{
updates := bson.M{
"$set": bson.M{
"calendarAccounts": user.CalendarAccounts,
},
Expand All @@ -47,8 +52,12 @@ func main() {
"accessTokenExpireDate": "",
"refreshToken": "",
},
})
if err != nil {
}
if user.PrimaryAccountKey == nil {
updates["$set"].(bson.M)["primaryAccountKey"] = accountKey
}

if _, err := db.UsersCollection.UpdateByID(context.Background(), user.Id, updates); err != nil {
log.Fatal(err)
}
}
Expand All @@ -69,6 +78,9 @@ type OldUser struct {
// additional accounts the user wants to see google calendar events for
CalendarAccounts map[string]models.CalendarAccount `json:"calendarAccounts" bson:"calendarAccounts,omitempty"`

// Primary account key (may be missing in old data)
PrimaryAccountKey *string `json:"primaryAccountKey" bson:"primaryAccountKey,omitempty"`

// Google OAuth stuff
TokenOrigin models.TokenOriginType `json:"-" bson:"tokenOrigin,omitempty"`
AccessToken string `json:"-" bson:"accessToken,omitempty"`
Expand Down
Loading