diff --git a/anzeigen-backend/src/main/java/de/muenchen/anzeigenportal/swbrett/users/controller/UserController.java b/anzeigen-backend/src/main/java/de/muenchen/anzeigenportal/swbrett/users/controller/UserController.java index 7682a2cd..edd84098 100644 --- a/anzeigen-backend/src/main/java/de/muenchen/anzeigenportal/swbrett/users/controller/UserController.java +++ b/anzeigen-backend/src/main/java/de/muenchen/anzeigenportal/swbrett/users/controller/UserController.java @@ -2,12 +2,14 @@ import de.muenchen.anzeigenportal.swbrett.users.model.SwbUserTO; import de.muenchen.anzeigenportal.swbrett.users.service.UserService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.Optional; +@Slf4j @RestController @RequestMapping("/users") public class UserController { @@ -24,18 +26,17 @@ public SwbUserTO getUser(@PathVariable("userId") final long userId) { /** * Put Method to encrypt the email with https. * - * @param email + * @param lhmObjectId unique description in keycloak for every user * @return SwbUserTO with id, if user found. id = null if no user found */ @PutMapping("/find") @ResponseStatus(HttpStatus.OK) - public SwbUserTO findUser(@RequestBody final String email) { - final Optional userTO = service.findUser(email); - if (userTO.isPresent()) { - return userTO.get(); - } else { - return new SwbUserTO(); - } + public SwbUserTO findUser(@RequestBody final String lhmObjectId) { + String sanitizedId = lhmObjectId.replace("\"", ""); // Entfernt Anführungszeichen + sanitizedId = sanitizedId.replaceAll("[\\r\\n]", ""); // Entfernt Zeilenumbrüche + final Optional userTO = service.findUser(sanitizedId); + log.debug("CONTROLLER | findUser with lhmObjectID: {} was: {}", sanitizedId, userTO.isPresent()); + return userTO.orElseGet(SwbUserTO::new); } @PostMapping() diff --git a/anzeigen-backend/src/main/java/de/muenchen/anzeigenportal/swbrett/users/service/UserService.java b/anzeigen-backend/src/main/java/de/muenchen/anzeigenportal/swbrett/users/service/UserService.java index 02391cc3..ddfd7aab 100644 --- a/anzeigen-backend/src/main/java/de/muenchen/anzeigenportal/swbrett/users/service/UserService.java +++ b/anzeigen-backend/src/main/java/de/muenchen/anzeigenportal/swbrett/users/service/UserService.java @@ -33,11 +33,7 @@ public SwbUserTO getUser(final long id) { public Optional findUser(final String lhmObjectId) { final Optional user = repository.findByLhmObjectId(lhmObjectId); - if (user.isPresent()) { - return Optional.of(mapper.toSwbUserTO(user.get())); - } else { - return Optional.empty(); - } + return user.map(swbUser -> mapper.toSwbUserTO(swbUser)); } public SwbUserTO createUser(final SwbUserTO userTO) { diff --git a/anzeigen-frontend/src/Constants.ts b/anzeigen-frontend/src/Constants.ts index 457aaa28..84d770ff 100644 --- a/anzeigen-frontend/src/Constants.ts +++ b/anzeigen-frontend/src/Constants.ts @@ -18,3 +18,8 @@ export const EV_SNACKBAR = "eventbus-snackbar"; */ export const API_ERROR_MSG = "Ein Fehler ist aufgetreten. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut."; + +/** + * Other constants + */ +export const AD_MAX_TITLE_LENGTH = 40; diff --git a/anzeigen-frontend/src/api/fetch-utils.ts b/anzeigen-frontend/src/api/fetch-utils.ts index 0520e28b..fe46f9da 100644 --- a/anzeigen-frontend/src/api/fetch-utils.ts +++ b/anzeigen-frontend/src/api/fetch-utils.ts @@ -121,7 +121,7 @@ export function defaultCatchHandler( * Builds the headers for the request. * @returns {Headers} */ -function getHeaders(): Headers { +export function getHeaders(): Headers { const headers = new Headers({ "Content-Type": "application/json", }); @@ -136,7 +136,7 @@ function getHeaders(): Headers { * Returns the XSRF-TOKEN. * @returns {string|string} */ -function getXSRFToken(): string { +export function getXSRFToken(): string { const help = document.cookie.match( "(^|;)\\s*" + "XSRF-TOKEN" + "\\s*=\\s*([^;]+)" ); diff --git a/anzeigen-frontend/src/api/swbrett/.openapi-generator/FILES b/anzeigen-frontend/src/api/swbrett/.openapi-generator/FILES index ac103283..ea2b3b7b 100644 --- a/anzeigen-frontend/src/api/swbrett/.openapi-generator/FILES +++ b/anzeigen-frontend/src/api/swbrett/.openapi-generator/FILES @@ -1,4 +1,3 @@ -.openapi-generator-ignore apis/DefaultApi.ts apis/index.ts index.ts diff --git a/anzeigen-frontend/src/api/user-client.ts b/anzeigen-frontend/src/api/user-client.ts index aa117e0a..17172038 100644 --- a/anzeigen-frontend/src/api/user-client.ts +++ b/anzeigen-frontend/src/api/user-client.ts @@ -13,8 +13,8 @@ import User from "@/types/User"; * * API-Definition (internal only): https://wiki.muenchen.de/betriebshandbuch/wiki/Red_Hat_Single_Sign-On_(Keycloak)#Scopes */ -export function getUser(): Promise { - return fetch("api/sso/userinfo", getConfig()) +export async function getUser(): Promise { + return await fetch("api/sso/userinfo", getConfig()) .catch(defaultCatchHandler) .then((response) => { defaultResponseHandler( diff --git a/anzeigen-frontend/src/components/Ad/Edit/AdAgbAccept.vue b/anzeigen-frontend/src/components/Ad/Edit/AdAgbAccept.vue index 6368cef7..52e92028 100644 --- a/anzeigen-frontend/src/components/Ad/Edit/AdAgbAccept.vue +++ b/anzeigen-frontend/src/components/Ad/Edit/AdAgbAccept.vue @@ -2,21 +2,62 @@ +import { useObjectUrl } from "@vueuse/core"; +import { onMounted, ref } from "vue"; + +import AGB from "@/static/AGB-1.pdf"; +import SecInfo from "@/static/Datenschutzhinweise.pdf"; + +/** + * Value if checkbox is checked + */ +const isChecked = defineModel({ default: false }); + +/** + * Disables the checkbox input + */ +defineProps<{ + disabled?: boolean; +}>(); - +/** + * Blob and objectUrl for AGB + */ +const agbBlob = ref(); +const agbFile = useObjectUrl(agbBlob); + +/** + * Blob and objectUrl for Datenschutzhinweise + */ +const securityBlob = ref(); +const securityFile = useObjectUrl(securityBlob); + +/** + * Load files on mount + */ +onMounted(async () => { + const [agbResponse, secResponse] = await Promise.all([ + fetch(AGB), + fetch(SecInfo), + ]); + + agbBlob.value = await agbResponse.blob(); + securityBlob.value = await secResponse.blob(); +}); + diff --git a/anzeigen-frontend/src/components/Ad/Edit/AdDialog.vue b/anzeigen-frontend/src/components/Ad/Edit/AdDialog.vue index b36d4b2b..1048b665 100644 --- a/anzeigen-frontend/src/components/Ad/Edit/AdDialog.vue +++ b/anzeigen-frontend/src/components/Ad/Edit/AdDialog.vue @@ -2,13 +2,14 @@ -

Anzeige erstellen oder bearbeiten

+

Anzeige erstellen

+

Anzeige bearbeiten

- + + + + + + + + + + + + + + + + - + - Erstellen / Speichern +

Erstellen

+

Speichern

diff --git a/anzeigen-frontend/src/components/Ad/Edit/AdEditStepper.vue b/anzeigen-frontend/src/components/Ad/Edit/AdEditStepper.vue index 90ab8869..0edb4820 100644 --- a/anzeigen-frontend/src/components/Ad/Edit/AdEditStepper.vue +++ b/anzeigen-frontend/src/components/Ad/Edit/AdEditStepper.vue @@ -19,22 +19,7 @@ - - - - - - - - - - - - - + @@ -74,9 +59,10 @@ diff --git a/anzeigen-frontend/src/components/Ad/Edit/CommonAdInformation.vue b/anzeigen-frontend/src/components/Ad/Edit/CommonAdInformation.vue new file mode 100644 index 00000000..0a6e893c --- /dev/null +++ b/anzeigen-frontend/src/components/Ad/Edit/CommonAdInformation.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/anzeigen-frontend/src/components/Ad/Edit/OptionalAdInformation.vue b/anzeigen-frontend/src/components/Ad/Edit/OptionalAdInformation.vue new file mode 100644 index 00000000..f0c8959e --- /dev/null +++ b/anzeigen-frontend/src/components/Ad/Edit/OptionalAdInformation.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/anzeigen-frontend/src/components/Ad/Edit/SellerAdInformation.vue b/anzeigen-frontend/src/components/Ad/Edit/SellerAdInformation.vue new file mode 100644 index 00000000..11d74d77 --- /dev/null +++ b/anzeigen-frontend/src/components/Ad/Edit/SellerAdInformation.vue @@ -0,0 +1,91 @@ + + + diff --git a/anzeigen-frontend/src/components/AdNavBar.vue b/anzeigen-frontend/src/components/AdNavBar.vue index 0e626293..bcfd79ea 100644 --- a/anzeigen-frontend/src/components/AdNavBar.vue +++ b/anzeigen-frontend/src/components/AdNavBar.vue @@ -1,5 +1,5 @@ diff --git a/anzeigen-frontend/src/components/common/AdDisplayCard.vue b/anzeigen-frontend/src/components/common/AdDisplayCard.vue index c793cd96..55683ab5 100644 --- a/anzeigen-frontend/src/components/common/AdDisplayCard.vue +++ b/anzeigen-frontend/src/components/common/AdDisplayCard.vue @@ -3,6 +3,9 @@ + + + diff --git a/anzeigen-frontend/src/composables/api/useAdApi.ts b/anzeigen-frontend/src/composables/api/useAdApi.ts new file mode 100644 index 00000000..c49e3086 --- /dev/null +++ b/anzeigen-frontend/src/composables/api/useAdApi.ts @@ -0,0 +1,39 @@ +import type { + AdTO, + CreateAdRequest, + DeleteAdRequest, + UpdateAdRequest, +} from "@/api/swbrett"; + +import { inject } from "vue"; + +import { useApiCall } from "@/composables/api/useApiCall"; +import { DEFAULT_API_KEY } from "@/composables/useApi"; + +export const useUpdateAd = () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const api = inject(DEFAULT_API_KEY)!; + + return useApiCall((params: UpdateAdRequest) => + api.updateAd(params) + ); +}; + +export const useDeleteAd = () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const api = inject(DEFAULT_API_KEY)!; + + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + return useApiCall((params: DeleteAdRequest) => + api.deleteAd(params) + ); +}; + +export function useCreateAd() { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const api = inject(DEFAULT_API_KEY)!; + + return useApiCall((params: CreateAdRequest) => + api.createAd(params) + ); +} diff --git a/anzeigen-frontend/src/composables/api/useApiCall.ts b/anzeigen-frontend/src/composables/api/useApiCall.ts new file mode 100644 index 00000000..a9b4ae81 --- /dev/null +++ b/anzeigen-frontend/src/composables/api/useApiCall.ts @@ -0,0 +1,30 @@ +import { readonly, ref } from "vue"; + +export function useApiCall( + apiMethod: (params: TRequest) => Promise +) { + const loadingInternal = ref(false); + const errorInternal = ref(false); + const dataInternal = ref(null); + + const loading = readonly(loadingInternal); + const error = readonly(errorInternal); + const data = readonly(dataInternal); + + const call = (params: TRequest): Promise => { + loadingInternal.value = true; + errorInternal.value = false; + + return apiMethod(params) + .then((data) => (dataInternal.value = data)) + .catch(() => (errorInternal.value = true)) + .finally(() => (loadingInternal.value = false)); + }; + + return { + loading, + error, + data, + call, + }; +} diff --git a/anzeigen-frontend/src/composables/api/useGetCategories.ts b/anzeigen-frontend/src/composables/api/useGetCategories.ts index b20e2620..343436f8 100644 --- a/anzeigen-frontend/src/composables/api/useGetCategories.ts +++ b/anzeigen-frontend/src/composables/api/useGetCategories.ts @@ -1,42 +1,14 @@ import type { AdCategory } from "@/api/swbrett"; -import { inject, readonly, ref } from "vue"; +import { inject } from "vue"; +import { useApiCall } from "@/composables/api/useApiCall"; import { DEFAULT_API_KEY } from "@/composables/useApi"; -export function useGetCategories() { +export const useGetCategories = () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const api = inject(DEFAULT_API_KEY)!; - const loadingInternal = ref(false); - const errorInternal = ref(false); - const dataInternal = ref(); - - const loading = readonly(loadingInternal); - const error = readonly(errorInternal); - const data = readonly(dataInternal); - - const call = () => { - loadingInternal.value = true; - errorInternal.value = false; - - return api - .getAllAdCategories() - .then((data) => { - dataInternal.value = data; - }) - .catch(() => { - errorInternal.value = true; - }) - .finally(() => { - loadingInternal.value = false; - }); - }; - - return { - loading, - error, - data, - call, - }; -} + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + return useApiCall(() => api.getAllAdCategories()); +}; diff --git a/anzeigen-frontend/src/composables/api/useUserApi.ts b/anzeigen-frontend/src/composables/api/useUserApi.ts new file mode 100644 index 00000000..fe99f812 --- /dev/null +++ b/anzeigen-frontend/src/composables/api/useUserApi.ts @@ -0,0 +1,35 @@ +import type { + CreateUserRequest, + FindUserRequest, + SwbUserTO, +} from "@/api/swbrett"; +import type User from "@/types/User"; + +import { inject } from "vue"; + +import { getUser } from "@/api/user-client"; +import { useApiCall } from "@/composables/api/useApiCall"; +import { DEFAULT_API_KEY } from "@/composables/useApi"; + +export const useCreateUser = () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const api = inject(DEFAULT_API_KEY)!; + + return useApiCall((params: CreateUserRequest) => + api.createUser(params) + ); +}; + +export const useFindUser = () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const api = inject(DEFAULT_API_KEY)!; + + return useApiCall((params: FindUserRequest) => + api.findUser(params) + ); +}; + +export const useUserInfo = () => { + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + return useApiCall(() => getUser()); +}; diff --git a/anzeigen-frontend/src/composables/useApi.ts b/anzeigen-frontend/src/composables/useApi.ts index 709af8ed..7044771a 100644 --- a/anzeigen-frontend/src/composables/useApi.ts +++ b/anzeigen-frontend/src/composables/useApi.ts @@ -1,15 +1,37 @@ +import type { HTTPHeaders } from "@/api/swbrett"; import type { InjectionKey } from "vue"; import { provide } from "vue"; +import { getHeaders } from "@/api/fetch-utils"; import { Configuration, DefaultApi } from "@/api/swbrett"; export const DEFAULT_API_KEY: InjectionKey = Symbol("defaultApi"); +/** + * Initialisiert und konfiguriert die API mit Standard-Einstellungen. + */ export const useApi = () => { const config = new Configuration({ basePath: "/api/backend-service", + credentials: "same-origin", + headers: convertHeaders(getHeaders()), }); const defaultApi = new DefaultApi(config); provide(DEFAULT_API_KEY, defaultApi); }; + +/** + * Konvertiert ein Headers-Objekt in ein einfaches Schlüssel-Wert-Paar-Objekt. + * @param {Headers} headers - Das zu konvertierende Headers-Objekt. + * @returns {HTTPHeaders} Ein Objekt mit den gleichen Headern. + */ +function convertHeaders(headers: Headers): HTTPHeaders { + const httpHeaders: HTTPHeaders = {}; + + headers.forEach((value, key) => { + httpHeaders[key] = value; + }); + + return httpHeaders; +} diff --git a/anzeigen-frontend/src/static/AGB-1.pdf b/anzeigen-frontend/src/static/AGB-1.pdf new file mode 100644 index 00000000..aa575b98 Binary files /dev/null and b/anzeigen-frontend/src/static/AGB-1.pdf differ diff --git a/anzeigen-frontend/src/static/Datenschutzhinweise.pdf b/anzeigen-frontend/src/static/Datenschutzhinweise.pdf new file mode 100644 index 00000000..d4e27d30 Binary files /dev/null and b/anzeigen-frontend/src/static/Datenschutzhinweise.pdf differ diff --git a/anzeigen-frontend/src/stores/adcategory.ts b/anzeigen-frontend/src/stores/adcategory.ts new file mode 100644 index 00000000..0e7a45ef --- /dev/null +++ b/anzeigen-frontend/src/stores/adcategory.ts @@ -0,0 +1,14 @@ +import type { AdCategory } from "@/api/swbrett"; + +import { defineStore } from "pinia"; +import { ref } from "vue"; + +export const useCategoriesStore = defineStore("category", () => { + const categories = ref([]); + + const setCategories = (payload: AdCategory[]) => { + categories.value = payload; + }; + + return { categories, setCategories }; +}); diff --git a/anzeigen-frontend/src/stores/user.ts b/anzeigen-frontend/src/stores/user.ts index 1bad4a8c..7f464393 100644 --- a/anzeigen-frontend/src/stores/user.ts +++ b/anzeigen-frontend/src/stores/user.ts @@ -1,3 +1,5 @@ +import type { SwbUserTO } from "@/api/swbrett"; + import { defineStore } from "pinia"; import { computed, ref } from "vue"; @@ -9,13 +11,28 @@ export interface UserState { export const useUserStore = defineStore("user", () => { const user = ref(null); + const userID = ref(); const getUser = computed((): User | null => { return user.value; }); + const swbUserTo = computed(() => { + return { + lhmObjectId: user.value?.lhmObjectID, + displayName: user.value?.displayName, + } as SwbUserTO; + }); + + const lhmObjectId = computed(() => user.value?.lhmObjectID); + function setUser(payload: User | null): void { user.value = payload; } - return { getUser, setUser }; + + function setUserId(payload: number) { + userID.value = payload; + } + + return { getUser, setUser, swbUserTo, lhmObjectId, setUserId }; }); diff --git a/api-spec/anzeigen_portal-openapi.yaml b/api-spec/anzeigen_portal-openapi.yaml index 82bd6c5c..66f9ac7b 100644 --- a/api-spec/anzeigen_portal-openapi.yaml +++ b/api-spec/anzeigen_portal-openapi.yaml @@ -697,3 +697,14 @@ components: type: "boolean" fileValue: $ref: "#/components/schemas/SwbFileTO" + securitySchemes: + spring_oauth: + type: oauth2 + description: Oauth2 flow + flows: + password: + authorizationUrl: http://keycloak:8100/auth/realms/local_realm/protocol/openid-connect/auth + tokenUrl: http://keycloak:8100/auth/realms/local_realm/protocol/openid-connect/token + refreshUrl: http://keycloak:8100/auth/realms/local_realm/protocol/openid-connect/token + scopes: + lhm_extended: lhm_extended