Skip to content

Commit

Permalink
Merge pull request #112 from dipamsen/user-auth
Browse files Browse the repository at this point in the history
Add single user auth, improve documentation
  • Loading branch information
harshkhandeparkar authored Jan 20, 2025
2 parents d601757 + 19110af commit db5b2be
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 15 deletions.
75 changes: 69 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
- [About The Project](#about-the-project)
- [Development](#development)
- [Deployment](#deployment)
- [Backend](#backend)
- [Environment Variables](#environment-variables)
- [Contact](#contact)
- [Maintainer(s)](#maintainers)
Expand All @@ -45,6 +46,7 @@
</details>

## About The Project

<div align="center">
<img width="60%" alt="image" src="./frontend/public/banner.png">
</div>
Expand All @@ -56,21 +58,75 @@ IQPS was originally created by [Shubham Mishra](https://github.com/grapheo12) in
> Currently in active development. Get involved at our [Slack](https://slack.metakgp.org/).
## Development

1. Clone this repository.
2. For starting the backend:
- Change directory to backend `cd backend`
- Make the env file by copying the template: `cp .env.template .env`
- Fill the env variable and set `DB_HOST=localhost` for running locally for development
- Start the DB by running `docker compose -f docker-compose.dev.yaml up -d`
- Start the Rust backend by running `cargo run .`
- Change directory to backend `cd backend`
- Make the env file by copying the template: `cp .env.template .env`
- Fill the env variables and set `DB_HOST=localhost` for running locally for development (see [Environment Variables](#environment-variables)). Make sure to create corresponding folders for `STATIC_FILE_STORAGE_LOCATION`, `UPLOADED_QPS_PATH`, and `LIBRARY_QPS_PATH`.
- Set up the database (see [Database](#database))
- Start the Rust backend by running `cargo run .`
3. Set up the frontend by running `pnpm install` and then `pnpm start` in the `frontend/` directory.
4. Profit.

### Database

To initialise the database for the first time:

1. Set environment variables for Postgres in the `.env` file.
2. Start the database by running `docker compose -f docker-compose.dev.yaml up -d`.
3. Initialise the database:
- Open a shell in the docker container by running `docker compose -f docker-compose.dev.yaml exec database-dev bash`.
- Connect to the database by running `psql -U $POSTGRES_USER -d $POSTGRES_DB`.
- Run the queries in `INIT_DB` in [`backend/src/db/queries.rs`](./backend/src/db/queries.rs) to initialise the database.

To run the pre-initialised database:

1. Start the database by running `docker compose -f docker-compose.dev.yaml up -d`.

For Production:

1. Set environment variables for Postgres in the `.env` file.
2. Start the database by running `docker compose -f docker-compose.yaml up -d`.
3. Initialise the database:
- Open a shell in the docker container by running `docker compose -f docker-compose.yaml exec iqps-backend bash`.
- Connect to the database by running `psql -U $POSTGRES_USER -d $POSTGRES_DB`.
- Run the queries in `INIT_DB` in [`backend/src/db/queries.rs`](./backend/src/db/queries.rs) to initialise the database.

### Authentication

IQPS uses GitHub OAuth for authentication to the `/admin` page. To set up authentication:

1. Create a new OAuth app on GitHub.
- Go to https://github.com/settings/developers and create a new OAuth app.
- Set the Homepage URL to `http://localhost:5173` and Authorization callback URL to `http://localhost:5173/oauth`.
- Once created, generate a client secret. Copy the client ID and secret into the `.env` file.
2. Set the Authentication environment variables in the `.env` file.

For Production:

1. Create a new OAuth app on GitHub. (Should be from the same GitHub account as the organization)
- Go to https://github.com/settings/developers and create a new OAuth app.
- Set the Homepage URL to `<prod-url>` and Authorization callback URL to `<prod-url>/oauth`.
- Once created, generate a client secret. Add the client ID and secret to environment variables.
2. Set the Authentication environment variables.

#### OAuth Flow

- Github OAuth documentation: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps

On visiting `/admin`, if the user is not logged in, they get redirected to the GitHub OAuth page. After the user logs in, GitHub redirects them back to our `/oauth` endpoint with a code. The backend then uses this code to fetch an access token and username. The username is then checked against the allowed admins. If so, a JWT token is generated with the user's username and sent back to the frontend. The frontend then stores this token in local storage and sends it with every request to the backend. The backend verifies this token and allows access to admin functions.

A user is considered as an admin if they are a part of the team `GH_ORG_TEAM_SLUG` in `GH_ORG_NAME`, or if their username is in the `GH_ADMIN_USERNAMES` list.

### Crawler

[WIP: Steps to locally set up crawler]

## Deployment

### Backend

0. Set up [MetaPloy](https://github.com/metakgp/metaploy) **for production**.
1. Clone this repository at a convenient location such as `/deployments`.
2. `cd backend/`
Expand All @@ -79,24 +135,31 @@ IQPS was originally created by [Shubham Mishra](https://github.com/grapheo12) in
5. Optionally set up a Systemd service to start the wiki on startup or use this [deployment github workflow](./.github/workflows/deploy.yaml).

### Environment Variables

Environment variables can be set using a `.env` file. Use the `.env.template` files for reference. See `backend/src/env.rs` for more documentation and types.

#### Backend

##### Database (Postgres)

- `DB_NAME`: Database name
- `DB_HOST`: Database hostname (eg: `localhost`)
- `DB_PORT`: Database port
- `DB_USER`: Database username
- `DB_PASSWORD`: Database password

##### Authentication

- `GH_CLIENT_ID`: Client ID of the Github OAuth app.
- `GH_CLIENT_SECRET`: Client secret of the Github OAuth app.
- `GH_ORG_NAME`: The name of the Github organization of the admins.
- `GH_ORG_TEAM_SLUG`: The URL slug of the Github org team of the admins.
- `GH_ORG_ADMIN_TOKEN`: Github token of organization admin (with `read:org` scope).
- `GH_ADMIN_USERNAMES`: Comma separated list of Github usernames of the admins. (other than the org team members)
- `JWT_SECRET`: A secret key/password for JWT signing. It should be a long, random, unguessable string.

##### Configuration

- `MAX_UPLOAD_LIMIT`: Maximum number of files that can be uploaded at once.
- `LOG_LOCATION`: The path to a local logfile.
- `STATIC_FILES_URL`: The URL of the static files server. (eg: `https://static.metakgp.org`)
Expand All @@ -107,11 +170,11 @@ Environment variables can be set using a `.env` file. Use the `.env.template` fi
- `CORS_ALLOWED_ORIGINS`: A comma (,) separated list of origins to be allowed in CORS.

#### Frontend

- `VITE_BACKEND_URL`: The IQPS backend URL. Use `http://localhost:8080` in development.
- `VITE_MAX_UPLOAD_LIMIT` The maximum number of files that can be uploaded at once. (Note: This is only a client-side limit)
- `VITE_GH_OAUTH_CLIENT_ID` The Client ID of the Github OAuth app.


## Contact

<p>
Expand Down
1 change: 1 addition & 0 deletions backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ GH_CLIENT_SECRET=
GH_ORG_NAME=
GH_ORG_TEAM_SLUG=
GH_ORG_ADMIN_TOKEN=
GH_ADMIN_USERNAMES=

JWT_SECRET=

Expand Down
11 changes: 10 additions & 1 deletion backend/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ struct GithubMembershipResponse {
/// Takes a Github OAuth code and creates a JWT authentication token for the user
/// 1. Uses the OAuth code to get an access token.
/// 2. Uses the access token to get the user's username.
/// 3. Uses the username and and a admin's access token to verify whether the user is a member of the admins github team.
/// 3. Uses the username and an admin's access token to verify whether the user is a member of the admins github team, or the admin themselves.
///
/// Returns the JWT if the user is authenticated, `None` otherwise.
pub async fn authenticate_user(
Expand Down Expand Up @@ -152,6 +152,15 @@ pub async fn authenticate_user(
.context("Error parsing username API response.")?
.login;

// Check if the user is in the admin list
if env_vars
.gh_admin_usernames
.split(",")
.any(|x| x == username)
{
return Ok(Some(generate_token(&username, env_vars).await?));
}

// Check the user's membership in the team
println!(
"https://api.github.com/orgs/{}/teams/{}/memberships/{}",
Expand Down
2 changes: 2 additions & 0 deletions backend/src/db/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ CREATE TABLE IF NOT EXISTS iqps (
from_library BOOLEAN DEFAULT FALSE,
upload_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
approve_status BOOLEAN DEFAULT FALSE,
approved_by TEXT DEFAULT '',
is_deleted BOOLEAN DEFAULT FALSE,
fts_course_details tsvector GENERATED ALWAYS AS (to_tsvector('english', course_code || ' ' || course_name)) stored
);
CREATE INDEX IF NOT EXISTS iqps_fts ON iqps USING gin (fts_course_details);
Expand Down
19 changes: 11 additions & 8 deletions backend/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,23 @@ pub struct EnvVars {
/// OAuth app client id (public token)
pub gh_client_id: String,
#[arg(env)]
/// An org admin's Github token (with the `read:org` permission)
pub gh_org_admin_token: String,
#[arg(env)]
/// JWT encryption secret (make it a long, randomized string)
jwt_secret: String,
#[arg(env)]
/// OAuth app client secret
pub gh_client_secret: String,
#[arg(env)]
#[arg(env, default_value = "")]
/// Github organization name
pub gh_org_name: String,
#[arg(env)]
#[arg(env, default_value = "")]
/// Github organization team slug (this team has access to admin dashboard)
pub gh_org_team_slug: String,
#[arg(env)]
/// An org admin's Github token (with the `read:org` permission)
pub gh_org_admin_token: String,
#[arg(env)]
/// JWT encryption secret (make it a long, randomized string)
jwt_secret: String,
#[arg(env, default_value = "")]
/// The usernames of the admins (additional to org team members, comma separated)
pub gh_admin_usernames: String,

// Other configs
#[arg(env, default_value = "10")]
Expand Down

0 comments on commit db5b2be

Please sign in to comment.