Skip to content
Merged
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
4 changes: 0 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
CLIENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
TENANT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
APP_URL="http://localhost:3000"
BACKEND_URL="http://localhost:3002"
API_URI="api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/access_as_user"
STUDENT_USERNAME_PATTERN="@edu"
DEFAULT_TEST_USER="[email protected]"
3 changes: 0 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ jobs:
cache: yarn
- name: Install dependencies and build website
env:
CLIENT_ID: ${{ secrets.CLIENT_ID }}
TENANT_ID: ${{ secrets.TENANT_ID }}
API_URI: ${{ secrets.API_URI }}
APP_URL: ${{ secrets.APP_URL }}
BACKEND_URL: ${{ secrets.BACKEND_URL }}
GH_OAUTH_CLIENT_ID: ${{ vars.GH_OAUTH_CLIENT_ID}}
Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
| :------------------------- | :------------- | :---------------------------------- | :------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APP_URL` | Production | `http://localhost:3000` | | Domain of the hosted app |
| `BACKEND_URL` | Production | `http://localhost:3002` | | Url of the API Endpoint |
| `CLIENT_ID` | Production | | | Azure ID: Client ID |
| `TENANT_ID` | Production | | | Azure AD: Tenant Id |
| `API_URI` | Production | | | Azure AD: API Url |
| `STUDENT_USERNAME_PATTERN` | Production | | `@edu` | Users with usernames matching this RegExp pattern are displayed as students (regardless of admin status). If unset, all non-admin users are displayed as students. |
| `DEFAULT_TEST_USER` | Development | | `[email protected]` | To log in offline. Email of the user to be selected by default. Must correspond to a user email found in the API's database.\* |
| `OFFLINE_API` | Dev/Production | `memory` | `true` | `memory` | `indexedDB` | In case the project shall be fully functional, but API persistent data is not needed (e.g. when run in Github Codespace), set this option to true (=`memory`). |
| `SENTRY_DSN` | Production | | | Sentry DSN for error tracking |
Expand Down
3 changes: 1 addition & 2 deletions docs/tdev/app-architecture/api-deploy/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ page_id: 560cfd4d-4464-42ff-a121-a3d116ea1994
import Steps from '@tdev-components/Steps'
import { Val, TemplateCode, DynamicInput } from '@tdev-components/DynamicValues';
import _ from 'es-toolkit/compat';
import { generateRandomBase64 } from './secureToken';
import { generateRandomBase64 } from '../helpers/secureToken';

# API Aufsetzen

Expand All @@ -16,7 +16,6 @@ import { generateRandomBase64 } from './secureToken';
<DynamicInput name="app" default='inf-teaching-api' />
<DynamicInput name="domain" default='teaching-api.gbsl.website' />
<DynamicInput name="APP_NAME" derived default={(page) => _.camelCase((page.dynamicValues.get('app') || 'inf-teaching-api'))} />
<DynamicInput name="API_URI" placeholder='api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' />
<DynamicInput name="ALLOWED_ORIGINS" default='gbsl.website' />
<DynamicInput name="ALLOW_SUBDOMAINS" default='true' />
<DynamicInput name="SESSION_SECRET" monospace default='$(openssl rand -base64 32)' onRecalculate={() => generateRandomBase64()} />
Expand Down
9 changes: 5 additions & 4 deletions docs/tdev/app-architecture/dokku/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ page_id: 323ff390-40d6-4bd5-ac6c-7a05f3515526

import Steps from '@tdev-components/Steps'
import { Val, TemplateCode, DynamicInput } from '@tdev-components/DynamicValues';
import { generateRandomBase64 } from '../helpers/secureToken';

# Dokku Deploy

Expand All @@ -18,8 +19,8 @@ Durch das Hosten auf einem eigenen Server kann mit bspw. `http-auth` der Zugriff
1. Eine neue App __<Val name="app" />__ auf dem Server erstellen
<DynamicInput name="app" default='info-teaching-website' />
<DynamicInput name="domain" default='info.gbsl.website' />
<DynamicInput name="API_URI" default='api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' />
<DynamicInput name="BACKEND_URL" default='https://xxx-api.gbsl.website' />
<DynamicInput name="BETTER_AUTH_URL" default='https://xxx-api.gbsl.website' />
<DynamicInput name="BETTER_AUTH_SECRET" monospace default='$(openssl rand -base64 32)' onRecalculate={() => generateRandomBase64()} />
<DynamicInput name="CLIENT_ID" default='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' />
<DynamicInput name="TENANT_ID" default='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' />
<TemplateCode>
Expand All @@ -34,7 +35,7 @@ Durch das Hosten auf einem eigenen Server kann mit bspw. `http-auth` der Zugriff

