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
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,22 @@ TL;DR
```
git clone git@github.com:eliasson/quarter.git
cd quarter
docker-compose -f docker/docker-compose.yaml up

# Start PostgreSQL
docker compose -f tools/docker/docker-compose.yaml up -d

# Build the frontend
cd webapp
npm ci
gleam build
npm run build
cd ..

# Symlink dist so the backend can find the frontend assets
ln -s ../../dist service/src/Quarter/dist

# Build and run the backend
cd service
dotnet build
dotnet run --project src/Quarter
```
Expand Down
4 changes: 2 additions & 2 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ By default Quarter is running in local mode. This is ideal for:

When running local mode a fix user will be used named _User_ and a static ID (`47ba567a-711e-4c4a-a7b0-07756d965a79`).

To run in local mode, set `Application.LocalMode` to `true`.
To run in local mode, set `LocalMode` to `true` in `service/src/Quarter/appsettings.json`.

### Identity providers

Expand All @@ -16,7 +16,7 @@ The identity provider is only used to authenticate the users. That will not gain
to Quarter. In order to do that they must be added as local users with a matching e-mail
address (this is done int the admin UI).

To run using an identity provider, set `Application.LocalMode` to `false` and add your ID's
To run using an identity provider, set `LocalMode` to `false` and add your ID's
and secrets in the application configuration (make sure not to commit these!):

```
Expand Down
73 changes: 61 additions & 12 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,83 @@
Quarter is built using .NET Core as backend and a Gleam based front-end using Gleam.

## Getting started with development
## Prerequisites

Make sure you have the following tools installed:

Install .NET Core from [here](https://dotnet.microsoft.com/download)
- [.NET SDK 9](https://dotnet.microsoft.com/download)
- [Node.js 20](https://nodejs.org/) (npm is included)
- [Gleam 1.14+](https://gleam.run/getting-started/installing/)
- [Erlang/OTP 28+](https://gleam.run/getting-started/installing/) (required by Gleam)
- [Docker](https://docs.docker.com/get-docker/) (for the PostgreSQL database)

## Getting started with development

1. Clone this repository:

`git clone https://github.com/eliasson/quarter.git`

2. Create a copy of the settings for you to customize:

`cp src/Quarter/appsettings.json src/Quarter/appsettings.Development.json`
`cp service/src/Quarter/appsettings.json service/src/Quarter/appsettings.Development.json`

By default Quarter will run in local mode, which is useful for development. See [authentication.md](authentication.md) for
details on how to setup external Identity Providers.

3. Start a PostgreSQL database

Start an instance of the PostgreSQL database using docker-compose.
Start an instance of the PostgreSQL database using Docker Compose.

Run `docker compose -f tools/docker/docker-compose.yaml up -d`

_The first time this is run it will execute the `tools/docker/init.sql` script to setup the local database
and privileges. All tables are created using Fluent Migrations at `service/src/Quarter.Core/Migrations`._

4. Build the frontend

The backend serves static files from a `dist/` directory at the repository root. You need to build the
frontend before starting the backend.

From the `webapp/` directory:

```
npm ci
gleam build
npm run build
```

Run `docker-compose -f docker/docker-compose.yaml up`
This compiles the Gleam code, bundles it with Vite, and outputs the result to `dist/` in the repository root.

_The first time this is run it will execute the `docker/init.sql` script to setup the local database
and privileges. All tables are created using Fluent Migrations at `src/Quarter.Core/Migrations`
During development you can use `npm run watch` (from `webapp/`) to automatically rebuild on changes.

4. Build and run from the root directory:
5. Symlink the dist directory

- `dotnet build` - download all libraries needed
- `dotnet test` - runs unit-test for all projects
The backend is configured to serve static files from `./dist` relative to where it runs
(`service/src/Quarter/`), but the frontend build outputs to `dist/` at the repository root.
You need to create a symlink so the backend can find the built frontend assets:

From the repository root:

```
ln -s ../../dist service/src/Quarter/dist
```

6. Build and run the backend

From the `service/` directory:

