Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N21-2133 Authentication #10

Merged
merged 9 commits into from
Aug 26, 2024
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
2 changes: 1 addition & 1 deletion ansible/group_vars/develop/shd-client.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
SHD_CLIENT_IMAGE: ghcr.io/hpi-schul-cloud/shd-client
SHD_CLIENT_PREFIX: shd2-
SHD_CLIENT_PREFIX: superhero-
2 changes: 1 addition & 1 deletion ansible/roles/shd-client-core/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
SHD_CLIENT_IMAGE: quay.io/schulcloudverbund/shd-client
SHD_CLIENT_PREFIX: dashboard2.
SHD_CLIENT_PREFIX: superhero.
6 changes: 0 additions & 6 deletions ansible/roles/shd-client-core/templates/ingress.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ metadata:
namespace: {{ NAMESPACE }}
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "{{ TLS_ENABLED|default("false") }}"
# type of authentication
nginx.ingress.kubernetes.io/auth-type: basic
# name of the secret that contains the user/password definitions
nginx.ingress.kubernetes.io/auth-secret: shd-basic-auth-secret
# message to display with an appropriate context why the authentication is required
nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required'
{% if CLUSTER_ISSUER is defined %}
cert-manager.io/cluster-issuer: {{ CLUSTER_ISSUER }}
{% endif %}
Expand Down
1 change: 1 addition & 0 deletions config/webpack/webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = merge(common, {
process.env.NODE_ENV === "development"
? {
port: 4100,
historyApiFallback: true,
allowedHosts: "all",
client: {
overlay: false,
Expand Down
325 changes: 137 additions & 188 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
"jest"
],
"scripts": {
"serve": "NODE_ENV=development webpack serve --config config/webpack/webpack.dev.js",
"serve:windows": "set NODE_ENV=development&& webpack serve --config config/webpack/webpack.dev.js",
"build": "NODE_ENV=production webpack --config config/webpack/webpack.prod.js",
"test": "npm run test:unit",
"serve": "cross-env NODE_ENV=development webpack serve --config config/webpack/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config config/webpack/webpack.prod.js",
"test": "cross-env NODE_ENV=test npm run test:unit",
"test:unit": "npx jest",
"test:unit:ci": "npm run test:unit -- --coverage --ci --maxWorkers=4",
"lint": "npx eslint 'src/**/*.{ts,js,vue}'",
Expand All @@ -19,6 +18,7 @@
"dependencies": {
"@vueuse/core": "^10.11.0",
"axios": "^1.7.2",
"cross-env": "^7.0.3",
"dayjs": "^1.11.12",
"pinia": "^2.2.0",
"ts-node": "^10.9.2",
Expand Down
14 changes: 10 additions & 4 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,28 @@
</div>
<h1 class="ma-0">New Superhero-Dashboard</h1>
</div>

<LogoutBtn />

<nav>
<VTabs align-tabs="center">
<VTab to="/">Home</VTab>
<VTab to="/about">About</VTab>
<VTab to="/login">Login</VTab>
</VTabs>
<VDivider />
</nav>
</div>
</header>

<div class="d-flex justify-center">
<RouterView />
</div>
<main>
<div class="d-flex justify-center">
<RouterView />
</div>
</main>
</VApp>
</template>

<script setup lang="ts">
import { LogoutBtn } from "@feature/logout";
import { RouterView } from "vue-router";
</script>
7 changes: 6 additions & 1 deletion src/locales/de.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export default {
"common.actions.login": "Anmelden",
"common.actions.logout": "Abmelden",
"common.words.emailAddress": "E-Mail Adresse",
"common.words.password": "Passwort",
"error.generic": "Ein Fehler ist aufgetreten",
"error.load": "Fehler beim Laden der Daten.",
"error.load": "Fehler beim Laden der Daten",
"error.login.noSuperhero": "Der ausgewählte Account ist kein Superhero",
};
7 changes: 6 additions & 1 deletion src/locales/en.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export default {
"common.actions.login": "Log in",
"common.actions.logout": "Log out",
"common.words.emailAddress": "E-Mail Address",
"common.words.password": "Password",
"error.generic": "An error has occurred",
"error.load": "Error while loading the data.",
"error.load": "Error while loading the data",
"error.login.noSuperhero": "The selected account is not a superhero",
};
7 changes: 6 additions & 1 deletion src/locales/es.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export default {
"common.actions.login": "Conectarse",
"common.actions.logout": "Cerrar sesión",
"common.words.emailAddress": "Correo electrónico",
"common.words.password": "Contraseña",
"error.generic": "Se ha producido un error",
"error.load": "Error al cargar los datos.",
"error.load": "Error al cargar los datos",
"error.login.noSuperhero": "La cuenta seleccionada no es un superhéroe",
};
7 changes: 6 additions & 1 deletion src/locales/uk.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export default {
"common.actions.login": "зареєструватися",
"common.actions.logout": "Вийти",
"common.words.emailAddress": "адреса електронної пошти",
"common.words.password": "пароль",
"error.generic": "Виникла помилка",
"error.load": "Помилка під час завантаження даних.",
"error.load": "Помилка під час завантаження даних",
"error.login.noSuperhero": "Вибраний обліковий запис не є супергероєм",
};
11 changes: 6 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import { createI18n } from "@/plugins/i18n";
import vuetify from "@/plugins/vuetify";
import router from "@/router";
import { initializeAxios } from "@/utils/api";
import { useAuthStore } from "@data/auth";
import { useAuthStore, useJwtCookie } from "@data/auth";
import { useEnvConfigStore } from "@data/env-config";
import { htmlConfig } from "@feature/render-html";
import axios from "axios";
import { createPinia } from "pinia";
import Cookies from "universal-cookie";
import { createApp } from "vue";
import VueDOMPurifyHTML from "vue-dompurify-html";
import App from "./App.vue";
Expand Down Expand Up @@ -38,13 +37,15 @@ app.use(VueDOMPurifyHTML, {

await useEnvConfigStore().loadConfig();

const cookies = new Cookies();
const jwt = cookies.get("jwt");
const authStore = useAuthStore();
const { getJwt } = useJwtCookie();

const jwt = getJwt();
if (jwt) {
axios.defaults.headers.common["Authorization"] = "Bearer " + jwt;

try {
await useAuthStore().login();
await authStore.fetchMe();
} catch (e) {
// eslint-disable-next-line no-console
console.error("### JWT invalid: ", e);
Expand Down
30 changes: 30 additions & 0 deletions src/modules/data/application-error/applicationError.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
ApiResponseError,
ApiValidationError,
mapAxiosErrorToResponseError,
} from "@/utils/api";
import { HttpStatusCode } from "./httpStatusCode.enum";

export interface ApplicationErrorProps {
Expand All @@ -18,4 +23,29 @@ export class ApplicationError extends Error {
this.statusCode = props.statusCode;
this.translationKey = props.translationKey;
}

static fromUnknown(
error: unknown,
translationKey?: string
): ApplicationError {
if (error instanceof ApplicationError) {
return error;
}

const apiError: ApiResponseError | ApiValidationError =
mapAxiosErrorToResponseError(error);

return ApplicationError.fromApiError(apiError, translationKey);
}

static fromApiError(
apiError: ApiResponseError | ApiValidationError,
translationKey = "error.generic"
): ApplicationError {
return new ApplicationError({
statusCode: apiError.code,
message: `${apiError.title}: ${apiError.message}`,
translationKey,
});
}
}
126 changes: 126 additions & 0 deletions src/modules/data/application-error/applicationError.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { defaultApiError, mapAxiosErrorToResponseError } from "@/utils/api";
import {
apiResponseErrorFactory,
applicationErrorFactory,
axiosErrorFactory,
} from "@@/tests/test-utils/factory";
import { ApplicationError } from "./applicationError";

describe(ApplicationError.name, () => {
describe("fromUnknown", () => {
describe("when the error is already an application error", () => {
const setup = () => {
const error = applicationErrorFactory.build();

return {
error,
};
};

it("should return the application error", () => {
const { error } = setup();

const result = ApplicationError.fromUnknown(error);

expect(result).toEqual<ApplicationError>(error);
});
});

describe("when the error is an axios error", () => {
const setup = () => {
const error = axiosErrorFactory.build();

return {
error,
};
};

it("should return an application error from the api error", () => {
const { error } = setup();

const result = ApplicationError.fromUnknown(error);

expect(result).toEqual<ApplicationError>(
ApplicationError.fromApiError(mapAxiosErrorToResponseError(error))
);
});
});

describe("when the error is not a known type", () => {
const setup = () => {
const error = new Error("test");

return {
error,
};
};

it("should return a generic application error from the default api error", () => {
const { error } = setup();

const result = ApplicationError.fromUnknown(error);

expect(result).toEqual<ApplicationError>(
new ApplicationError({
statusCode: defaultApiError.code,
message: `${defaultApiError.title}: ${defaultApiError.message}`,
translationKey: "error.generic",
})
);
});
});
});

describe("fromApiError", () => {
describe("when using the default translation key", () => {
const setup = () => {
const apiError = apiResponseErrorFactory.build();

return {
apiError,
};
};

it("should return an application error with a generic message", () => {
const { apiError } = setup();

const result = ApplicationError.fromApiError(apiError);

expect(result).toEqual<ApplicationError>(
new ApplicationError({
statusCode: apiError.code,
message: `${apiError.title}: ${apiError.message}`,
translationKey: "error.generic",
})
);
});
});

describe("when specifying a translation key", () => {
const setup = () => {
const apiError = apiResponseErrorFactory.build();

return {
apiError,
};
};

it("should return an application error with the translation key", () => {
const { apiError } = setup();

const result = ApplicationError.fromApiError(
apiError,
"error.not.generic"
);

expect(result).toEqual<ApplicationError>(
new ApplicationError({
statusCode: apiError.code,
message: `${apiError.title}: ${apiError.message}`,
translationKey: "error.not.generic",
})
);
});
});
});
});
Loading
Loading