A modified version of Wafrn used on https://wf.jbc.lol (mirror of https://git.jbc.lol/jbcrn/wf.jbc.lol which is a mirror of https://codeberg.org/jbcarreon123/wf.jbc.lol)

add documentation and fix environment variables

+99 -53
+8
.env.example
··· 53 53 POSTGRES_METRICS_PASSWORD= 54 54 POSTGRES_METRICS_DBNAME=pgwatch_metrics 55 55 GF_SECURITY_ADMIN_PASSWORD= 56 + 57 + # OpenID Connect auth 58 + # Only do this if you know what you are doing! See docs/openid.md for more info 59 + OIDC_ENABLED=false 60 + OIDC_ISSUER=https://auth.localhost 61 + OIDC_CLIENT_ID=wafrn 62 + OIDC_CLIENT_SECRET=secret 63 + OIDC_AUTH_NAME=OpenID
+37
docs/openid.md
··· 1 + # Setting up OpenID Connect (OIDC) authentication 2 + 3 + If you are handling multiple services aside from Wafrn, you can implement something called an identity provider (or an IdP) which is a service that provides a central account to log in on multiple services. 4 + 5 + Linking it on Wafrn is possible, if your provider supports the OpenID Connect specification. Implementing this shouldn't have any breaking changes on your instance. Signing up using OpenID is not implemented due to security issues, but it will prefill your email and username on the register screen. 6 + 7 + Here's how you can implement OIDC auth on your instance: 8 + 9 + 1. Create a authentication client on your identity provider of choice. I will use Keycloak on this guide but you can use anything else (like Authentik, Authelia, etc). 10 + 11 + 1. Set your client type to OpenID Connect (if possible) 12 + 13 + 2. Set the Client ID to anything you want. Take note of that ID, we will need that later. 14 + 15 + 3. Set the client name to 'Wafrn'. You can change that if you like, that will be the name shown if someone authenticates to your Wafrn instance for the first time. Optionally, set the description of it. 16 + 17 + 4. Enable both client authentication and authorization, and don't touch anything else. 18 + 19 + 5. Set the root URL to your Wafrn instance's homepage (e.g. `https://app.wafrn.net/`), and set the home URL with the same thing. 20 + 21 + 6. Set the redirect URL to `https://wafrn.example/api/login/oidc/callback*`. 22 + 23 + 7. Copy the provided Client secret. We will need that. 24 + 25 + 2. In your .env file, edit these values: 26 + 27 + - `OIDC_ENABLED` to `true` 28 + 29 + - `OIDC_ISSUER` to your IdP's issuer URL 30 + 31 + - `OIDC_CLIENT_ID` is your specified client ID 32 + 33 + - `OIDC_CLIENT_SECRET` to the provided client secret 34 + 35 + - `OIDC_AUTH_NAME` to the name you want to call your auth provider 36 + 37 + 3. Now, restart the container. To take effect easily, you should clear your browser local storage by running `localStorage.clear()` in your browser console.
+4 -5
packages/backend/environment.dev.ts
··· 124 124 shortenPosts: 3, 125 125 disablePWA: false, 126 126 maintenance: false, 127 - oidcAuthName: "OpenID", 128 - oidcEnabled: true 127 + oidcAuthName: 'OpenID', 128 + oidcEnabled: true, 129 129 }, 130 130 oidcConfig: { 131 131 enabled: true, 132 - redirectUrl: "http://localhost/login/oidc", 133 - issuer: "http://auth.localhost/", 134 - clientId: "wafrn", 132 + issuer: 'http://auth.localhost/', 133 + clientId: 'wafrn', 135 134 clientSecret: "this is a secret" 136 135 } 137 136 }
+9 -1
packages/backend/environment.example.ts
··· 120 120 externalCacheurl: '${{FRONTEND_CACHE_URL}}', 121 121 shortenPosts: ${{FRONTEND_SHORTEN_POSTS:-3}}, 122 122 disablePWA: ${{FRONTEND_DISABLE_PWA:-false}}, 123 - maintenance: ${{FRONTEND_MAINTENANCE:-false}} 123 + maintenance: ${{FRONTEND_MAINTENANCE:-false}}, 124 + oidcAuthName: '${{OIDC_AUTH_NAME:-OpenID}}', 125 + oidcEnabled: ${{OIDC_ENABLED:-false}} 126 + }, 127 + oidcConfig: { 128 + enabled: ${{OIDC_ENABLED:-false}}, 129 + issuer: '${{OIDC_ISSUER:-http://auth.localhost/}}', 130 + clientId: '${{OIDC_CLIENT_ID:-wafrn}}', 131 + clientSecret: '${{OIDC_CLIENT_SECRET:-secret}}' 124 132 } 125 133 }
+6
packages/backend/environment.local.example.ts
··· 118 118 shortenPosts: 3, 119 119 disablePWA: false, 120 120 maintenance: false 121 + }, 122 + oidcConfig: { 123 + enabled: true, 124 + issuer: 'http://auth.localhost/', 125 + clientId: 'wafrn', 126 + clientSecret: "this is a secret" 121 127 } 122 128 }
+1 -2
packages/backend/interfaces/environment.ts
··· 83 83 enabled: boolean, 84 84 issuer: string, 85 85 clientId: string, 86 - clientSecret: string, 87 - redirectUrl: string 86 + clientSecret: string 88 87 } 89 88 }
+1 -1
packages/backend/routes/users.ts
··· 677 677 }) 678 678 679 679 if (!userWithEmail) { 680 - res.redirect('/login?error=user_not_registered') 680 + res.redirect(`/register?email=${userInfo.email}&username=${userInfo.preferred_username}`) 681 681 return 682 682 } 683 683
+21 -42
packages/frontend/src/app/pages/register/register.component.html
··· 1 1 <mat-card class="pb-3 mb-6 lg:mx-4 mat-card-higher wafrn-container"> 2 2 <mat-card-header class="pb-3 wafrn-container-header"> 3 - <mat-card-title class="flex gap-2" 4 - ><fa-icon [icon]="faUserPlus"></fa-icon 5 - ><span class="font-medium">{{ 'register.title' | translate }}</span></mat-card-title 6 - > 3 + <mat-card-title class="flex gap-2"><fa-icon [icon]="faUserPlus"></fa-icon><span class="font-medium">{{ 4 + 'register.title' | translate }}</span></mat-card-title> 7 5 </mat-card-header> 8 6 <section class="my-6 mx-3 text-center"> 9 7 <h1 class="mb-0 text-5xl">{{ 'register.welcome' | translate }}</h1> 10 8 </section> 11 9 12 10 @if (manuallyReview) { 13 - <section class="mx-3 mb-4"> 14 - <app-info-card type="info">{{ 'register.manualReviewInfo' | translate }}</app-info-card> 15 - </section> 11 + <section class="mx-3 mb-4"> 12 + <app-info-card type="info">{{ 'register.manualReviewInfo' | translate }}</app-info-card> 13 + </section> 16 14 } 17 15 18 16 <form class="px-3" [formGroup]="loginForm" (ngSubmit)="onSubmit()"> 19 17 <mat-form-field class="w-full"> 20 18 <mat-label>Email</mat-label> 21 - <input formControlName="email" type="email" matInput /> 19 + <input formControlName="email" type="email" matInput [(ngModel)]="email" /> 22 20 </mat-form-field> 23 21 24 22 <mat-form-field class="w-full"> ··· 32 30 33 31 <mat-form-field class="w-full mb-3" subscriptSizing="dynamic"> 34 32 <mat-label>Username</mat-label> 35 - <input formControlName="url" type="text" matInput /> 33 + <input formControlName="url" type="text" matInput [(ngModel)]="username" /> 36 34 <mat-hint>Right now we do not allow special characters or spaces</mat-hint> 37 35 <mat-error>Username contains invalid characters</mat-error> 38 36 </mat-form-field> ··· 45 43 46 44 <mat-form-field (click)="picker.open()" class="w-full mb-3" subscriptSizing="dynamic"> 47 45 <mat-label>Your birth date <b>(There is a minimum registration age!)</b></mat-label> 48 - <input 49 - formControlName="birthDate" 50 - matInput 51 - [min]="minDate" 52 - [max]="minimumRegistrationDate" 53 - [matDatepicker]="picker" 54 - /> 55 - <mat-hint 56 - >MM/DD/YYYY - Your birthday date is required for legal reasons in the european union and USA. Its not shared 57 - with anyone. Minimum registration age varies depending on the instance</mat-hint 58 - > 46 + <input formControlName="birthDate" matInput [min]="minDate" [max]="minimumRegistrationDate" 47 + [matDatepicker]="picker" /> 48 + <mat-hint>MM/DD/YYYY - Your birthday date is required for legal reasons in the european union and USA. Its not 49 + shared 50 + with anyone. Minimum registration age varies depending on the instance</mat-hint> 59 51 <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle> 60 52 <mat-datepicker #picker></mat-datepicker> 61 53 </mat-form-field> ··· 64 56 <mat-hint>This actually does not do anything at all</mat-hint> 65 57 <mat-select> 66 58 @for (gender of genders; track $index) { 67 - <mat-option [value]="gender" [innerHTML]="gender">{{ gender }}</mat-option> 59 + <mat-option [value]="gender" [innerHTML]="gender">{{ gender }}</mat-option> 68 60 } 69 61 </mat-select> 70 62 </mat-form-field> ··· 74 66 <button type="button" mat-stroked-button color="primary" (click)="avatarInput.click()"> 75 67 <fa-icon [icon]="faUpload" class="m-1"></fa-icon>Choose File 76 68 </button> 77 - <input 78 - #avatarInput 79 - formControlName="avatar" 80 - id="avatar" 81 - type="file" 82 - accept="image/jpeg,image/png,image/webp,image/gif" 83 - hidden 84 - (change)="imgSelected($event)" 85 - /> 69 + <input #avatarInput formControlName="avatar" id="avatar" type="file" 70 + accept="image/jpeg,image/png,image/webp,image/gif" hidden (change)="imgSelected($event)" /> 86 71 <p class="m-auto ml-1">{{ selectedFileName }}</p> 87 72 </div> 88 73 </div> 89 74 90 75 <div class="flex"> 91 - <button 92 - color="primary" 93 - mat-flat-button 94 - extended 95 - [disabled]="!loginForm.valid" 96 - class="w-full border-round-md mt-4" 97 - > 98 - <span class="flex gap-2" 99 - >{{ 'register.registerButton' | translate }} <fa-icon [icon]="submitIcon"></fa-icon 100 - ></span> 76 + <button color="primary" mat-flat-button extended [disabled]="!loginForm.valid" 77 + class="w-full border-round-md mt-4"> 78 + <span class="flex gap-2">{{ 'register.registerButton' | translate }} <fa-icon 79 + [icon]="submitIcon"></fa-icon></span> 101 80 </button> 102 81 @if (loginForm.disabled) { 103 - <mat-spinner diameter="24" class="mt-5 mx-3"></mat-spinner> 82 + <mat-spinner diameter="24" class="mt-5 mx-3"></mat-spinner> 104 83 } 105 84 </div> 106 85 </form> 107 - </mat-card> 86 + </mat-card>
+12 -2
packages/frontend/src/app/pages/register/register.component.ts
··· 5 5 6 6 import { faArrowRight, faEye, faEyeSlash, faUpload, faUserPlus } from '@fortawesome/free-solid-svg-icons' 7 7 import { EnvironmentService } from 'src/app/services/environment.service' 8 - import { Router } from '@angular/router' 8 + import { ActivatedRoute, Router } from '@angular/router' 9 9 10 10 @Component({ 11 11 selector: 'app-register', ··· 270 270 avatar: new UntypedFormControl('', []) 271 271 }) 272 272 273 + searchParams = new URLSearchParams(window.location.search) 274 + email = this.searchParams.get('email') 275 + username = this.searchParams.get('username') 276 + 273 277 constructor( 274 278 private loginService: LoginService, 275 279 private messages: MessageService, 276 - private router: Router 280 + private router: Router, 281 + private route: ActivatedRoute 277 282 ) { 283 + this.route.queryParams.subscribe(params => { 284 + this.email = params['email'] 285 + this.username = params['username'] 286 + }) 287 + 278 288 // minimum age: 14 279 289 this.minimumRegistrationDate = new Date() 280 290 this.minimumRegistrationDate.setFullYear(this.minimumRegistrationDate.getFullYear() - 18)