This is a prototype for an application for everyday tasks concerning life in the university choir of TU Wien.
The main feature is a choir admin dashboard. It includes the following:
- Event overview (choir practice, concerts, etc.)
- The calendar is automatically synced daily with the TU Wien Google calendar
- If needed, users can also trigger sync from the UI
- Attendance management for events (e.g. to keep track of how often each choir member attends choir practice)
- Choir member management
- Import members (name and voice group) from CSV
- 🚧 WIP: Choir repertoire management (list of songs, linked files, etc.)
- Stats page (choir members, attendances, members by voice group, etc.)
- Admin account managment
- Admin accounts with email and password
- Invite system for new admins: existing admin users can create invite links for new admins (custom account creation links with invite token, tied to specific email)
I plan to add public-facing sites for choir members as well. Currently, only a page for the current program is available. It includes a list of songs and links to the corresponding files (sheet music, lyrics, etc.).
Go to the this site and log in with test.admin@example.com
and password asdfasdf
.
- Make sure Node and yarn are installed on the machine. On Ubuntu, this should be as simple as running
sudo apt update && sudo apt install nodejs && sudo apt install yarn
. Check online what the equivalent commands for Mac/Windows are. Note: Prisma apparently crashes on Debian systems when using Node 18 and PostgreSQL (prisma/prisma#10649). You can use the Node Version Manager to install another version of Node and switch to it for this project. - Run
yarn
. This will install all the Node modules used by this NextJS project.
- Make sure postgres is installed and running (
psql
command works - this guide might help) - Run
psql
(orsudo -u postgres psql
if that doesn't work) to login into the "postgres console" (not sure what proper term for it is) - In the postgres console, run the following SQL commands (replacing
yourpass
with a password of your choice):This will create aCREATE DATABASE choir_manager; CREATE USER choir_admin WITH PASSWORD 'yourpass'; GRANT ALL PRIVILEGES ON DATABASE choir_manager TO choir_admin;
choir_manager
database and a database userchoir_admin
that essentially has "super-user" access to the database. NOTE: I am not entirely sure about the security implications of not having an encrypted password. I guess it should be fine as the user is only "reachable" locally anyway? - Now, the database should be accessible via the connection string
postgresql://choir_admin:yourpass@localhost/choir_manager
, replacingyourpass
with the password you chose earlier. More details on how connection URIs work can be found here.
I currently I use Supabase's postgres cloud DB. Check out the Supabase docs or other sources on the web for details on how to set things up.
Prisma is an ORM for Node.js and Typescript. It is used to interact with the database from the application code. It also features a typesafe schema, which is used to generate a client that maps entities to DB tables and relations and can be used to interact with the database.
To set up Prisma, follow these steps:
- Create a
.env
file in the root folder of this project, containing a single line:whereDATABASE_URL=<connection-string> NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=<some-secret-string>
<connection-string>
is the connection DB connection string from the previous step.NEXTAUTH_URL
andNEXTAUTH_SECRET
are needed bynextauth
(for details click here). Replace<some-secret-string>
with a random string generated by runningopenssl rand -base64 32
on the command line. - Run
npx prisma db push
. This will use Prisma, an ORM for Node.js and Typescript, to sync the created database with the database schema defined inprisma/schema.prisma
. As the database will be empty before we run this command, it essentially just creates all the tables mentioned in the schema. Then, runnpx prisma generate
to create the Prisma client, which is used to interact with the database from the application code. The client will then have access to all the entities of the schema (mapped from DB tables and columns). - (Optional) Run
npx prisma studio
. This will launch Prisma Studio (Prisma's "database explorer") on your machine and make the GUI available via your browser (per default onlocalhost:5555
). You can use it to check out the models used in the database - no records will exist at this point.
I don't like the Prisma client interface. I find the syntax quite hard to learn (especially when querying/updating/deleting relations) and too different from regular SQL. Also, Prisma does NOT use SQL joins when combining data from different tables. Tbh, this isn't that much of a problem for this project. However, I still wanted to try something different. Hence, I went with drizzle, a lightweight ORM that uses SQL-like syntax and allows for joins. It's still rapidly evolving but I like the API a lot. It is used by the newer parts of this app. Some stuff, like the authentication logic builds on Prisma though, so migrating away from Prisma completely is not an option atm.
Similar to Prisma, drizzle also features a typesafe schema. However, instead of generating a client from the CLI, we generate a schema. Tables, relations etc. from this schema can then be imported in any file that interacts with the DB. The schema is generated from the DB schema using drizzle-kit
's introspect
command.
To configure drizzle, create a file called drizzle.config.ts
in the root folder of this project (check out the drizzle.config.example.ts
file for reference). Then you should be able to run npx drizzle-kit introspect:pg
to generate the drizzle client schema file.
In the env
file mentioned above, you also need to add other environment variables:
INVITE_TOKEN_SECRET=<another-secret-string>
The INVITE_TOKEN_SECRET
is needed for generating invite tokens for new application users (admin accounts). Replace <another-secret-string>
with another random string generated by running openssl rand -base64 32
.
GOOGLE_API_KEY=<your-api-key>
The GOOGLE_API_KEY
is needed to allow the applcation to use the Google Calendar API for syncing the choir practice events in the database with the choir practice events of the TU Wien Chor public calendar. You need to generate it in the Google Developer console. Once you have it, replace <your-api-key>
with the key you obtained. For details on how to obtain your key, see here.
Now that we've installed the dependencies and the database stuff, environmnent variables etc. are set up, the application is ready. Type yarn dev
to start it.
Then, navigate to localhost:3000/register
to create a user with a name, email and password of your choice. The credentials will be stored in the database (passwords are encrypted/hashed using bcrypt
, so they should be quite safe, even if the database were compromised).
After registering, you should be logged in and see an admin page. All other admin users that try to register will need an invitation link. To generate those, you can click on 'Invites'. There, you will see an overview page for all invite links that have been generated (there will be none, initially). To generate a new one, just click the button on that page. You can send the generated link to someone you would like to add as a user.
The application will look weird in its initial state, as neither events nor songs or choir members will exist yet.
You can add choir members from the 'Mitglieder' page in the admin dashboard. To get started with some fake choir members quickly, upload the file located in mock-data/fake_choir_members.csv
by dragging it into the designated area and clicking the 'Bestätigen' button. This will add all the choir members from the file to the database.
Similarly, songs can be imported from a text file on the 'Lieder' page. However, this time a simple file with one song title per line is expected. You can find an example file in mock-data/songs.txt
.
Events are synced from the choirs' public Google calendar. Go to the 'Termine' page and click the button that says 'Mit Google Kalender synchronisieren'. This will fetch all events from the calendar and add them to the database. Note that this will only work if you have set up the GOOGLE_API_KEY
environment variable as described above.
The Prisma schema file prisma/schema.prisma
is the source of truth for the database schema (and hence the whole 'data model' of this application). It is used to generate the Prisma client and sync the database with the schema. The drizzle DB client also relies on it indirectly: its schema is generated from the DB schema using drizzle-kit
's introspect
command. So, whenever you change the Prisma schema, you need to
- run
npx prisma db push
to sync the "push" the changes in the Prisma schema to the DB (updating its schema), - run
npx prisma generate
to update the Prisma client, and - run
npx drizzle-kit introspect:pg
to update the drizzle client schema from the updated DB schema.
Conveniently, there's a script in package.json
that does all of that: update-schema-and-clients
. So, whenever you change the Prisma schema, just run yarn update-schema-and-clients
and you should be good to go.
Note: because I had an issue with the infer mode for timestamp columns being set to mode: 'string'
in the introspection script (see this GitHub issue), I had to manually remove all instances of that string from the output schema file with sed -i '' \"s/mode: 'string'//g\" drizzle/schema.ts
(to make replacement work on Mac - this might not work on Linux, and surely doesn't on Windows - find an equivalent if needed).
This is an ER Diagram I created from the public
DB schema of the database (which is the schema used by the core application; Prisma, Supabase etc. create other schemas in the DB). It was created with DBeaver. Check this StackExchange discussion for details on the notation/symbols used in the diagram.
This project is very much a playground for me to learn how to make things happen. I was still figuring things out as I added features. As a consequence of this, some of this code is a bit of a sh*tshow. Here I keep track of and prioritize stuff, so that I could fix it if I were to find the time to work on this.
- More control about admin accounts (changing passwords, emails, names, deleting accounts, etc.)
- cool new tech
- use drizzle instead of Prisma for DB access in new parts of the app
- use tRPC routers instead of Next.js API routes in new parts of the app
- use new Next.js 13
app
directory instead ofpages
directory - full transition from
pages/api
to tRPC routers - transition from Prisma to drizzle
- code smells and changes required to reduce them
- unify data fetching (some routes use getStaticProps, some getServerSideProps, some fetch on client side - even that is done with different approaches lol)
- refactor ugly backend stats collection code to just use SQL lol
- find a simpler and more bullet-proof way to handle authentication (or rather, actually understand what nextauth does lol)
- UI
- redesign (really not that beautiful)
- replace Material UI with more lightweight custom components