dokku config:set --no-restart {{app}} API_URI={{API_URI}}
dokku config:set --no-restart {{app}} APP_URL=https://{{domain}}
dokku config:set --no-restart {{app}} BACKEND_URL={{BACKEND_URL}}
dokku config:set --no-restart {{app}} BETTER_AUTH_URL={{BETTER_AUTH_URL}}
dokku config:set --no-restart {{app}} CLIENT_ID={{CLIENT_ID}}
dokku config:set --no-restart {{app}} TENANT_ID={{TENANT_ID}}
```
Expand All @@ -44,7 +45,7 @@ Durch das Hosten auf einem eigenen Server kann mit bspw. `http-auth` der Zugriff
```bash title="/home/dokku/{{app}}/ENV"
API_URI="{{API_URI}}"
APP_URL="https://{{domain}}"
BACKEND_URL="{{BACKEND_URL}}"
BETTER_AUTH_URL="{{BETTER_AUTH_URL}}"
CLIENT_ID="{{CLIENT_ID}}"
TENANT_ID="{{TENANT_ID}}"
NGINX_ROOT="build"
Expand Down
8 changes: 0 additions & 8 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ const config: Config = applyTransformers({
/** Use test user in local dev: set DEFAULT_TEST_USER to the default test users email adress*/
TEST_USER: DEFAULT_TEST_USER,
OFFLINE_API: OFFLINE_API,
/** User.ts#isStudent returns `true` for users matching this pattern. If unset, it returns `true` for all non-admin users. */
STUDENT_USERNAME_PATTERN: process.env.STUDENT_USERNAME_PATTERN,
NO_AUTH: (process.env.NODE_ENV !== 'production' || OFFLINE_API) && !!DEFAULT_TEST_USER,
/** The Domain Name where the api is running */
APP_URL: process.env.NETLIFY
Expand All @@ -82,12 +80,6 @@ const config: Config = applyTransformers({
: process.env.APP_URL || 'http://localhost:3000',
/** The Domain Name of this app */
BACKEND_URL: process.env.BACKEND_URL || 'http://localhost:3002',
/** The application id generated in https://portal.azure.com */
CLIENT_ID: process.env.CLIENT_ID,
/** Tenant / Verzeichnis-ID (Mandant) */
TENANT_ID: process.env.TENANT_ID,
/** The application id uri generated in https://portal.azure.com */
API_URI: process.env.API_URI,
GIT_COMMIT_SHA: GIT_COMMIT_SHA,
SENTRY_DSN: process.env.SENTRY_DSN,
GH_OAUTH_CLIENT_ID: GH_OAUTH_CLIENT_ID,
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
"updateTdev": "ts-node updateSync/updateTdev.ts"
},
"dependencies": {
"@azure/msal-browser": "^v3.28.0",
"@azure/msal-react": "^2.2.0",
"@docusaurus/core": "^3.8.1",
"@docusaurus/faster": "^3.8.1",
"@docusaurus/preset-classic": "^3.8.1",
Expand All @@ -49,6 +47,7 @@
"@sentry/webpack-plugin": "^3.3.1",
"ace-builds": "^1.41.0",
"axios": "^1.9.0",
"better-auth": "^1.3.26",
"browser-image-compression": "^2.0.2",
"clsx": "^2.1.1",
"docusaurus-plugin-sass": "^0.2.6",
Expand Down
15 changes: 9 additions & 6 deletions src/api/OfflineApi/Adapter/MemoryDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ class MemoryDbAdapter implements DbAdapter {

async getAll<T>(storeName: string): Promise<T[]> {
if (!this.db[storeName]) {
return [];
return Promise.resolve([]);
}
return Object.values(this.db[storeName]) as T[];
return Promise.resolve(Object.values(this.db[storeName]) as T[]);
}

async byDocumentRootId<T extends DocumentType>(
Expand All @@ -22,31 +22,34 @@ class MemoryDbAdapter implements DbAdapter {
if (!documentRootId) {
return Promise.resolve([]);
}
return this.filter('documents', (doc) => doc.documentRootId === documentRootId) as Promise<
Document<T>[]
>;
return Promise.resolve(
this.filter('documents', (doc) => doc.documentRootId === documentRootId) as Promise<Document<T>[]>
);
}

async put<T>(storeName: string, item: T & { id: string }): Promise<void> {
if (!this.db[storeName]) {
this.db[storeName] = {};
}
this.db[storeName][item.id] = item;
return Promise.resolve();
}

async delete(storeName: string, id: string): Promise<void> {
if (this.db[storeName] && this.db[storeName][id]) {
delete this.db[storeName][id];
}
return Promise.resolve();
}
async filter<T>(storeName: string, filterFn: (item: T) => boolean): Promise<T[]> {
const allItems = await this.getAll<T>(storeName);
return allItems.filter(filterFn);
return Promise.resolve(allItems.filter(filterFn));
}

async destroyDb(): Promise<void> {
this.db = {};
console.log('MemoryDbAdapter: Database destroyed');
return Promise.resolve();
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/api/OfflineApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const LOG_REQUESTS = false;
let OfflineUser: User = {
id: 'c23c0238-4aeb-457f-9a2c-3d2d5d8931c0',
email: '[email protected]',
name: 'Offline User',
firstName: 'Offline',
lastName: 'User',
role: process.env.NODE_ENV === 'production' ? Role.STUDENT : Role.ADMIN,
Expand All @@ -29,6 +30,7 @@ const DB_NAME = `${siteConfig.organizationName ?? 'gbsl'}-${siteConfig.projectNa
const DOCUMENTS_STORE = 'documents';
const STUDENT_GROUPS_STORE = 'studentGroups';
const PERMISSIONS_STORE = 'permissions';
export const getOfflineUser = () => ({ ...OfflineUser });

const resolveResponse = <T>(data: T, statusCode: number = 200, statusText: string = ''): AxiosPromise<T> => {
return Promise.resolve({
Expand Down Expand Up @@ -90,6 +92,7 @@ export default class OfflineApi {
this.dbAdapter = new MemoryDbAdapter();
}
} else {
console.log('setup memory adapter');
this.dbAdapter = new MemoryDbAdapter();
}
if (LOG_REQUESTS) {
Expand Down Expand Up @@ -236,8 +239,6 @@ export default class OfflineApi {
return resolveResponse([] as unknown as T);
case 'allowedActions':
return resolveResponse([] as unknown as T);
case 'checklogin':
return resolveResponse({ user: OfflineUser } as unknown as T, 200, 'ok');
case 'documents':
if (id) {
const document = await this.dbAdapter.get<Document<any>>(DOCUMENTS_STORE, id);
Expand Down
12 changes: 12 additions & 0 deletions src/api/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@ export function createAllowedAction(
export function allowedActions(signal: AbortSignal): AxiosPromise<AllowedAction[]> {
return api.get(`/admin/allowedActions`, { signal });
}

export function linkUserPassword(
userId: string,
userPW: string,
signal: AbortSignal
): AxiosPromise<AllowedAction[]> {
return api.post(`/admin/users/${userId}/linkUserPassword`, { pw: userPW }, { signal });
}

export function revokeUserPassword(userId: string, signal: AbortSignal): AxiosPromise<AllowedAction[]> {
return api.post(`/admin/users/${userId}/revokeUserPassword`, { signal });
}
102 changes: 3 additions & 99 deletions src/api/base.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import { BACKEND_URL, apiConfig } from '../authConfig';
import { InteractionRequiredAuthError } from '@azure/msal-browser';
import { msalInstance } from '../theme/Root';
import axios, { AxiosInstance } from 'axios';
import siteConfig from '@generated/docusaurus.config';
import OfflineApi from './OfflineApi';
const { NO_AUTH, OFFLINE_API } = siteConfig.customFields as {
NO_AUTH?: boolean;
const { BACKEND_URL, OFFLINE_API } = siteConfig.customFields as {
BACKEND_URL: string;
OFFLINE_API?: boolean | 'memory' | 'indexedDB';
};

Expand All @@ -25,97 +22,4 @@ const api: AxiosInstance & { mode?: 'indexedDB' | 'memory'; destroyDb?: () => Pr
headers: {}
});

export const setupDefaultAxios = () => {
/** clear all current interceptors and set them up... */
api.interceptors.request.clear();
api.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
if (config.headers['Authorization']) {
delete config.headers['Authorization'];
}
return config;
},
(error) => {
Promise.reject(error);
}
);
};

export const setupMsalAxios = () => {
/** clear all current interceptors and set them up... */
api.interceptors.request.clear();
api.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
if (process.env.NODE_ENV !== 'production' && NO_AUTH) {
return config;
}
// This will only return a non-null value if you have logic somewhere else that calls the setActiveAccount API
// --> @see src/theme/Root.tsx
const activeAccount = msalInstance.getActiveAccount();
if (activeAccount) {
try {
const accessTokenResponse = await msalInstance.acquireTokenSilent({
scopes: apiConfig.scopes,
account: activeAccount
});
const accessToken = accessTokenResponse.accessToken;
if (config.headers && accessToken) {
config.headers['Authorization'] = 'Bearer ' + accessToken;
}
} catch (e) {
delete config.headers['Authorization'];
if (e instanceof InteractionRequiredAuthError) {
// If there are no cached tokens, or the cached tokens are expired, then the user will need to interact
// with the page to get a new token.
console.log('User interaction required to get a new token.', e);
// hacky way to get the user to log in again - only happens on firefox when
// the default "no 3dparty cookies" setting is active
const msalKeys = Object.keys(localStorage).filter((k) => k.startsWith('msal.'));
msalKeys.forEach((k) => localStorage.removeItem(k));
// proceed with the login
await msalInstance.acquireTokenRedirect({
scopes: apiConfig.scopes,
account: activeAccount
});
}
}
} else {
/*
* User is not signed in. Throw error or wait for user to login.
* Do not attempt to log a user in outside of the context of MsalProvider
*/
if (config.headers['Authorization']) {
delete config.headers['Authorization'];
}
}
return config;
},
(error) => {
Promise.reject(error);
}
);
};

export const setupNoAuthAxios = (userEmail: string) => {
if (process.env.NODE_ENV === 'production') {
return;
}
/** clear all current interceptors and set them up... */
api.interceptors.request.clear();
api.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
config.headers['Authorization'] = JSON.stringify({
email: userEmail
});
return config;
},
(error) => {
Promise.reject(error);
}
);
};
export const checkLogin = (signal: AbortSignal) => {
return api.get('/checklogin', { signal });
};

export default api;
37 changes: 34 additions & 3 deletions src/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { mdiEmailLock, mdiGithub, mdiMicrosoft } from '@mdi/js';
import api from './base';
import { AxiosPromise } from 'axios';
import { IfmColors } from '@tdev-components/shared/Colors';

export enum Role {
STUDENT = 'STUDENT',
TEACHER = 'TEACHER',
ADMIN = 'ADMIN'
STUDENT = 'student',
TEACHER = 'teacher',
ADMIN = 'admin'
}

export const RoleNames: { [key in Role]: string } = {
Expand All @@ -13,18 +15,47 @@ export const RoleNames: { [key in Role]: string } = {
[Role.ADMIN]: 'Admin'
};

export const RoleColors: { [key in Role]: string } = {
[Role.STUDENT]: 'blue',
[Role.TEACHER]: 'green',
[Role.ADMIN]: 'red'
};

export const RoleAccessLevel: { [key in Role]: number } = {
[Role.STUDENT]: 0,
[Role.TEACHER]: 1,
[Role.ADMIN]: 2
};

export enum AuthProvider {
MICROSOFT = 'microsoft',
CREDENTIAL = 'credential',
GITHUB = 'github'
}

export const AuthProviderIcons = {
[AuthProvider.MICROSOFT]: mdiMicrosoft,
[AuthProvider.CREDENTIAL]: mdiEmailLock,
[AuthProvider.GITHUB]: mdiGithub
};

export const AuthProviderColor = {
[AuthProvider.MICROSOFT]: IfmColors.blue,
[AuthProvider.CREDENTIAL]: IfmColors.info,
[AuthProvider.GITHUB]: IfmColors.black
};

export type User = {
id: string;
email: string;
name: string;
firstName: string;
lastName: string;
role: Role;
authProviders?: AuthProvider[];
banned?: boolean;
banReason?: string;
banExpires?: Date;
createdAt: string;
updatedAt: string;
};
Expand Down
Loading