- `dotnet build` - download all libraries needed and build the solution
- `dotnet test` - runs unit tests for all projects
- `dotnet run --project src/Quarter` - starts the service at localhost

Run `open https://localhost:5001/` go to **Manage** section and create your first project
some activities. Then head to **Timesheet** and start register time!
7. Open the application

Run `open https://localhost:5001/`, go to **Manage** section and create your first project and
some activities. Then head to **Timesheet** and start registering time!

## Configuration

By default Quarter will run in local mode, which is useful for development. The configuration
is located at `service/src/Quarter/appsettings.json` with development overrides in
`service/src/Quarter/appsettings.Development.json`.

See [authentication.md](authentication.md) for details on how to setup external Identity Providers.
5 changes: 3 additions & 2 deletions service/src/Quarter/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ public void ConfigureServices(IServiceCollection services)

services.AddRouting();
services.AddRazorPages();
services.AddControllers(o => o.EnableEndpointRouting = false);
services.AddControllers();
services.AddAuthorization();
services.AddHttpContextAccessor();
services.RegisterStartupTasks();
var localMode = Configuration.GetValue<bool>("LocalMode");
services.RegisterStartupTasks(localMode);
}

private void ConfigureAuth(IServiceCollection services)
Expand Down
35 changes: 35 additions & 0 deletions service/src/Quarter/StartupTasks/LocalUserStartupTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Quarter.Auth;
using Quarter.Core.Exceptions;
using Quarter.Core.Repositories;

namespace Quarter.StartupTasks;

/// <summary>
/// Ensures the hardcoded local mode user exists in the database at startup.
/// Without this, every API request in local mode fails because the authentication
/// handler provides a user ID that does not exist in the database.
/// </summary>
public class LocalUserStartupTask(
ILogger<LocalUserStartupTask> logger,
IRepositoryFactory repositoryFactory)
: IStartupTask
{
private readonly IUserRepository _userRepository = repositoryFactory.UserRepository();

public async Task ExecuteAsync()
{
try
{
await _userRepository.GetByIdAsync(LocalUser.UserId, CancellationToken.None);
logger.LogInformation("Local mode user already exists");
}
catch (NotFoundException)
{
await _userRepository.CreateAsync(LocalUser.User, CancellationToken.None);
logger.LogInformation("Created local mode user {Email}", LocalUser.User.Email);
}
}
}
5 changes: 4 additions & 1 deletion service/src/Quarter/StartupTasks/StartupTaskConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ namespace Quarter.StartupTasks;

public static class StartupTaskConfiguration
{
public static void RegisterStartupTasks(this IServiceCollection serviceCollection)
public static void RegisterStartupTasks(this IServiceCollection serviceCollection, bool localMode)
{
serviceCollection.AddTransient<IStartupTask, InitialUserStartupTask>();

if (localMode)
serviceCollection.AddTransient<IStartupTask, LocalUserStartupTask>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class StartupTaskConfigurationTest
[Test]
public void ItShouldHaveRegisteredStartupTasks()
{
var provider = CreateServiceProvider();
var provider = CreateServiceProvider(localMode: false);
var startupTasks = provider.GetServices<IStartupTask>()
.Select(t => t.GetType());
Assert.That(startupTasks, Is.EquivalentTo(new[]
Expand All @@ -22,12 +22,25 @@ public void ItShouldHaveRegisteredStartupTasks()
}));
}

private static ServiceProvider CreateServiceProvider()
[Test]
public void ItShouldRegisterLocalUserStartupTaskInLocalMode()
{
var provider = CreateServiceProvider(localMode: true);
var startupTasks = provider.GetServices<IStartupTask>()
.Select(t => t.GetType());
Assert.That(startupTasks, Is.EquivalentTo(new[]
{
typeof(InitialUserStartupTask),
typeof(LocalUserStartupTask)
}));
}

private static ServiceProvider CreateServiceProvider(bool localMode)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddLogging();
serviceCollection.UseQuarterUnderTest();
serviceCollection.RegisterStartupTasks();
serviceCollection.RegisterStartupTasks(localMode);
return serviceCollection.BuildServiceProvider();
}
}