{"version":3,"file":"js/app.2b034eda.js","mappings":"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","sources":["webpack://hellewi-ilmoittautuminen/./frontend-src/installCompositionApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/App.vue?e5d3","webpack://hellewi-ilmoittautuminen/frontend-src/App.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/App.vue?5dce","webpack://hellewi-ilmoittautuminen/./frontend-src/App.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/filters.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/i18n.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/components/TenantLanguage.vue?aec1","webpack://hellewi-ilmoittautuminen/./frontend-src/api/runtime.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/BenefitTypeCulture.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/BenefitTypeExcercise.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/ClientLessonAction.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/CourseSortOrder.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/CourseSortfield.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/Contact.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/CpuPayment.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/CpuResponseStatus.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/ErrorItemType.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCartItemType.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/CpuProduct.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/Geopoint.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiAgeLimits.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiBrand.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCallout.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCartItem.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCartItemId.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCartStatus.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCatalog.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCatalogItemType.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCatalogItem.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCatalogItemText.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCatalogSettings.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCatalogSettingsEnabledCatalogItemTypes.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourseNotificationLabel.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourseStatus.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiLocationSortfields.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiReservationSortfields.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourse.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourseCount.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourseDay.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourseLesson.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourseMinimal.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourseMinimalParent.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourseNotification.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCoursePartial.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCoursePeriod.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCoursePrice.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCoursePriceInstallment.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCoursePriceInstallmentInstallments.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiCourseProduct.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiFile.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiGetRegistrationResponse.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiImage.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiLanguage.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiLessonParticipantCount.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiLocation.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiMyRegistrationsResponse.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiParticipantCount.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiPostRegistrationResponse.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiPromotion.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiTenantType.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/MethodOfPayment.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/MethodOfPaymentInfotext.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PaymentServiceName.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PaytrailApiAlgorithm.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PaytrailApiReceiptStatus.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PurchaseProductItemType.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PurchaseProductStatus.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PurchaseProductType.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/Sortdir.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/UpdatableCourseProperties.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/Weekday.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiTag.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/HellewiText.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PaymentFormField.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PurchaseAmountNumber.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PurchaseInvoiceNumber.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PurchaseProductItemNumber.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/PurchaseProductNumber.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/models/RegistrationPriceNumber.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/apis/BrandApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/apis/LocationApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/apis/CalloutsApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/apis/CartApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/apis/CatalogApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/apis/CourseApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/apis/MobileApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/utils/api-utils.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/apis/PaymentApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/api/apis/RegistrationApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/utils/misc-utils.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/utils/http-status-codes.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/useBrandApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/useCalloutsApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/useCartApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/useCatalogApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/useCourseApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/usePaymentApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/useRegistrationApi.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Header.vue?755b","webpack://hellewi-ilmoittautuminen/frontend-src/components/Header.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Header.vue?f1a2","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Header.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Callouts.vue?f6e0","webpack://hellewi-ilmoittautuminen/frontend-src/components/Callouts.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Callouts.vue?aea6","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Callouts.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Footer.vue?0203","webpack://hellewi-ilmoittautuminen/frontend-src/components/Footer.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Footer.vue?8181","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Footer.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/utils/meta-pixel.ts","webpack://hellewi-ilmoittautuminen/frontend-src/components/TenantLanguage.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/TenantLanguage.vue?c5bb","webpack://hellewi-ilmoittautuminen/./frontend-src/components/TenantLanguage.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Cart.vue?a3ad","webpack://hellewi-ilmoittautuminen/frontend-src/components/registration/RegistrationStatus.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/ClientCard.vue?9570","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/CartItem.vue?b2e4","webpack://hellewi-ilmoittautuminen/./frontend-src/utils/agelimit-translation.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/RegistrationStatus.vue?6e54","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/RegistrationStatus.vue?d48d","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/RegistrationStatus.vue","webpack://hellewi-ilmoittautuminen/frontend-src/components/cart/CartItem.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/CartItem.vue?407f","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/CartItem.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/RegistrationForm.vue?966d","webpack://hellewi-ilmoittautuminen/./frontend-src/utils/agelimit.ts","webpack://hellewi-ilmoittautuminen/frontend-src/components/cart/RegistrationForm.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/RegistrationForm.vue?4edb","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/RegistrationForm.vue","webpack://hellewi-ilmoittautuminen/frontend-src/components/cart/ClientCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/ClientCard.vue?b87d","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/ClientCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/PayerCard.vue?cebf","webpack://hellewi-ilmoittautuminen/frontend-src/components/cart/PayerCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/PayerCard.vue?e1fe","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/PayerCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/CartSummary.vue?a67c","webpack://hellewi-ilmoittautuminen/frontend-src/components/cart/CartSummary.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/CartSummary.vue?10ad","webpack://hellewi-ilmoittautuminen/./frontend-src/components/cart/CartSummary.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/useTitle.ts","webpack://hellewi-ilmoittautuminen/frontend-src/views/Cart.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Cart.vue?1338","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Cart.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Course.vue?7c94","webpack://hellewi-ilmoittautuminen/frontend-src/components/CourseInfoDl.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/CourseInfoDl.vue?714e","webpack://hellewi-ilmoittautuminen/./frontend-src/utils/date-utils.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/components/CourseInfoDl.vue?19e5","webpack://hellewi-ilmoittautuminen/./frontend-src/components/CourseInfoDl.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/CourseMap.vue?0af3","webpack://hellewi-ilmoittautuminen/frontend-src/components/course/CourseMap.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/CourseMap.vue?0e9a","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/CourseMap.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/LessonsCollapse.vue?41e1","webpack://hellewi-ilmoittautuminen/frontend-src/components/course/LessonsCollapse.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/LessonsCollapse.vue?6deb","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/LessonsCollapse.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/LessonsRegistration.vue?2e51","webpack://hellewi-ilmoittautuminen/frontend-src/components/CourseAvailability.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/CourseAvailability.vue?4acc","webpack://hellewi-ilmoittautuminen/./frontend-src/components/CourseAvailability.vue?b13e","webpack://hellewi-ilmoittautuminen/./frontend-src/components/CourseAvailability.vue","webpack://hellewi-ilmoittautuminen/frontend-src/components/course/LessonsRegistration.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/LessonsRegistration.vue?35fd","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/LessonsRegistration.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/RegistrationBox.vue?97ef","webpack://hellewi-ilmoittautuminen/./frontend-src/utils/course-utils.ts","webpack://hellewi-ilmoittautuminen/frontend-src/components/course/RegistrationBox.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/RegistrationBox.vue?1f17","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/RegistrationBox.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/SocialShare.vue?beb2","webpack://hellewi-ilmoittautuminen/frontend-src/components/course/SocialShare.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/SocialShare.vue?9eeb","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/SocialShare.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/CourseImage.vue?6ba6","webpack://hellewi-ilmoittautuminen/frontend-src/components/course/CourseImage.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/CourseImage.vue?67a6","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/CourseImage.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/CourseFile.vue?39d3","webpack://hellewi-ilmoittautuminen/frontend-src/components/course/CourseFile.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/CourseFile.vue?9cab","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/CourseFile.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/BreadCrumbs.vue?c213","webpack://hellewi-ilmoittautuminen/frontend-src/components/course/BreadCrumbs.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/BreadCrumbs.vue?5d91","webpack://hellewi-ilmoittautuminen/./frontend-src/components/course/BreadCrumbs.vue","webpack://hellewi-ilmoittautuminen/frontend-src/views/Course.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Course.vue?6f5d","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Course.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Help.vue?1a6a","webpack://hellewi-ilmoittautuminen/frontend-src/views/Help.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Help.vue?4a63","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Help.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Home.vue?6f10","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/useSearchParams.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/CourseList.vue?a8fe","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/CourseCard.vue?d9dd","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/CourseCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/CourseCard.vue?d76e","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/CourseCard.vue","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/CourseList.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/CourseList.vue?6ac1","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/CourseList.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/CatalogFilters.vue?25e8","webpack://hellewi-ilmoittautuminen/./frontend-src/hooks/useCatalogFilters.ts","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/CatalogFilters.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/CatalogFilters.vue?76a1","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/CatalogFilters.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/MobileFilters.vue?2af6","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/MobileFilters.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/MobileFilters.vue?6f2a","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/MobileFilters.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/PromotionCarousel.vue?dd72","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/PromotionCarousel.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/PromotionCarousel.vue?142f","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/PromotionCarousel.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/SearchInput.vue?f3b3","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/SearchInput.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/SearchInput.vue?8837","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/SearchInput.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/Sort.vue?efd0","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/Sort.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/Sort.vue?08fc","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/Sort.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/Hero.vue?8c8e","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/Hero.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/Hero.vue?42ad","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/Hero.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/FilterTags.vue?7674","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/FilterTags.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/FilterTags.vue?3bfc","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/FilterTags.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/ScrollToFilters.vue?01bb","webpack://hellewi-ilmoittautuminen/frontend-src/components/home/ScrollToFilters.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/ScrollToFilters.vue?7367","webpack://hellewi-ilmoittautuminen/./frontend-src/components/home/ScrollToFilters.vue","webpack://hellewi-ilmoittautuminen/frontend-src/views/Home.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Home.vue?4dcb","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Home.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/MyRegistrations.vue?1a33","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Registration.vue?e336","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/InvoiceCard.vue?77e9","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/Invoice.vue?e18a","webpack://hellewi-ilmoittautuminen/frontend-src/components/registration/Invoice.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/Invoice.vue?288c","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/Invoice.vue","webpack://hellewi-ilmoittautuminen/frontend-src/components/registration/InvoiceCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/InvoiceCard.vue?c66c","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/InvoiceCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/PaymentCard.vue?7e8b","webpack://hellewi-ilmoittautuminen/frontend-src/components/registration/PaymentCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/PaymentCard.vue?bcbc","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/PaymentCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/RegistrationCard.vue?a994","webpack://hellewi-ilmoittautuminen/frontend-src/components/registration/RegistrationCard.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/RegistrationCard.vue?df65","webpack://hellewi-ilmoittautuminen/./frontend-src/components/registration/RegistrationCard.vue","webpack://hellewi-ilmoittautuminen/frontend-src/components/Registration.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Registration.vue?8ee4","webpack://hellewi-ilmoittautuminen/./frontend-src/components/Registration.vue","webpack://hellewi-ilmoittautuminen/frontend-src/views/MyRegistrations.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/MyRegistrations.vue?ab67","webpack://hellewi-ilmoittautuminen/./frontend-src/views/MyRegistrations.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/MyRegistrationsLoginLink.vue?99ae","webpack://hellewi-ilmoittautuminen/frontend-src/views/MyRegistrationsLoginLink.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/MyRegistrationsLoginLink.vue?f7ef","webpack://hellewi-ilmoittautuminen/./frontend-src/views/MyRegistrationsLoginLink.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/NewRegistration.vue?31c5","webpack://hellewi-ilmoittautuminen/frontend-src/views/NewRegistration.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/NewRegistration.vue?f804","webpack://hellewi-ilmoittautuminen/./frontend-src/views/NewRegistration.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/NotFound.vue?73c9","webpack://hellewi-ilmoittautuminen/frontend-src/views/NotFound.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/NotFound.vue?5c64","webpack://hellewi-ilmoittautuminen/./frontend-src/views/NotFound.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Payment.vue?28a9","webpack://hellewi-ilmoittautuminen/frontend-src/views/Payment.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Payment.vue?2dc9","webpack://hellewi-ilmoittautuminen/./frontend-src/views/Payment.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/SingleRegistration.vue?bfb8","webpack://hellewi-ilmoittautuminen/frontend-src/views/SingleRegistration.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/views/SingleRegistration.vue?dd87","webpack://hellewi-ilmoittautuminen/./frontend-src/views/SingleRegistration.vue","webpack://hellewi-ilmoittautuminen/./frontend-src/router.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/vee-validate.ts","webpack://hellewi-ilmoittautuminen/./frontend-src/main.ts","webpack://hellewi-ilmoittautuminen/webpack/bootstrap","webpack://hellewi-ilmoittautuminen/webpack/runtime/chunk loaded","webpack://hellewi-ilmoittautuminen/webpack/runtime/compat get default export","webpack://hellewi-ilmoittautuminen/webpack/runtime/define property getters","webpack://hellewi-ilmoittautuminen/webpack/runtime/global","webpack://hellewi-ilmoittautuminen/webpack/runtime/harmony module decorator","webpack://hellewi-ilmoittautuminen/webpack/runtime/hasOwnProperty shorthand","webpack://hellewi-ilmoittautuminen/webpack/runtime/make namespace object","webpack://hellewi-ilmoittautuminen/webpack/runtime/node module decorator","webpack://hellewi-ilmoittautuminen/webpack/runtime/publicPath","webpack://hellewi-ilmoittautuminen/webpack/runtime/jsonp chunk loading","webpack://hellewi-ilmoittautuminen/webpack/startup"],"sourcesContent":["import Vue from 'vue';\nimport VueCompositionApi from '@vue/composition-api';\n\nVue.use(VueCompositionApi);\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"app\"}},[_c('router-view')],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js!../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js!../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./App.vue?vue&type=template&id=669014bc\"\nimport script from \"./App.vue?vue&type=script&lang=ts\"\nexport * from \"./App.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import {\n format,\n isSameDay,\n isSameMonth,\n isSameYear,\n isValid,\n setISODay,\n startOfDay,\n subMinutes\n} from 'date-fns';\n\n// use custom formatters for dates and times\n\nexport const DATE_FORMAT_FI = 'd.M.yyyy';\nexport const DATETIME_FORMAT_FI = 'd.M.yyyy H:mm';\nexport const TIME_FORMAT_FI = 'H:mm';\nexport const TIME_SECONDS_FORMAT_FI = 'H:mm:ss';\n\n/**\n * Formats a date in Finnish time format H:mm, e.g.\n * 12:04\n *\n * @param date date object, possibly null\n * @return time as string\n */\nexport const formatTime = (date: Date | null | undefined): string =>\n date && isValid(date) ? format(date, TIME_FORMAT_FI) : '';\n\n/**\n * Formats a date in Finnish date format d.M.yyyy, e.g.\n * 1.5.2020\n *\n * @param date date object, possibly null\n * @return date as string\n */\nexport const formatDate = (date: Date | null | undefined): string =>\n date && isValid(date) ? format(date, DATE_FORMAT_FI) : '';\n\n/**\n * Formats a date in Finnish date-time format d.M.yyyy H:mm, e.g.\n * 1.5.2020 12:04\n *\n * @param date date object, possibly null\n * @return time as string\n */\nexport const formatDateTime = (date: Date | null | undefined): string =>\n date && isValid(date) ? format(date, DATETIME_FORMAT_FI) : '';\n\n/**\n * Formats a time range in Finnish time format H:mm-H:mm, e.g.\n * 12:00-12:05\n *\n * @param begins date object, possibly null\n * @param ends date object, possibly null\n * @return time range as string\n */\nexport const formatTimeRange = (\n begins: Date | null | undefined,\n ends: Date | null | undefined\n): string => {\n if (begins && ends && isValid(begins) && isValid(ends)) {\n return `${formatTime(begins)}\\u2013${formatTime(ends)}`;\n } else if (begins && isValid(begins)) {\n return formatTime(begins);\n } else if (ends && isValid(ends)) {\n return formatTime(ends);\n } else {\n return '';\n }\n};\n\n/**\n * Formats a date range in Finnish date format d.M.yyyy-d.M.yyyy, e.g.\n * 1.1.2020-1.5.2020\n *\n * If the dates are the same, return that 1.1.2020\n * If the dates have the same month and year, return 1.-2.1.2020\n * If the dates have the same year, return 1.1.-1.2.2020\n *\n * @param begins date object, possibly null\n * @param ends date object, possibly null\n * @return time range as string\n */\nexport const formatDateRange = (\n begins: Date | undefined | null,\n ends: Date | undefined | null\n): string => {\n if (begins && ends && isValid(begins) && isValid(ends) && isSameDay(begins, ends)) {\n return formatDate(begins);\n } else if (begins && ends && isValid(begins) && isValid(ends)) {\n const beginsFormat =\n isSameMonth(begins, ends) && isSameYear(begins, ends)\n ? 'd.'\n : isSameYear(begins, ends)\n ? 'd.M.' // prettier-ignore\n : DATE_FORMAT_FI; // prettier-ignore\n\n return `${format(begins, beginsFormat)}\\u2013${formatDate(ends)}`;\n } else if (begins && isValid(begins)) {\n return formatDate(begins);\n } else if (ends && isValid(ends)) {\n return formatDate(ends);\n } else {\n return '';\n }\n};\n\n/**\n * Formats a date-time range in Finnish date-time format d.M.yyyy H:mm, e.g.\n * 1.1.2020 12:00 - 1.2.2020 12:05\n *\n * If dates are the same, return 1.1.2020 12:00-12:05\n * If only one is given, return it, 1.1.2020 12:00\n *\n * @param begins date object, possibly null\n * @param ends date object, possibly null\n * @return time range as string\n */\nexport const formatDateTimeRange = (\n begins: Date | null | undefined,\n ends: Date | null | undefined\n): string => {\n if (begins && ends && isValid(begins) && isValid(ends) && isSameDay(begins, ends)) {\n return `${formatDateTime(begins)}\\u2013${formatTime(ends)}`;\n } else if (begins && ends && isValid(begins) && isValid(ends)) {\n return `${formatDateTime(begins)} \\u2013 ${formatDateTime(ends)}`;\n } else if (begins && isValid(begins)) {\n return formatDateTime(begins);\n } else if (ends && isValid(ends)) {\n return formatDateTime(ends);\n } else {\n return '';\n }\n};\n\n/**\n * Subtracts one minute from date if it is midnight (d.M.yyyy 00:00)\n *\n * If date is e.g. 2.2.2021 00:00, return 1.2.2021 23:59\n * If date is e.g. 2.2.2021 12:00, return 2.2.2021 12:00\n *\n * @param date date object, possibly null\n * @return time as string\n */\nexport const formatMidnight = (date: Date | null | undefined): string => {\n if (!date) {\n return '';\n }\n if (date.getTime() === startOfDay(date).getTime()) {\n return formatDateTime(subMinutes(date, 1));\n } else {\n return formatDateTime(date);\n }\n};\n\nexport const fakeDateFromWeekday = (weekday: number | undefined): Date | undefined => {\n if (weekday != null) {\n return setISODay(new Date(), weekday);\n } else {\n return undefined;\n }\n};\n","import VueI18n, { DateTimeFormats } from 'vue-i18n';\n\nconst numberFormats = {\n fi: {\n currency: {\n style: 'currency',\n currency: 'EUR'\n },\n twodecimal: {\n style: 'decimal',\n minimumFractionDigits: 2,\n maximumFractionDigits: 2\n },\n percent: {\n style: 'percent'\n }\n },\n sv: {\n currency: {\n style: 'currency',\n currency: 'EUR'\n }\n },\n en: {\n currency: {\n style: 'currency',\n currency: 'EUR'\n }\n }\n};\n\nconst dateTimeFormats = {\n fi: {\n weekdayShort: {\n weekday: 'short'\n },\n weekdayLong: {\n weekday: 'long'\n }\n },\n en: {\n weekdayShort: {\n weekday: 'short'\n },\n weekdayLong: {\n weekday: 'long'\n }\n },\n sv: {\n weekdayShort: {\n weekday: 'short'\n },\n weekdayLong: {\n weekday: 'long'\n }\n }\n} as DateTimeFormats;\n\nexport const initializeI18n = (): VueI18n =>\n new VueI18n({\n locale: 'fi',\n fallbackLocale: 'fi',\n numberFormats,\n dateTimeFormats,\n silentFallbackWarn: true\n });\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{style:({\n '--brandcolor': _vm.brand ? _vm.brand.color : '',\n '--brandcomplementarycolor': _vm.brand ? _vm.brand.complementarycolor : '',\n '--brandfont': _vm.brand ? _vm.brand.font : '',\n '--brandheaderfont': _vm.brand ? _vm.brand.headerfont : ''\n })},[(_vm.i18nIsLoaded && !_vm.errorMsg)?_c('section',[_c('Header'),_c('Callouts'),_c('div',{staticClass:\"container\"},[_c('main',[_c('router-view')],1)]),_c('Footer')],1):_vm._e(),(_vm.errorMsg)?_c('main',{staticClass:\"errorContainer\"},[_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h1',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('error.title')))]),_c('p',[_vm._v(_vm._s(_vm.$t(_vm.errorMsg)))])])])]):_vm._e()])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nexport const BASE_PATH = \"https://api.opistopalvelut.fi/v1/demo/fi\".replace(/\\/+$/, \"\");\n\nconst isBlob = (value: any) => typeof Blob !== 'undefined' && value instanceof Blob;\n\n/**\n * This is the base class for all generated API classes.\n */\nexport class BaseAPI {\n\n private middleware: Middleware[];\n\n constructor(protected configuration = new Configuration()) {\n this.middleware = configuration.middleware;\n }\n\n withMiddleware(this: T, ...middlewares: Middleware[]) {\n const next = this.clone();\n next.middleware = next.middleware.concat(...middlewares);\n return next;\n }\n\n withPreMiddleware(this: T, ...preMiddlewares: Array) {\n const middlewares = preMiddlewares.map((pre) => ({ pre }));\n return this.withMiddleware(...middlewares);\n }\n\n withPostMiddleware(this: T, ...postMiddlewares: Array) {\n const middlewares = postMiddlewares.map((post) => ({ post }));\n return this.withMiddleware(...middlewares);\n }\n\n protected async request(context: RequestOpts): Promise {\n const { url, init } = this.createFetchParams(context);\n const response = await this.fetchApi(url, init);\n if (response.status >= 200 && response.status < 300) {\n return response;\n }\n throw response;\n }\n\n private createFetchParams(context: RequestOpts) {\n let url = this.configuration.basePath + context.path;\n if (context.query !== undefined && Object.keys(context.query).length !== 0) {\n // only add the querystring to the URL if there are query parameters.\n // this is done to avoid urls ending with a \"?\" character which buggy webservers\n // do not handle correctly sometimes.\n url += '?' + this.configuration.queryParamsStringify(context.query);\n }\n const body = ((typeof FormData !== \"undefined\" && context.body instanceof FormData) || context.body instanceof URLSearchParams || isBlob(context.body))\n ? context.body\n : JSON.stringify(context.body);\n\n const headers = Object.assign({}, this.configuration.headers, context.headers);\n const init = {\n method: context.method,\n headers: headers,\n body,\n credentials: this.configuration.credentials\n };\n return { url, init };\n }\n\n private fetchApi = async (url: string, init: RequestInit) => {\n let fetchParams = { url, init };\n for (const middleware of this.middleware) {\n if (middleware.pre) {\n fetchParams = await middleware.pre({\n fetch: this.fetchApi,\n ...fetchParams,\n }) || fetchParams;\n }\n }\n let response = await this.configuration.fetchApi(fetchParams.url, fetchParams.init);\n for (const middleware of this.middleware) {\n if (middleware.post) {\n response = await middleware.post({\n fetch: this.fetchApi,\n url,\n init,\n response: response.clone(),\n }) || response;\n }\n }\n return response;\n }\n\n /**\n * Create a shallow clone of `this` by constructing a new instance\n * and then shallow cloning data members.\n */\n private clone(this: T): T {\n const constructor = this.constructor as any;\n const next = new constructor(this.configuration);\n next.middleware = this.middleware.slice();\n return next;\n }\n};\n\nexport class RequiredError extends Error {\n name: \"RequiredError\" = \"RequiredError\";\n constructor(public field: string, msg?: string) {\n super(msg);\n }\n}\n\nexport const COLLECTION_FORMATS = {\n csv: \",\",\n ssv: \" \",\n tsv: \"\\t\",\n pipes: \"|\",\n};\n\nexport type FetchAPI = GlobalFetch['fetch'];\n\nexport interface ConfigurationParameters {\n basePath?: string; // override base path\n fetchApi?: FetchAPI; // override for fetch implementation\n middleware?: Middleware[]; // middleware to apply before/after fetch requests\n queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings\n username?: string; // parameter for basic security\n password?: string; // parameter for basic security\n apiKey?: string | ((name: string) => string); // parameter for apiKey security\n accessToken?: string | ((name?: string, scopes?: string[]) => string); // parameter for oauth2 security\n headers?: HTTPHeaders; //header params we want to use on every request\n credentials?: RequestCredentials; //value for the credentials param we want to use on each request\n}\n\nexport class Configuration {\n constructor(private configuration: ConfigurationParameters = {}) {}\n\n get basePath(): string {\n return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;\n }\n\n get fetchApi(): FetchAPI {\n return this.configuration.fetchApi || window.fetch.bind(window);\n }\n\n get middleware(): Middleware[] {\n return this.configuration.middleware || [];\n }\n\n get queryParamsStringify(): (params: HTTPQuery) => string {\n return this.configuration.queryParamsStringify || querystring;\n }\n\n get username(): string | undefined {\n return this.configuration.username;\n }\n\n get password(): string | undefined {\n return this.configuration.password;\n }\n\n get apiKey(): ((name: string) => string) | undefined {\n const apiKey = this.configuration.apiKey;\n if (apiKey) {\n return typeof apiKey === 'function' ? apiKey : () => apiKey;\n }\n return undefined;\n }\n\n get accessToken(): ((name: string, scopes?: string[]) => string) | undefined {\n const accessToken = this.configuration.accessToken;\n if (accessToken) {\n return typeof accessToken === 'function' ? accessToken : () => accessToken;\n }\n return undefined;\n }\n\n get headers(): HTTPHeaders | undefined {\n return this.configuration.headers;\n }\n\n get credentials(): RequestCredentials | undefined {\n return this.configuration.credentials;\n }\n}\n\nexport type Json = any;\nexport type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\nexport type HTTPHeaders = { [key: string]: string };\nexport type HTTPQuery = { [key: string]: string | number | null | boolean | Array | HTTPQuery };\nexport type HTTPBody = Json | FormData | URLSearchParams;\nexport type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original';\n\nexport interface FetchParams {\n url: string;\n init: RequestInit;\n}\n\nexport interface RequestOpts {\n path: string;\n method: HTTPMethod;\n headers: HTTPHeaders;\n query?: HTTPQuery;\n body?: HTTPBody;\n}\n\nexport function exists(json: any, key: string) {\n const value = json[key];\n return value !== null && value !== undefined;\n}\n\nexport function querystring(params: HTTPQuery, prefix: string = ''): string {\n return Object.keys(params)\n .map((key) => {\n const fullKey = prefix + (prefix.length ? `[${key}]` : key);\n const value = params[key];\n if (value instanceof Array) {\n const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue)))\n .join(`&${encodeURIComponent(fullKey)}=`);\n return `${encodeURIComponent(fullKey)}=${multiValue}`;\n }\n if (value instanceof Date) {\n return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;\n }\n if (value instanceof Object) {\n return querystring(value as HTTPQuery, fullKey);\n }\n return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;\n })\n .filter(part => part.length > 0)\n .join('&');\n}\n\nexport function mapValues(data: any, fn: (item: any) => any) {\n return Object.keys(data).reduce(\n (acc, key) => ({ ...acc, [key]: fn(data[key]) }),\n {}\n );\n}\n\nexport function canConsumeForm(consumes: Consume[]): boolean {\n for (const consume of consumes) {\n if ('multipart/form-data' === consume.contentType) {\n return true;\n }\n }\n return false;\n}\n\nexport interface Consume {\n contentType: string\n}\n\nexport interface RequestContext {\n fetch: FetchAPI;\n url: string;\n init: RequestInit;\n}\n\nexport interface ResponseContext {\n fetch: FetchAPI;\n url: string;\n init: RequestInit;\n response: Response;\n}\n\nexport interface Middleware {\n pre?(context: RequestContext): Promise;\n post?(context: ResponseContext): Promise;\n}\n\nexport interface ApiResponse {\n raw: Response;\n value(): Promise;\n}\n\nexport interface ResponseTransformer {\n (json: any): T;\n}\n\nexport class JSONApiResponse {\n constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {}\n\n async value(): Promise {\n return this.transformer(await this.raw.json());\n }\n}\n\nexport class VoidApiResponse {\n constructor(public raw: Response) {}\n\n async value(): Promise {\n return undefined;\n }\n}\n\nexport class BlobApiResponse {\n constructor(public raw: Response) {}\n\n async value(): Promise {\n return await this.raw.blob();\n };\n}\n\nexport class TextApiResponse {\n constructor(public raw: Response) {}\n\n async value(): Promise {\n return await this.raw.text();\n };\n}\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum BenefitTypeCulture {\n Culture = 'culture'\n}\n\nexport function BenefitTypeCultureFromJSON(json: any): BenefitTypeCulture {\n return BenefitTypeCultureFromJSONTyped(json, false);\n}\n\nexport function BenefitTypeCultureFromJSONTyped(json: any, ignoreDiscriminator: boolean): BenefitTypeCulture {\n return json as BenefitTypeCulture;\n}\n\nexport function BenefitTypeCultureToJSON(value?: BenefitTypeCulture | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum BenefitTypeExcercise {\n Exercise = 'exercise'\n}\n\nexport function BenefitTypeExcerciseFromJSON(json: any): BenefitTypeExcercise {\n return BenefitTypeExcerciseFromJSONTyped(json, false);\n}\n\nexport function BenefitTypeExcerciseFromJSONTyped(json: any, ignoreDiscriminator: boolean): BenefitTypeExcercise {\n return json as BenefitTypeExcercise;\n}\n\nexport function BenefitTypeExcerciseToJSON(value?: BenefitTypeExcercise | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum ClientLessonAction {\n Add = 'add',\n Delete = 'delete'\n}\n\nexport function ClientLessonActionFromJSON(json: any): ClientLessonAction {\n return ClientLessonActionFromJSONTyped(json, false);\n}\n\nexport function ClientLessonActionFromJSONTyped(json: any, ignoreDiscriminator: boolean): ClientLessonAction {\n return json as ClientLessonAction;\n}\n\nexport function ClientLessonActionToJSON(value?: ClientLessonAction | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * Default catalog courses sorting order\n * @export\n * @enum {string}\n */\nexport enum CourseSortOrder {\n Code = 'code',\n Name = 'name',\n Date = 'date',\n Datedesc = 'datedesc'\n}\n\nexport function CourseSortOrderFromJSON(json: any): CourseSortOrder {\n return CourseSortOrderFromJSONTyped(json, false);\n}\n\nexport function CourseSortOrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): CourseSortOrder {\n return json as CourseSortOrder;\n}\n\nexport function CourseSortOrderToJSON(value?: CourseSortOrder | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum CourseSortfield {\n Id = 'id',\n Code = 'code',\n Name = 'name',\n Begins = 'begins'\n}\n\nexport function CourseSortfieldFromJSON(json: any): CourseSortfield {\n return CourseSortfieldFromJSONTyped(json, false);\n}\n\nexport function CourseSortfieldFromJSONTyped(json: any, ignoreDiscriminator: boolean): CourseSortfield {\n return json as CourseSortfield;\n}\n\nexport function CourseSortfieldToJSON(value?: CourseSortfield | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * Interfaces for registration viewing\n * @export\n * @interface Contact\n */\nexport interface Contact {\n [key: string]: object | any;\n /**\n * First name\n * @type {string}\n * @memberof Contact\n */\n firstname?: string;\n /**\n * Last name or company name\n * @type {string}\n * @memberof Contact\n */\n lastname?: string;\n /**\n * E-Mail address\n * @type {string}\n * @memberof Contact\n */\n email?: string;\n /**\n * Phone number\n * @type {string}\n * @memberof Contact\n */\n phone?: string;\n}\n\nexport function ContactFromJSON(json: any): Contact {\n return ContactFromJSONTyped(json, false);\n}\n\nexport function ContactFromJSONTyped(json: any, ignoreDiscriminator: boolean): Contact {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'firstname': !exists(json, 'firstname') ? undefined : json['firstname'],\n 'lastname': !exists(json, 'lastname') ? undefined : json['lastname'],\n 'email': !exists(json, 'email') ? undefined : json['email'],\n 'phone': !exists(json, 'phone') ? undefined : json['phone'],\n };\n}\n\nexport function ContactToJSON(value?: Contact | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'firstname': value.firstname,\n 'lastname': value.lastname,\n 'email': value.email,\n 'phone': value.phone,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n CpuProduct,\n CpuProductFromJSON,\n CpuProductFromJSONTyped,\n CpuProductToJSON,\n} from './';\n\n/**\n * Fields for generating payment form\n * @export\n * @interface CpuPayment\n */\nexport interface CpuPayment {\n [key: string]: object | any;\n /**\n * Version number of the API. Use the version number in the title of this document (e.g. \"3.0.1\").\n * \n * Numeric string. Maximum length 11.\n * @type {string}\n * @memberof CpuPayment\n */\n apiVersion: CpuPaymentApiVersionEnum;\n /**\n * Alphanumeric identifier for the source system.\n * \n * This is source-system-specific information created by CPU Oy.\n * @type {string}\n * @memberof CpuPayment\n */\n source: string;\n /**\n * Alphanumeric identifier for the payment.\n * \n * An identifier for the payment inside the source system (e.g. order number).\n * \n * Hellewi adds timestamp in front of referencenumber to counter Cpu dupicate payment checks\n * \n * Maximum length 40.\n * @type {string}\n * @memberof CpuPayment\n */\n id: string;\n /**\n * The operational mode of the interface.\n * \n * value always 3\n * @type {string}\n * @memberof CpuPayment\n */\n mode: CpuPaymentModeEnum;\n /**\n * Action to be performed.\n * - 'new payment' When creating a new normal payment\n * @type {string}\n * @memberof CpuPayment\n */\n action: CpuPaymentActionEnum;\n /**\n * Payment-specific free-form description.\n * \n * E.g. customer name. The description is included as the heading preceding\n * the product sale information in the email confirmation sent to the customer following payment.\n * \n * Maximum length 100, no HTML code\n * @type {string}\n * @memberof CpuPayment\n */\n description?: string;\n /**\n * List of product sales events.\n * \n * Information on separate product sales divided by the following product-sales-specific parameters:\n * @type {Array}\n * @memberof CpuPayment\n */\n products: Array;\n /**\n * Customer email address.\n * \n * The web shop sends confirmations of payments made to this address.\n * If an email address is not provided as an interface parameter, the web shop will prompt the customer\n * to provide one upon navigation to the payment section.\n * \n * Maximum length 100\n * @type {string}\n * @memberof CpuPayment\n */\n email?: string;\n /**\n * The customer's first name.\n * If a name is not provided as an interface parameter, the web shop will prompt the customer\n * to provide one upon navigation to the payment section.\n * \n * Maximum length 100\n * @type {string}\n * @memberof CpuPayment\n */\n firstName?: string;\n /**\n * The customer’s last name.\n * If a name is not provided as an interface parameter, the web shop will prompt the customer\n * to provide one upon navigation to the payment section.\n * \n * Maximum length 100\n * @type {string}\n * @memberof CpuPayment\n */\n lastName?: string;\n /**\n * The desired language version of the online payment interface.\n * The available language versions depend on the implementation of the online payment interface.\n * \n * Maximum length 2\n * @type {string}\n * @memberof CpuPayment\n */\n language?: string;\n /**\n * The return address back to the source system.\n * The web shop directs the customer to this address after a payment has been made or cancelled\n * and includes the order parameters as normal GET parameters. For more on the parameters, see 3.3.1.\n * \n * Return parameters:\n * Id: Alphanumeric identifier for the payment.\n * The identifier for the payment internal to the source system and sent in the request for creating a payment.\n * Status: The status of the payment.\n * For more information on payment statuses, see 3.6.\n * Reference: Web shop order number.\n * Payment identifier used internally by the web shop (cpu).\n * Payments[]: List of payment transactions used to pay the payment. Provided for statistical purposes and if the\n * payment details needs to be printed from the source system.\n * - Not transmitted for payments that are in progress, unsuccessful or cancelled.\n * - Note that only the latest payment is transmitted when customer is returned back to the source system.\n * A complete list of payments are sent in the programmatic payment notification sent as HTTP POST in JSON format.\n * PaymentMethod: Payment method code. For more information, see 3.7.\n * PaymentSum: Payment sum in cents.\n * Timestamp: Timestamp of the payment in format of YYYYMMDDHHNN.\n * PaymentDescription: Additional payment description as preformatted text. May include multiple lines.\n * Returned when payment was created with action \"new subscription\" or \"new subscription payment\".\n * Hash: Checksum A SHA-256 checksum calculated from the string including the transmitted parameter values and a secret key.\n * Used to check the correctness of the response message. For more information, see 3.3.3.\n * \n * Maximum length 1000\n * @type {string}\n * @memberof CpuPayment\n */\n returnAddress: string;\n /**\n * The address of the source system for programme contacts.\n * Using the HTTP POST method, the web shop sends a response in JSON format to this address.\n * Its parameters are described under 3.4.1.\n * \n * Parameters:\n * Id: Alphanumeric identifier for the payment.\n * The identifier for the payment internal to the source system and sent in the request for creating a payment.\n * Status: The status of the payment.\n * For more information on payment statuses, see 3.6.\n * Reference: Web shop order number.\n * Payment identifier used internally by the web shop (cpu).\n * Payments[]: List of payment transactions used to pay the payment. Provided for statistical purposes and if the\n * payment details needs to be printed from the source system.\n * PaymentMethod: Payment method code. For more information, see 3.7.\n * PaymentSum: Payment sum in cents.\n * Timestamp: Timestamp of the payment in format of YYYYMMDDHHNN.\n * PaymentDescription: Additional payment description as preformatted text. May include multiple lines.\n * Returned when payment was created with action \"new subscription\" or \"new subscription payment\".\n * Hash: Checksum A SHA-256 checksum calculated from the string including the transmitted parameter values and a secret key.\n * Used to check the correctness of the response message. For more information, see 3.3.3.\n * Maximum length 1000\n * @type {string}\n * @memberof CpuPayment\n */\n notificationAddress: string;\n /**\n * Required when action is “new subscription payment”.\n * \n * Maximum length 100\n * @type {string}\n * @memberof CpuPayment\n */\n subscriptionCode?: string;\n /**\n * The expected period in days for recurring payments related to this subscription.\n * Required when action is “new subscription”.\n * \n * positive integer 7-999\n * @type {string}\n * @memberof CpuPayment\n */\n subscriptionPeriod?: string;\n /**\n * Datetime when the subscription ends - no payments can be done after this. Can be left empty, which means that\n * the recurring payment is valid and can be used until the payer's credit card expiry date.\n * If payer's credit card expires, the payments will fail so submitting larger value than that has the same\n * effect than leaving this empty.\n * \n * YYYYMMDDHHNN 202104302359\n * @type {string}\n * @memberof CpuPayment\n */\n subscriptionEnd?: string;\n /**\n * Checksum.\n * A SHA-256 checksum calculated from a string including the parameter values and the secret\n * key used by the web shop to verify the received payment request.\n * @type {string}\n * @memberof CpuPayment\n */\n hash: string;\n}\n\n/**\n* @export\n* @enum {string}\n*/\nexport enum CpuPaymentApiVersionEnum {\n _301 = '3.0.1'\n}/**\n* @export\n* @enum {string}\n*/\nexport enum CpuPaymentModeEnum {\n _3 = '3'\n}/**\n* @export\n* @enum {string}\n*/\nexport enum CpuPaymentActionEnum {\n NewPayment = 'new payment'\n}\n\nexport function CpuPaymentFromJSON(json: any): CpuPayment {\n return CpuPaymentFromJSONTyped(json, false);\n}\n\nexport function CpuPaymentFromJSONTyped(json: any, ignoreDiscriminator: boolean): CpuPayment {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'apiVersion': json['ApiVersion'],\n 'source': json['Source'],\n 'id': json['Id'],\n 'mode': json['Mode'],\n 'action': json['Action'],\n 'description': !exists(json, 'Description') ? undefined : json['Description'],\n 'products': ((json['Products'] as Array).map(CpuProductFromJSON)),\n 'email': !exists(json, 'Email') ? undefined : json['Email'],\n 'firstName': !exists(json, 'FirstName') ? undefined : json['FirstName'],\n 'lastName': !exists(json, 'LastName') ? undefined : json['LastName'],\n 'language': !exists(json, 'Language') ? undefined : json['Language'],\n 'returnAddress': json['ReturnAddress'],\n 'notificationAddress': json['NotificationAddress'],\n 'subscriptionCode': !exists(json, 'SubscriptionCode') ? undefined : json['SubscriptionCode'],\n 'subscriptionPeriod': !exists(json, 'SubscriptionPeriod') ? undefined : json['SubscriptionPeriod'],\n 'subscriptionEnd': !exists(json, 'SubscriptionEnd') ? undefined : json['SubscriptionEnd'],\n 'hash': json['Hash'],\n };\n}\n\nexport function CpuPaymentToJSON(value?: CpuPayment | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'ApiVersion': value.apiVersion,\n 'Source': value.source,\n 'Id': value.id,\n 'Mode': value.mode,\n 'Action': value.action,\n 'Description': value.description,\n 'Products': ((value.products as Array).map(CpuProductToJSON)),\n 'Email': value.email,\n 'FirstName': value.firstName,\n 'LastName': value.lastName,\n 'Language': value.language,\n 'ReturnAddress': value.returnAddress,\n 'NotificationAddress': value.notificationAddress,\n 'SubscriptionCode': value.subscriptionCode,\n 'SubscriptionPeriod': value.subscriptionPeriod,\n 'SubscriptionEnd': value.subscriptionEnd,\n 'Hash': value.hash,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum CpuResponseStatus {\n NUMBER_1 = 1,\n NUMBER_0 = 0,\n NUMBER_2 = 2,\n NUMBER_3 = 3,\n NUMBER_4 = 4,\n NUMBER_97 = 97,\n NUMBER_98 = 98,\n NUMBER_99 = 99\n}\n\nexport function CpuResponseStatusFromJSON(json: any): CpuResponseStatus {\n return CpuResponseStatusFromJSONTyped(json, false);\n}\n\nexport function CpuResponseStatusFromJSONTyped(json: any, ignoreDiscriminator: boolean): CpuResponseStatus {\n return json as CpuResponseStatus;\n}\n\nexport function CpuResponseStatusToJSON(value?: CpuResponseStatus | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum ErrorItemType {\n Missing = 'missing',\n MissingField = 'missingField',\n Invalid = 'invalid',\n AlreadyExists = 'alreadyExists',\n Custom = 'custom'\n}\n\nexport function ErrorItemTypeFromJSON(json: any): ErrorItemType {\n return ErrorItemTypeFromJSONTyped(json, false);\n}\n\nexport function ErrorItemTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean): ErrorItemType {\n return json as ErrorItemType;\n}\n\nexport function ErrorItemTypeToJSON(value?: ErrorItemType | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum HellewiCartItemType {\n Course = 'course',\n Lesson = 'lesson'\n}\n\nexport function HellewiCartItemTypeFromJSON(json: any): HellewiCartItemType {\n return HellewiCartItemTypeFromJSONTyped(json, false);\n}\n\nexport function HellewiCartItemTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCartItemType {\n return json as HellewiCartItemType;\n}\n\nexport function HellewiCartItemTypeToJSON(value?: HellewiCartItemType | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface CpuProduct\n */\nexport interface CpuProduct {\n [key: string]: object | any;\n /**\n * Alphanumeric product code in the web shop (cpu payment service backend).\n * \n * Ties the payment to an existing product in the web shop.\n * Among other things, the product name, tax rate and posting used in bookkeeping are\n * determined automatically in the point-of-sale system by this code.\n * \n * Maximum length 25\n * @type {string}\n * @memberof CpuProduct\n */\n code: string;\n /**\n * Number of products (default 1). integer\n * \n * For refunds, a negative value must be used.\n * @type {number}\n * @memberof CpuProduct\n */\n amount?: number;\n /**\n * Unit price in cents including tax.\n * \n * positive integer\n * @type {number}\n * @memberof CpuProduct\n */\n price: number;\n /**\n * Free-form description of product sale.\n * \n * Included for each product line in the email confirmation sent to the customer following payment.\n * It can also be included in the point-of-sale system’s product sales reports.\n * \n * Maximum length 100, no HTML code\n * @type {string}\n * @memberof CpuProduct\n */\n description?: string;\n /**\n * Tax rate code of the point-of-sale system in the web shop.\n * \n * Among other things, this determines the VAT rate and the tax account used in bookkeeping.\n * Must be entered in situations where the tax rate for the sales is different to the\n * web shop’s default tax rate for the product.\n * \n * Maximum length 3\n * @type {string}\n * @memberof CpuProduct\n */\n taxcode?: string;\n}\n\nexport function CpuProductFromJSON(json: any): CpuProduct {\n return CpuProductFromJSONTyped(json, false);\n}\n\nexport function CpuProductFromJSONTyped(json: any, ignoreDiscriminator: boolean): CpuProduct {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'code': json['Code'],\n 'amount': !exists(json, 'Amount') ? undefined : json['Amount'],\n 'price': json['Price'],\n 'description': !exists(json, 'Description') ? undefined : json['Description'],\n 'taxcode': !exists(json, 'Taxcode') ? undefined : json['Taxcode'],\n };\n}\n\nexport function CpuProductToJSON(value?: CpuProduct | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'Code': value.code,\n 'Amount': value.amount,\n 'Price': value.price,\n 'Description': value.description,\n 'Taxcode': value.taxcode,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface Geopoint\n */\nexport interface Geopoint {\n [key: string]: object | any;\n /**\n * Latitude\n * @type {number}\n * @memberof Geopoint\n */\n lat: number;\n /**\n * Longitude\n * @type {number}\n * @memberof Geopoint\n */\n lon: number;\n}\n\nexport function GeopointFromJSON(json: any): Geopoint {\n return GeopointFromJSONTyped(json, false);\n}\n\nexport function GeopointFromJSONTyped(json: any, ignoreDiscriminator: boolean): Geopoint {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'lat': json['lat'],\n 'lon': json['lon'],\n };\n}\n\nexport function GeopointToJSON(value?: Geopoint | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'lat': value.lat,\n 'lon': value.lon,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiAgeLimits\n */\nexport interface HellewiAgeLimits {\n [key: string]: object | any;\n /**\n * \n * @type {number}\n * @memberof HellewiAgeLimits\n */\n minAge?: number;\n /**\n * \n * @type {number}\n * @memberof HellewiAgeLimits\n */\n maxAge?: number;\n}\n\nexport function HellewiAgeLimitsFromJSON(json: any): HellewiAgeLimits {\n return HellewiAgeLimitsFromJSONTyped(json, false);\n}\n\nexport function HellewiAgeLimitsFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiAgeLimits {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'minAge': !exists(json, 'minAge') ? undefined : json['minAge'],\n 'maxAge': !exists(json, 'maxAge') ? undefined : json['maxAge'],\n };\n}\n\nexport function HellewiAgeLimitsToJSON(value?: HellewiAgeLimits | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'minAge': value.minAge,\n 'maxAge': value.maxAge,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiLocation,\n HellewiLocationFromJSON,\n HellewiLocationFromJSONTyped,\n HellewiLocationToJSON,\n HellewiTenantType,\n HellewiTenantTypeFromJSON,\n HellewiTenantTypeFromJSONTyped,\n HellewiTenantTypeToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiBrand\n */\nexport interface HellewiBrand {\n [key: string]: object | any;\n /**\n * Tenant identifier\n * @type {string}\n * @memberof HellewiBrand\n */\n tenant: string;\n /**\n * Tenant's name\n * @type {string}\n * @memberof HellewiBrand\n */\n name: string;\n /**\n * URL for tenant's logo\n * @type {string}\n * @memberof HellewiBrand\n */\n logo?: string;\n /**\n * URL to tenant's homepage\n * @type {string}\n * @memberof HellewiBrand\n */\n homepage?: string;\n /**\n * URL to tenant's Twitter account\n * @type {string}\n * @memberof HellewiBrand\n */\n twitter?: string;\n /**\n * URL to tenant's Facebook account\n * @type {string}\n * @memberof HellewiBrand\n */\n facebook?: string;\n /**\n * URL to tenant's Instagram account\n * @type {string}\n * @memberof HellewiBrand\n */\n instagram?: string;\n /**\n * URL to tenant's LinkedIn account\n * @type {string}\n * @memberof HellewiBrand\n */\n linkedin?: string;\n /**\n * \n * @type {HellewiLocation}\n * @memberof HellewiBrand\n */\n location?: HellewiLocation;\n /**\n * Tenant's customer service phone number\n * @type {string}\n * @memberof HellewiBrand\n */\n phone?: string;\n /**\n * E-Mail address\n * @type {string}\n * @memberof HellewiBrand\n */\n email?: string;\n /**\n * Tenant's gtag\n * @type {string}\n * @memberof HellewiBrand\n */\n gtag?: string;\n /**\n * Tenant's cookiebot\n * @type {string}\n * @memberof HellewiBrand\n */\n cookiebot?: string;\n /**\n * Tenant's metapixel\n * @type {string}\n * @memberof HellewiBrand\n */\n metapixel?: string;\n /**\n * Tenant's tradedoubler\n * @type {string}\n * @memberof HellewiBrand\n */\n tradedoubler?: string;\n /**\n * Tenant's custom scripts\n * @type {string}\n * @memberof HellewiBrand\n */\n scripts?: string;\n /**\n * \n * @type {HellewiTenantType}\n * @memberof HellewiBrand\n */\n type: HellewiTenantType;\n /**\n * Main color\n * @type {string}\n * @memberof HellewiBrand\n */\n color?: string;\n /**\n * Complementary color\n * @type {string}\n * @memberof HellewiBrand\n */\n complementarycolor?: string;\n /**\n * Default font\n * @type {string}\n * @memberof HellewiBrand\n */\n font?: string;\n /**\n * Footer text\n * @type {string}\n * @memberof HellewiBrand\n */\n footer?: string;\n /**\n * Header font\n * @type {string}\n * @memberof HellewiBrand\n */\n headerfont?: string;\n /**\n * Hero-header title\n * @type {string}\n * @memberof HellewiBrand\n */\n herotitle?: string;\n /**\n * Hero-header image\n * @type {string}\n * @memberof HellewiBrand\n */\n heroimage?: string;\n /**\n * Hero-header text\n * @type {string}\n * @memberof HellewiBrand\n */\n herotext?: string;\n /**\n * Highlight color\n * @type {string}\n * @memberof HellewiBrand\n */\n highlightcolor?: string;\n /**\n * Company billing\n * @type {boolean}\n * @memberof HellewiBrand\n */\n onlycompanybilling?: boolean;\n}\n\nexport function HellewiBrandFromJSON(json: any): HellewiBrand {\n return HellewiBrandFromJSONTyped(json, false);\n}\n\nexport function HellewiBrandFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiBrand {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'tenant': json['tenant'],\n 'name': json['name'],\n 'logo': !exists(json, 'logo') ? undefined : json['logo'],\n 'homepage': !exists(json, 'homepage') ? undefined : json['homepage'],\n 'twitter': !exists(json, 'twitter') ? undefined : json['twitter'],\n 'facebook': !exists(json, 'facebook') ? undefined : json['facebook'],\n 'instagram': !exists(json, 'instagram') ? undefined : json['instagram'],\n 'linkedin': !exists(json, 'linkedin') ? undefined : json['linkedin'],\n 'location': !exists(json, 'location') ? undefined : HellewiLocationFromJSON(json['location']),\n 'phone': !exists(json, 'phone') ? undefined : json['phone'],\n 'email': !exists(json, 'email') ? undefined : json['email'],\n 'gtag': !exists(json, 'gtag') ? undefined : json['gtag'],\n 'cookiebot': !exists(json, 'cookiebot') ? undefined : json['cookiebot'],\n 'metapixel': !exists(json, 'metapixel') ? undefined : json['metapixel'],\n 'tradedoubler': !exists(json, 'tradedoubler') ? undefined : json['tradedoubler'],\n 'scripts': !exists(json, 'scripts') ? undefined : json['scripts'],\n 'type': HellewiTenantTypeFromJSON(json['type']),\n 'color': !exists(json, 'color') ? undefined : json['color'],\n 'complementarycolor': !exists(json, 'complementarycolor') ? undefined : json['complementarycolor'],\n 'font': !exists(json, 'font') ? undefined : json['font'],\n 'footer': !exists(json, 'footer') ? undefined : json['footer'],\n 'headerfont': !exists(json, 'headerfont') ? undefined : json['headerfont'],\n 'herotitle': !exists(json, 'herotitle') ? undefined : json['herotitle'],\n 'heroimage': !exists(json, 'heroimage') ? undefined : json['heroimage'],\n 'herotext': !exists(json, 'herotext') ? undefined : json['herotext'],\n 'highlightcolor': !exists(json, 'highlightcolor') ? undefined : json['highlightcolor'],\n 'onlycompanybilling': !exists(json, 'onlycompanybilling') ? undefined : json['onlycompanybilling'],\n };\n}\n\nexport function HellewiBrandToJSON(value?: HellewiBrand | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'tenant': value.tenant,\n 'name': value.name,\n 'logo': value.logo,\n 'homepage': value.homepage,\n 'twitter': value.twitter,\n 'facebook': value.facebook,\n 'instagram': value.instagram,\n 'linkedin': value.linkedin,\n 'location': HellewiLocationToJSON(value.location),\n 'phone': value.phone,\n 'email': value.email,\n 'gtag': value.gtag,\n 'cookiebot': value.cookiebot,\n 'metapixel': value.metapixel,\n 'tradedoubler': value.tradedoubler,\n 'scripts': value.scripts,\n 'type': HellewiTenantTypeToJSON(value.type),\n 'color': value.color,\n 'complementarycolor': value.complementarycolor,\n 'font': value.font,\n 'footer': value.footer,\n 'headerfont': value.headerfont,\n 'herotitle': value.herotitle,\n 'heroimage': value.heroimage,\n 'herotext': value.herotext,\n 'highlightcolor': value.highlightcolor,\n 'onlycompanybilling': value.onlycompanybilling,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiCallout\n */\nexport interface HellewiCallout {\n [key: string]: object | any;\n /**\n * Callouts text\n * @type {string}\n * @memberof HellewiCallout\n */\n text?: string;\n /**\n * Callouts publishdate\n * @type {Date}\n * @memberof HellewiCallout\n */\n startsAt?: Date;\n}\n\nexport function HellewiCalloutFromJSON(json: any): HellewiCallout {\n return HellewiCalloutFromJSONTyped(json, false);\n}\n\nexport function HellewiCalloutFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCallout {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'text': !exists(json, 'text') ? undefined : json['text'],\n 'startsAt': !exists(json, 'startsAt') ? undefined : (new Date(json['startsAt'])),\n };\n}\n\nexport function HellewiCalloutToJSON(value?: HellewiCallout | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'text': value.text,\n 'startsAt': value.startsAt === undefined ? undefined : (value.startsAt.toISOString()),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiCartItemId,\n HellewiCartItemIdFromJSON,\n HellewiCartItemIdFromJSONTyped,\n HellewiCartItemIdToJSON,\n HellewiCartItemType,\n HellewiCartItemTypeFromJSON,\n HellewiCartItemTypeFromJSONTyped,\n HellewiCartItemTypeToJSON,\n HellewiCourseLesson,\n HellewiCourseLessonFromJSON,\n HellewiCourseLessonFromJSONTyped,\n HellewiCourseLessonToJSON,\n HellewiCoursePartial,\n HellewiCoursePartialFromJSON,\n HellewiCoursePartialFromJSONTyped,\n HellewiCoursePartialToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCartItem\n */\nexport interface HellewiCartItem {\n [key: string]: object | any;\n /**\n * Cart item id\n * @type {number}\n * @memberof HellewiCartItem\n */\n id: number;\n /**\n * \n * @type {HellewiCartItemType}\n * @memberof HellewiCartItem\n */\n type: HellewiCartItemType;\n /**\n * \n * @type {HellewiCartItemId}\n * @memberof HellewiCartItem\n */\n itemid: HellewiCartItemId;\n /**\n * Time when this item was added to cart\n * @type {Date}\n * @memberof HellewiCartItem\n */\n timestamp: Date;\n /**\n * Is this cart on a spare place.\n * \n * This is always present if type is 'course'\n * @type {boolean}\n * @memberof HellewiCartItem\n */\n spare?: boolean;\n /**\n * Position in the spare place queue.\n * \n * This might not be strictly accurate as carts might expire without actual\n * registration, and in this case, counters are not updated for the following\n * cart items.\n * \n * This is always present if type is 'course'\n * @type {number}\n * @memberof HellewiCartItem\n */\n sparecounter?: number;\n /**\n * \n * @type {HellewiCoursePartial}\n * @memberof HellewiCartItem\n */\n course: HellewiCoursePartial;\n /**\n * \n * @type {HellewiCourseLesson}\n * @memberof HellewiCartItem\n */\n lesson?: HellewiCourseLesson;\n}\n\nexport function HellewiCartItemFromJSON(json: any): HellewiCartItem {\n return HellewiCartItemFromJSONTyped(json, false);\n}\n\nexport function HellewiCartItemFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCartItem {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n 'type': HellewiCartItemTypeFromJSON(json['type']),\n 'itemid': HellewiCartItemIdFromJSON(json['itemid']),\n 'timestamp': (new Date(json['timestamp'])),\n 'spare': !exists(json, 'spare') ? undefined : json['spare'],\n 'sparecounter': !exists(json, 'sparecounter') ? undefined : json['sparecounter'],\n 'course': HellewiCoursePartialFromJSON(json['course']),\n 'lesson': !exists(json, 'lesson') ? undefined : HellewiCourseLessonFromJSON(json['lesson']),\n };\n}\n\nexport function HellewiCartItemToJSON(value?: HellewiCartItem | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'type': HellewiCartItemTypeToJSON(value.type),\n 'itemid': HellewiCartItemIdToJSON(value.itemid),\n 'timestamp': (value.timestamp.toISOString()),\n 'spare': value.spare,\n 'sparecounter': value.sparecounter,\n 'course': HellewiCoursePartialToJSON(value.course),\n 'lesson': HellewiCourseLessonToJSON(value.lesson),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiCartItemId\n */\nexport interface HellewiCartItemId {\n [key: string]: object | any;\n /**\n * Course ID\n * \n * This is either\n * - a number as string (single-tenant endpoints, e.g. `'3'`)\n * - string with tenant id (in lowercase) and course id number (multi-tenant\n * endpoints, e.g. `'demo.opistopalvelut.fi-3'`)\n * \n * Validation pattern: `^(?:[a-z0-9-.]+-)?[0-9]+$`\n * @type {string}\n * @memberof HellewiCartItemId\n */\n id: string;\n /**\n * Lesson ID\n * @type {number}\n * @memberof HellewiCartItemId\n */\n lessonid?: number;\n /**\n * Hmac expiry time\n * @type {string}\n * @memberof HellewiCartItemId\n */\n expiry?: string;\n /**\n * Hmac request id\n * @type {string}\n * @memberof HellewiCartItemId\n */\n reqid?: string;\n /**\n * Id of the unlisted course\n * @type {string}\n * @memberof HellewiCartItemId\n */\n unlistedid?: string;\n /**\n * Hmac is used to allow unlisted courses to be added to cart\n * @type {string}\n * @memberof HellewiCartItemId\n */\n hmac?: string;\n}\n\nexport function HellewiCartItemIdFromJSON(json: any): HellewiCartItemId {\n return HellewiCartItemIdFromJSONTyped(json, false);\n}\n\nexport function HellewiCartItemIdFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCartItemId {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n 'lessonid': !exists(json, 'lessonid') ? undefined : json['lessonid'],\n 'expiry': !exists(json, 'expiry') ? undefined : json['expiry'],\n 'reqid': !exists(json, 'reqid') ? undefined : json['reqid'],\n 'unlistedid': !exists(json, 'unlistedid') ? undefined : json['unlistedid'],\n 'hmac': !exists(json, 'hmac') ? undefined : json['hmac'],\n };\n}\n\nexport function HellewiCartItemIdToJSON(value?: HellewiCartItemId | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'lessonid': value.lessonid,\n 'expiry': value.expiry,\n 'reqid': value.reqid,\n 'unlistedid': value.unlistedid,\n 'hmac': value.hmac,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiCartStatus\n */\nexport interface HellewiCartStatus {\n [key: string]: object | any;\n /**\n * Number of items in cart\n * @type {number}\n * @memberof HellewiCartStatus\n */\n count: number;\n /**\n * Time left in cart, in seconds\n * @type {number}\n * @memberof HellewiCartStatus\n */\n timeleft?: number;\n}\n\nexport function HellewiCartStatusFromJSON(json: any): HellewiCartStatus {\n return HellewiCartStatusFromJSONTyped(json, false);\n}\n\nexport function HellewiCartStatusFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCartStatus {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'count': json['count'],\n 'timeleft': !exists(json, 'timeleft') ? undefined : json['timeleft'],\n };\n}\n\nexport function HellewiCartStatusToJSON(value?: HellewiCartStatus | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'count': value.count,\n 'timeleft': value.timeleft,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiCatalogItem,\n HellewiCatalogItemFromJSON,\n HellewiCatalogItemFromJSONTyped,\n HellewiCatalogItemToJSON,\n} from './';\n\n/**\n * Catalog with all different CatalogItems grouped and sorted\n * @export\n * @interface HellewiCatalog\n */\nexport interface HellewiCatalog {\n [key: string]: object | any;\n /**\n * Department\n * \n * These are configurable by tenant, and they usually contain for example the\n * city which organizes the courses (e.g. Tampere, Ylöjärvi, Kuru, Viljakkala).\n * \n * Department, Category and Subject form a tree: a category's parent is always\n * a department (or undefined), and subject's parent is always a category\n * (or undefined).\n * @type {Array}\n * @memberof HellewiCatalog\n */\n department: Array;\n /**\n * Category\n * \n * Higher-level course categorisation. These are for example arts, languages,\n * health and such.\n * \n * These are also configurable by tenant, so different tenant's categories\n * are not comparable with each others.\n * \n * Category can have department as parent, and it can have multiple subjects\n * as children.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n category: Array;\n /**\n * Subject\n * \n * Lower-level course categorisation. These are for example English, French,\n * German under languages-category.\n * \n * Subject's parent is a category (if any).\n * @type {Array}\n * @memberof HellewiCatalog\n */\n subject: Array;\n /**\n * Course type\n * @type {Array}\n * @memberof HellewiCatalog\n */\n coursetype: Array;\n /**\n * Classification\n * \n * High level classification of courses. These are based on education sectors.\n * \n * These are static for all the tenants, and can be used when combining data\n * from multiple tenants.\n * \n * Education sectors are classified the following way (itcode and\n * educationsector's name):\n * \n * **1: Sports and wellness / Liikunta ja hyvinvointi / Motion och välmående**\n * \n * - 752 Liikunta ja urheilu\n * - 799 Muu sosiaali-, terveys- ja liikunta-alan koulutus\n * - 751 Terveysala ja hammashuolto\n * - 101 Vapaa-aika- ja nuorisotyö\n * \n * **2: Crafts / Käsityö / Hantverk**\n * \n * - 201 Käsi- ja taideteollisuus ja käden taidot\n * - 508 Tekstiili- ja vaatetusala\n * \n * **3: Languages / Kielet / Språk**\n * \n * - 10201 Kielitiede\n * - 10202 Suomi\n * - 10203 Ruotsi\n * - 10204 Englanti\n * - 10205 Saksa\n * - 10206 Ranska\n * - 10207 Venäjä\n * - 10208 Espanja\n * - 10209 Italia\n * - 10299 Muut kielet\n * \n * **4: Music / Musiikki / Musik**\n * \n * - 205 Musiikki\n * \n * **5: Arts / Kuvataide / Bildkonst och formgivning**\n * \n * - 206 Kuvataide\n * \n * **6: Dance and theater / Tanssi ja teatteri / Dans och teater**\n * \n * - 204 Teatteri ja tanssi\n * \n * **7: Technology and business / Tekniikka ja talous / Teknik och ekonomi**\n * \n * - 40201 Tietokoneen ajokorttikoulutus\n * - 40299 Muu tietotekniikan hyväksikäyttö\n * - 504 Tieto- ja tietoliikennetekniikka\n * - 505 Graafinen ja viestintätekniikka\n * - 202 Viestintä- ja informaatioala\n * - 301 Liiketalous ja kauppa\n * - 351 Yrittäjyys ja yrittäjyyskasvatus\n * - 302 Kansantalous\n * - 304 Tilastointi ja tilastotiede\n * - 401 Matematiikka\n * - 451 Fysiikka ja kemia sekä geo-, avaruus- ja tähtitiet\n * - 452 Biologia ja maantiede\n * - 499 Muu luonnontietteiden alan koulutus\n * - 501 Arkkitehtuuri ja rakentaminen\n * - 502 Kone-, metalli- ja energiatekniikka\n * - 503 Sähkö- ja automaatiotekniikka\n * - 506 Elintarvikeala ja biotekniikka\n * - 507 Prosessi-, kemian ja materiaalitekniikka\n * - 509 Ajoneuvo- ja kuljetusala\n * - 510 Tuotantotalous\n * - 599 Muu tekniikan ja liikenteen alan koulutus\n * \n * **8: Society and humanities / Yhteiskunta ja humanismi / Samhälle och humaniora**\n * \n * - 203 Kirjallisuus\n * - 207 Kulttuurin- ja taiteiden tutkimus\n * - 151 Opetus- ja kasvatustyö ja psykologia\n * - 103 Historia ja arkeologia\n * - 199 Muu humanistisen ja kasvatusalan koulutus\n * - 399 Muu yhteiskunnallisten aineiden, liiketalouden ja\n * - 104 Filosofia\n * - 107 Teologia\n * - 305 Sosiaalitieteet\n * - 306 Politiikka ja politiikkatieteet\n * - 307 Oikeuskäytäntö ja oikeustieteet\n * \n * **9: Nature and environment / Luonto ja ympäristö / natur och miljö**\n * \n * - 602 Puutarhatalous ja puutarhanhoito\n * - 605 Luonto- ja ympäristöala\n * - 651 Maatila- ja metsätalous\n * - 603 Kalatalous ja kalastus\n * - 699 Muu luonnonvara- ja ympäristöalan koulutus\n * \n * **10: Food, drink and travel / Ruoka, juoma ja matkailu / Mat, dryck och resor**\n * \n * - 802 Majoitus- ja ravitsemisala sekä ruoan valmistus\n * - 851 Kotitalous- ja kuluttajapalvelut sekä puhdistus\n * - 801 Matkailuala\n * - 899 Muu matkailu-, ravitsemis- ja talousalan koulutus\n * \n * **99: Others / Muut / Övriga**\n * \n * - 999 Muu koulutus\n * - 99 Muu yleissivistävä koulutus\n * - 2 Perusopetus\n * - 3 Lukiokoulutus\n * - 51 Oppimisvalmiuksien kehittäminen ja motivointi\n * - 299 Muu kulttuurialan koulutus\n * - 30301 Kansalais- ja järjestötoiminta\n * - 30399 Muu hallinnon alan koulutus\n * - 701 Sosiaaliala\n * - 753 Farmasia ja muu lääkehuolto sekä tekniset terveysp\n * - 709 Eläinlääketiede\n * - 710 Kauneudenhoitoala\n * - 901 Sotilas- ja rajavartioala\n * - 902 Palo- ja pelastusala\n * - 951 Poliisi- ja vartiointiala\n * @type {Array}\n * @memberof HellewiCatalog\n */\n classification: Array;\n /**\n * Education sector (koulutusala)\n * \n * Lower level course categorisation. This follows the Statistics Finland's\n * [National Classification of Education](https://www.stat.fi/fi/luokitukset/koulutus),\n * although an older version of it.\n * \n * Education sector's parent is a classification\n * @type {Array}\n * @memberof HellewiCatalog\n */\n educationsector: Array;\n /**\n * Education type\n * @type {Array}\n * @memberof HellewiCatalog\n */\n educationtype: Array;\n /**\n * Level of study\n * \n * These are levels for how courses compare with each others, e.g. beginner,\n * intermediate, advanced. For language courses for example, the level of\n * study can follow CEFR levels A1, A2, B1, etc.\n * \n * These are configurable by the tenant, so different tenant's categories are\n * not comparable with each others.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n levelofstudy: Array;\n /**\n * Teaching format\n * \n * Teaching format of the course.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n teachingformat: Array;\n /**\n * Language\n * \n * Language used in the course.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n language: Array;\n /**\n * Location\n * \n * Where course is held.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n location: Array;\n /**\n * Locationgroup\n * \n * Where course is held.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n locationgroup: Array;\n /**\n * Term (lukuvuosi)\n * \n * Years in education usually start in the autumn and continue until summer.\n * Term is this time period, usually divided into autumn and spring semesters\n * (catalog item period).\n * @type {Array}\n * @memberof HellewiCatalog\n */\n term: Array;\n /**\n * Period (lukukausi)\n * \n * Courses usually have a certain period during which they are held. There are\n * usually two periods during a term: autumn and spring (syys- ja kevätlukukausi).\n * \n * Period's parent is a term.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n period: Array;\n /**\n * Tag\n * \n * Custom tag for a course, these can be defined by the tenant.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n tag: Array;\n /**\n * Tenant\n * \n * Tenant that is organizing the courses. This makes only in multi-tenant context,\n * in single-tenant there will be only one tenant here.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n tenant: Array;\n /**\n * Unit\n * \n * Unit that is organizing the course.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n unit: Array;\n /**\n * Weekday\n * \n * On which day(s) the course is held.\n * @type {Array}\n * @memberof HellewiCatalog\n */\n weekday: Array;\n /**\n * Date\n * \n * Filters for courses dates\n * @type {Array}\n * @memberof HellewiCatalog\n */\n date: Array;\n}\n\nexport function HellewiCatalogFromJSON(json: any): HellewiCatalog {\n return HellewiCatalogFromJSONTyped(json, false);\n}\n\nexport function HellewiCatalogFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCatalog {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'department': ((json['department'] as Array).map(HellewiCatalogItemFromJSON)),\n 'category': ((json['category'] as Array).map(HellewiCatalogItemFromJSON)),\n 'subject': ((json['subject'] as Array).map(HellewiCatalogItemFromJSON)),\n 'coursetype': ((json['coursetype'] as Array).map(HellewiCatalogItemFromJSON)),\n 'classification': ((json['classification'] as Array).map(HellewiCatalogItemFromJSON)),\n 'educationsector': ((json['educationsector'] as Array).map(HellewiCatalogItemFromJSON)),\n 'educationtype': ((json['educationtype'] as Array).map(HellewiCatalogItemFromJSON)),\n 'levelofstudy': ((json['levelofstudy'] as Array).map(HellewiCatalogItemFromJSON)),\n 'teachingformat': ((json['teachingformat'] as Array).map(HellewiCatalogItemFromJSON)),\n 'language': ((json['language'] as Array).map(HellewiCatalogItemFromJSON)),\n 'location': ((json['location'] as Array).map(HellewiCatalogItemFromJSON)),\n 'locationgroup': ((json['locationgroup'] as Array).map(HellewiCatalogItemFromJSON)),\n 'term': ((json['term'] as Array).map(HellewiCatalogItemFromJSON)),\n 'period': ((json['period'] as Array).map(HellewiCatalogItemFromJSON)),\n 'tag': ((json['tag'] as Array).map(HellewiCatalogItemFromJSON)),\n 'tenant': ((json['tenant'] as Array).map(HellewiCatalogItemFromJSON)),\n 'unit': ((json['unit'] as Array).map(HellewiCatalogItemFromJSON)),\n 'weekday': ((json['weekday'] as Array).map(HellewiCatalogItemFromJSON)),\n 'date': ((json['date'] as Array).map(HellewiCatalogItemFromJSON)),\n };\n}\n\nexport function HellewiCatalogToJSON(value?: HellewiCatalog | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'department': ((value.department as Array).map(HellewiCatalogItemToJSON)),\n 'category': ((value.category as Array).map(HellewiCatalogItemToJSON)),\n 'subject': ((value.subject as Array).map(HellewiCatalogItemToJSON)),\n 'coursetype': ((value.coursetype as Array).map(HellewiCatalogItemToJSON)),\n 'classification': ((value.classification as Array).map(HellewiCatalogItemToJSON)),\n 'educationsector': ((value.educationsector as Array).map(HellewiCatalogItemToJSON)),\n 'educationtype': ((value.educationtype as Array).map(HellewiCatalogItemToJSON)),\n 'levelofstudy': ((value.levelofstudy as Array).map(HellewiCatalogItemToJSON)),\n 'teachingformat': ((value.teachingformat as Array).map(HellewiCatalogItemToJSON)),\n 'language': ((value.language as Array).map(HellewiCatalogItemToJSON)),\n 'location': ((value.location as Array).map(HellewiCatalogItemToJSON)),\n 'locationgroup': ((value.locationgroup as Array).map(HellewiCatalogItemToJSON)),\n 'term': ((value.term as Array).map(HellewiCatalogItemToJSON)),\n 'period': ((value.period as Array).map(HellewiCatalogItemToJSON)),\n 'tag': ((value.tag as Array).map(HellewiCatalogItemToJSON)),\n 'tenant': ((value.tenant as Array).map(HellewiCatalogItemToJSON)),\n 'unit': ((value.unit as Array).map(HellewiCatalogItemToJSON)),\n 'weekday': ((value.weekday as Array).map(HellewiCatalogItemToJSON)),\n 'date': ((value.date as Array).map(HellewiCatalogItemToJSON)),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum HellewiCatalogItemType {\n Category = 'category',\n Categorysubject = 'categorysubject',\n Classification = 'classification',\n Coursetype = 'coursetype',\n Department = 'department',\n Educationsector = 'educationsector',\n Educationtype = 'educationtype',\n Language = 'language',\n Levelofstudy = 'levelofstudy',\n Location = 'location',\n Locationgroup = 'locationgroup',\n Period = 'period',\n Subject = 'subject',\n Tag = 'tag',\n Tenant = 'tenant',\n Teachingformat = 'teachingformat',\n Term = 'term',\n Unit = 'unit',\n Weekday = 'weekday',\n Date = 'date',\n Dateinput = 'dateinput',\n Coursesbeginning = 'coursesbeginning',\n Registrationopen = 'registrationopen'\n}\n\nexport function HellewiCatalogItemTypeFromJSON(json: any): HellewiCatalogItemType {\n return HellewiCatalogItemTypeFromJSONTyped(json, false);\n}\n\nexport function HellewiCatalogItemTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCatalogItemType {\n return json as HellewiCatalogItemType;\n}\n\nexport function HellewiCatalogItemTypeToJSON(value?: HellewiCatalogItemType | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiCatalogItemType,\n HellewiCatalogItemTypeFromJSON,\n HellewiCatalogItemTypeFromJSONTyped,\n HellewiCatalogItemTypeToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCatalogItem\n */\nexport interface HellewiCatalogItem {\n [key: string]: object | any;\n /**\n * \n * @type {HellewiCatalogItemType}\n * @memberof HellewiCatalogItem\n */\n type: HellewiCatalogItemType;\n /**\n * Hellewi ID\n * @type {number}\n * @memberof HellewiCatalogItem\n */\n id?: number;\n /**\n * Keywords that can be used to match filters\n * @type {Array}\n * @memberof HellewiCatalogItem\n */\n keywords?: Array;\n /**\n * Parent keyword\n * @type {string}\n * @memberof HellewiCatalogItem\n */\n parent?: string;\n /**\n * Color\n * @type {string}\n * @memberof HellewiCatalogItem\n */\n color?: string;\n /**\n * Name\n * @type {string}\n * @memberof HellewiCatalogItem\n */\n name: string;\n /**\n * Sorting order\n * @type {number}\n * @memberof HellewiCatalogItem\n */\n sort?: number;\n /**\n * Course count\n * \n * How many courses have this catalog item with current search parameters.\n * This attribute is present only in [Catalog](#operation/Catalog) endpoint.\n * @type {number}\n * @memberof HellewiCatalogItem\n */\n coursecount?: number;\n /**\n * Translate label\n * \n * Whether the label should be translated in the user interface.\n * @type {boolean}\n * @memberof HellewiCatalogItem\n */\n translatelabel?: boolean;\n}\n\nexport function HellewiCatalogItemFromJSON(json: any): HellewiCatalogItem {\n return HellewiCatalogItemFromJSONTyped(json, false);\n}\n\nexport function HellewiCatalogItemFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCatalogItem {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'type': HellewiCatalogItemTypeFromJSON(json['type']),\n 'id': !exists(json, 'id') ? undefined : json['id'],\n 'keywords': !exists(json, 'keywords') ? undefined : json['keywords'],\n 'parent': !exists(json, 'parent') ? undefined : json['parent'],\n 'color': !exists(json, 'color') ? undefined : json['color'],\n 'name': json['name'],\n 'sort': !exists(json, 'sort') ? undefined : json['sort'],\n 'coursecount': !exists(json, 'coursecount') ? undefined : json['coursecount'],\n 'translatelabel': !exists(json, 'translatelabel') ? undefined : json['translatelabel'],\n };\n}\n\nexport function HellewiCatalogItemToJSON(value?: HellewiCatalogItem | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'type': HellewiCatalogItemTypeToJSON(value.type),\n 'id': value.id,\n 'keywords': value.keywords,\n 'parent': value.parent,\n 'color': value.color,\n 'name': value.name,\n 'sort': value.sort,\n 'coursecount': value.coursecount,\n 'translatelabel': value.translatelabel,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiCatalogItemType,\n HellewiCatalogItemTypeFromJSON,\n HellewiCatalogItemTypeFromJSONTyped,\n HellewiCatalogItemTypeToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCatalogItemText\n */\nexport interface HellewiCatalogItemText {\n [key: string]: object | any;\n /**\n * \n * @type {HellewiCatalogItemType}\n * @memberof HellewiCatalogItemText\n */\n type: HellewiCatalogItemType;\n /**\n * Hellewi ID\n * @type {number}\n * @memberof HellewiCatalogItemText\n */\n id?: number;\n /**\n * Keywords that can be used to match filters\n * @type {Array}\n * @memberof HellewiCatalogItemText\n */\n keywords?: Array;\n /**\n * Parent keyword\n * @type {string}\n * @memberof HellewiCatalogItemText\n */\n parent?: string;\n /**\n * Color\n * @type {string}\n * @memberof HellewiCatalogItemText\n */\n color?: string;\n /**\n * Name\n * @type {string}\n * @memberof HellewiCatalogItemText\n */\n name: string;\n /**\n * Sorting order\n * @type {number}\n * @memberof HellewiCatalogItemText\n */\n sort?: number;\n /**\n * Course count\n * \n * How many courses have this catalog item with current search parameters.\n * This attribute is present only in [Catalog](#operation/Catalog) endpoint.\n * @type {number}\n * @memberof HellewiCatalogItemText\n */\n coursecount?: number;\n /**\n * Translate label\n * \n * Whether the label should be translated in the user interface.\n * @type {boolean}\n * @memberof HellewiCatalogItemText\n */\n translatelabel?: boolean;\n /**\n * \n * @type {string}\n * @memberof HellewiCatalogItemText\n */\n description?: string;\n}\n\nexport function HellewiCatalogItemTextFromJSON(json: any): HellewiCatalogItemText {\n return HellewiCatalogItemTextFromJSONTyped(json, false);\n}\n\nexport function HellewiCatalogItemTextFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCatalogItemText {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'type': HellewiCatalogItemTypeFromJSON(json['type']),\n 'id': !exists(json, 'id') ? undefined : json['id'],\n 'keywords': !exists(json, 'keywords') ? undefined : json['keywords'],\n 'parent': !exists(json, 'parent') ? undefined : json['parent'],\n 'color': !exists(json, 'color') ? undefined : json['color'],\n 'name': json['name'],\n 'sort': !exists(json, 'sort') ? undefined : json['sort'],\n 'coursecount': !exists(json, 'coursecount') ? undefined : json['coursecount'],\n 'translatelabel': !exists(json, 'translatelabel') ? undefined : json['translatelabel'],\n 'description': !exists(json, 'description') ? undefined : json['description'],\n };\n}\n\nexport function HellewiCatalogItemTextToJSON(value?: HellewiCatalogItemText | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'type': HellewiCatalogItemTypeToJSON(value.type),\n 'id': value.id,\n 'keywords': value.keywords,\n 'parent': value.parent,\n 'color': value.color,\n 'name': value.name,\n 'sort': value.sort,\n 'coursecount': value.coursecount,\n 'translatelabel': value.translatelabel,\n 'description': value.description,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n CourseSortOrder,\n CourseSortOrderFromJSON,\n CourseSortOrderFromJSONTyped,\n CourseSortOrderToJSON,\n HellewiCatalogSettingsEnabledCatalogItemTypes,\n HellewiCatalogSettingsEnabledCatalogItemTypesFromJSON,\n HellewiCatalogSettingsEnabledCatalogItemTypesFromJSONTyped,\n HellewiCatalogSettingsEnabledCatalogItemTypesToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCatalogSettings\n */\nexport interface HellewiCatalogSettings {\n [key: string]: object | any;\n /**\n * \n * @type {HellewiCatalogSettingsEnabledCatalogItemTypes}\n * @memberof HellewiCatalogSettings\n */\n enabledcatalogitemtypes: HellewiCatalogSettingsEnabledCatalogItemTypes;\n /**\n * \n * @type {CourseSortOrder}\n * @memberof HellewiCatalogSettings\n */\n defaultCourseSortOrder?: CourseSortOrder;\n}\n\nexport function HellewiCatalogSettingsFromJSON(json: any): HellewiCatalogSettings {\n return HellewiCatalogSettingsFromJSONTyped(json, false);\n}\n\nexport function HellewiCatalogSettingsFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCatalogSettings {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'enabledcatalogitemtypes': HellewiCatalogSettingsEnabledCatalogItemTypesFromJSON(json['enabledcatalogitemtypes']),\n 'defaultCourseSortOrder': !exists(json, 'defaultCourseSortOrder') ? undefined : CourseSortOrderFromJSON(json['defaultCourseSortOrder']),\n };\n}\n\nexport function HellewiCatalogSettingsToJSON(value?: HellewiCatalogSettings | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'enabledcatalogitemtypes': HellewiCatalogSettingsEnabledCatalogItemTypesToJSON(value.enabledcatalogitemtypes),\n 'defaultCourseSortOrder': CourseSortOrderToJSON(value.defaultCourseSortOrder),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * Settings for showing catalog in user interface\n * @export\n * @interface HellewiCatalogSettingsEnabledCatalogItemTypes\n */\nexport interface HellewiCatalogSettingsEnabledCatalogItemTypes {\n [key: string]: object | any;\n /**\n * Department catalog items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n department: boolean;\n /**\n * Category catalog items should be shown as a single list\n * \n * I.e. select only the category catalog items with parent: null\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n category: boolean;\n /**\n * Category and subject catalog items should be shown as a tree\n * \n * Categories with parent: null and subjects with each respective\n * category as parent.\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n categorysubject: boolean;\n /**\n * Course type\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n coursetype: boolean;\n /**\n * Education type\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n educationtype: boolean;\n /**\n * Category catalog items should be shown as a single list\n * \n * I.e. select only the category catalog items with parent: null\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n subject: boolean;\n /**\n * Location catalog items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n location: boolean;\n /**\n * Locationgroup catalog items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n locationgroup: boolean;\n /**\n * Period catalog items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n period: boolean;\n /**\n * Weekday catalog items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n weekday: boolean;\n /**\n * Custom course tags should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n tag: boolean;\n /**\n * Education sectors should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n educationsector: boolean;\n /**\n * Level of study items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n levelofstudy: boolean;\n /**\n * Teaching format items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n teachingformat: boolean;\n /**\n * Language items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n language: boolean;\n /**\n * Unit items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n unit: boolean;\n /**\n * Date items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n date: boolean;\n /**\n * Date items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n coursesbeginning: boolean;\n /**\n * Date items should be shown as a single list\n * @type {boolean}\n * @memberof HellewiCatalogSettingsEnabledCatalogItemTypes\n */\n registrationopen: boolean;\n}\n\nexport function HellewiCatalogSettingsEnabledCatalogItemTypesFromJSON(json: any): HellewiCatalogSettingsEnabledCatalogItemTypes {\n return HellewiCatalogSettingsEnabledCatalogItemTypesFromJSONTyped(json, false);\n}\n\nexport function HellewiCatalogSettingsEnabledCatalogItemTypesFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCatalogSettingsEnabledCatalogItemTypes {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'department': json['department'],\n 'category': json['category'],\n 'categorysubject': json['categorysubject'],\n 'coursetype': json['coursetype'],\n 'educationtype': json['educationtype'],\n 'subject': json['subject'],\n 'location': json['location'],\n 'locationgroup': json['locationgroup'],\n 'period': json['period'],\n 'weekday': json['weekday'],\n 'tag': json['tag'],\n 'educationsector': json['educationsector'],\n 'levelofstudy': json['levelofstudy'],\n 'teachingformat': json['teachingformat'],\n 'language': json['language'],\n 'unit': json['unit'],\n 'date': json['date'],\n 'coursesbeginning': json['coursesbeginning'],\n 'registrationopen': json['registrationopen'],\n };\n}\n\nexport function HellewiCatalogSettingsEnabledCatalogItemTypesToJSON(value?: HellewiCatalogSettingsEnabledCatalogItemTypes | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'department': value.department,\n 'category': value.category,\n 'categorysubject': value.categorysubject,\n 'coursetype': value.coursetype,\n 'educationtype': value.educationtype,\n 'subject': value.subject,\n 'location': value.location,\n 'locationgroup': value.locationgroup,\n 'period': value.period,\n 'weekday': value.weekday,\n 'tag': value.tag,\n 'educationsector': value.educationsector,\n 'levelofstudy': value.levelofstudy,\n 'teachingformat': value.teachingformat,\n 'language': value.language,\n 'unit': value.unit,\n 'date': value.date,\n 'coursesbeginning': value.coursesbeginning,\n 'registrationopen': value.registrationopen,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum HellewiCourseNotificationLabel {\n Success = 'success',\n Danger = 'danger',\n Warning = 'warning',\n Info = 'info'\n}\n\nexport function HellewiCourseNotificationLabelFromJSON(json: any): HellewiCourseNotificationLabel {\n return HellewiCourseNotificationLabelFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseNotificationLabelFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourseNotificationLabel {\n return json as HellewiCourseNotificationLabel;\n}\n\nexport function HellewiCourseNotificationLabelToJSON(value?: HellewiCourseNotificationLabel | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * Course statuses\n * \n * - `NOT_YET_STARTED` Course has not yet started\n * - `IN_PROGRESS` Course is in progress\n * - `ENDED` Course has ended\n * - `CANCELLED` Course has been cancelled (before it started)\n * - `INTERRUPTED` Course has been interrupted (after it started)\n * \n * Currently only one of the statuses can be active at the same time, but\n * it is also possible that none of them are.\n * \n * Statuses can be added in the future, so the field is an array, and\n * implementation cannot make any assumptions that the requested status\n * would be always in index 0.\n * @export\n * @enum {string}\n */\nexport enum HellewiCourseStatus {\n NotYetStarted = 'NOT_YET_STARTED',\n InProgress = 'IN_PROGRESS',\n Ended = 'ENDED',\n Cancelled = 'CANCELLED',\n Interrupted = 'INTERRUPTED',\n RegistrationToLessons = 'REGISTRATION_TO_LESSONS'\n}\n\nexport function HellewiCourseStatusFromJSON(json: any): HellewiCourseStatus {\n return HellewiCourseStatusFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseStatusFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourseStatus {\n return json as HellewiCourseStatus;\n}\n\nexport function HellewiCourseStatusToJSON(value?: HellewiCourseStatus | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum HellewiLocationSortfields {\n Id = 'id',\n Name = 'name'\n}\n\nexport function HellewiLocationSortfieldsFromJSON(json: any): HellewiLocationSortfields {\n return HellewiLocationSortfieldsFromJSONTyped(json, false);\n}\n\nexport function HellewiLocationSortfieldsFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiLocationSortfields {\n return json as HellewiLocationSortfields;\n}\n\nexport function HellewiLocationSortfieldsToJSON(value?: HellewiLocationSortfields | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum HellewiReservationSortfields {\n Id = 'id',\n Date = 'date',\n StartTime = 'startTime',\n EndTime = 'endTime'\n}\n\nexport function HellewiReservationSortfieldsFromJSON(json: any): HellewiReservationSortfields {\n return HellewiReservationSortfieldsFromJSONTyped(json, false);\n}\n\nexport function HellewiReservationSortfieldsFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiReservationSortfields {\n return json as HellewiReservationSortfields;\n}\n\nexport function HellewiReservationSortfieldsToJSON(value?: HellewiReservationSortfields | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiAgeLimits,\n HellewiAgeLimitsFromJSON,\n HellewiAgeLimitsFromJSONTyped,\n HellewiAgeLimitsToJSON,\n HellewiCatalogItem,\n HellewiCatalogItemFromJSON,\n HellewiCatalogItemFromJSONTyped,\n HellewiCatalogItemToJSON,\n HellewiCourseDay,\n HellewiCourseDayFromJSON,\n HellewiCourseDayFromJSONTyped,\n HellewiCourseDayToJSON,\n HellewiCourseLesson,\n HellewiCourseLessonFromJSON,\n HellewiCourseLessonFromJSONTyped,\n HellewiCourseLessonToJSON,\n HellewiCourseNotification,\n HellewiCourseNotificationFromJSON,\n HellewiCourseNotificationFromJSONTyped,\n HellewiCourseNotificationToJSON,\n HellewiCoursePartial,\n HellewiCoursePartialFromJSON,\n HellewiCoursePartialFromJSONTyped,\n HellewiCoursePartialToJSON,\n HellewiCoursePeriod,\n HellewiCoursePeriodFromJSON,\n HellewiCoursePeriodFromJSONTyped,\n HellewiCoursePeriodToJSON,\n HellewiCoursePrice,\n HellewiCoursePriceFromJSON,\n HellewiCoursePriceFromJSONTyped,\n HellewiCoursePriceToJSON,\n HellewiCourseProduct,\n HellewiCourseProductFromJSON,\n HellewiCourseProductFromJSONTyped,\n HellewiCourseProductToJSON,\n HellewiCourseStatus,\n HellewiCourseStatusFromJSON,\n HellewiCourseStatusFromJSONTyped,\n HellewiCourseStatusToJSON,\n HellewiFile,\n HellewiFileFromJSON,\n HellewiFileFromJSONTyped,\n HellewiFileToJSON,\n HellewiImage,\n HellewiImageFromJSON,\n HellewiImageFromJSONTyped,\n HellewiImageToJSON,\n HellewiLanguage,\n HellewiLanguageFromJSON,\n HellewiLanguageFromJSONTyped,\n HellewiLanguageToJSON,\n HellewiLocation,\n HellewiLocationFromJSON,\n HellewiLocationFromJSONTyped,\n HellewiLocationToJSON,\n HellewiParticipantCount,\n HellewiParticipantCountFromJSON,\n HellewiParticipantCountFromJSONTyped,\n HellewiParticipantCountToJSON,\n HellewiTag,\n HellewiTagFromJSON,\n HellewiTagFromJSONTyped,\n HellewiTagToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCourse\n */\nexport interface HellewiCourse {\n [key: string]: object | any;\n /**\n * Course ID\n * \n * This is either\n * - a number as string (single-tenant endpoints, e.g. `'3'`)\n * - string with tenant id (in lowercase) and course id number (multi-tenant\n * endpoints, e.g. `'demo.opistopalvelut.fi-3'`)\n * \n * Validation pattern: `^(?:[a-z0-9-.]+-)?[0-9]+$`\n * @type {string}\n * @memberof HellewiCourse\n */\n id: string;\n /**\n * Code / koodi\n * @type {string}\n * @memberof HellewiCourse\n */\n code?: string;\n /**\n * Name\n * @type {string}\n * @memberof HellewiCourse\n */\n name?: string;\n /**\n * Tenant\n * @type {string}\n * @memberof HellewiCourse\n */\n tenant: string;\n /**\n * Course statuses\n * \n * - `NOT_YET_STARTED` Course has not yet started\n * - `IN_PROGRESS` Course is in progress\n * - `ENDED` Course has ended\n * - `CANCELLED` Course has been cancelled (before it started)\n * - `INTERRUPTED` Course has been interrupted (after it started)\n * \n * Currently only one of the statuses can be active at the same time, but\n * it is also possible that none of them are.\n * \n * Statuses can be added in the future, so the field is an array, and\n * implementation cannot make any assumptions that the requested status\n * would be always in index 0.\n * @type {Array}\n * @memberof HellewiCourse\n */\n statuses: Array;\n /**\n * Course begins on date\n * @type {Date}\n * @memberof HellewiCourse\n */\n begins?: Date;\n /**\n * Course ends on date\n * @type {Date}\n * @memberof HellewiCourse\n */\n ends?: Date;\n /**\n * Registration begins on date/time\n * \n * null means that registration is not open\n * @type {Date}\n * @memberof HellewiCourse\n */\n registrationbegins?: Date;\n /**\n * Registration ends on date/time, soft limit\n * \n * If this time is in the past, registrations are still accepted, but\n * user interfaces should show this field as time when registrations close\n * @type {Date}\n * @memberof HellewiCourse\n */\n registrationendssoft?: Date;\n /**\n * Registration ends on date/time, hard limit\n * \n * null means that registration has no ending limit\n * @type {Date}\n * @memberof HellewiCourse\n */\n registrationendshard?: Date;\n /**\n * Whether registration is open at the moment\n * \n * See rules in [Registration](#section/Registration)\n * @type {boolean}\n * @memberof HellewiCourse\n */\n registrationopen?: boolean;\n /**\n * Teacher\n * @type {string}\n * @memberof HellewiCourse\n */\n teacher?: string;\n /**\n * ECTS credits / opintopisteet\n * @type {number}\n * @memberof HellewiCourse\n */\n ectscredits?: number;\n /**\n * Topical / ajankohtainen\n * \n * This course should be emphasized\n * @type {boolean}\n * @memberof HellewiCourse\n */\n topical?: boolean;\n /**\n * Day(s) / kurssipäivät\n * @type {Array}\n * @memberof HellewiCourse\n */\n days?: Array;\n /**\n * \n * @type {HellewiLocation}\n * @memberof HellewiCourse\n */\n location?: HellewiLocation;\n /**\n * Notification\n * @type {Array}\n * @memberof HellewiCourse\n */\n notifications?: Array;\n /**\n * Periods and terms / lukukaudet ja lukuvuodet\n * @type {Array}\n * @memberof HellewiCourse\n */\n periods?: Array;\n /**\n * \n * @type {HellewiLanguage}\n * @memberof HellewiCourse\n */\n language?: HellewiLanguage;\n /**\n * Tags\n * @type {Array}\n * @memberof HellewiCourse\n */\n tags?: Array;\n /**\n * Prices\n * @type {Array}\n * @memberof HellewiCourse\n */\n prices?: Array;\n /**\n * \n * @type {HellewiCoursePartial}\n * @memberof HellewiCourse\n */\n moduleparent?: HellewiCoursePartial;\n /**\n * Child courses for a module parent course\n * @type {Array}\n * @memberof HellewiCourse\n */\n modulechildren?: Array;\n /**\n * Catalog items\n * \n * Different item types are explained under [GetCatalog](#operation/GetCatalog)\n * response\n * @type {Array}\n * @memberof HellewiCourse\n */\n catalogitems: Array;\n /**\n * \n * @type {HellewiAgeLimits}\n * @memberof HellewiCourse\n */\n ageLimits?: HellewiAgeLimits;\n /**\n * Course products\n * @type {Array}\n * @memberof HellewiCourse\n */\n courseProducts?: Array;\n /**\n * \n * @type {HellewiCatalogItem}\n * @memberof HellewiCourse\n */\n department?: HellewiCatalogItem;\n /**\n * \n * @type {HellewiCatalogItem}\n * @memberof HellewiCourse\n */\n category?: HellewiCatalogItem;\n /**\n * \n * @type {HellewiCatalogItem}\n * @memberof HellewiCourse\n */\n subject?: HellewiCatalogItem;\n /**\n * Description\n * @type {string}\n * @memberof HellewiCourse\n */\n description?: string;\n /**\n * Additionalinfo\n * @type {string}\n * @memberof HellewiCourse\n */\n additionalinfo?: string;\n /**\n * Ask about the course / kysy kurssista\n * @type {string}\n * @memberof HellewiCourse\n */\n askabout?: string;\n /**\n * Learningobjectives\n * @type {string}\n * @memberof HellewiCourse\n */\n learningobjectives?: string;\n /**\n * Evaluationcriteria\n * @type {string}\n * @memberof HellewiCourse\n */\n evaluationcriteria?: string;\n /**\n * Keywords that can be used to match filters\n * @type {Array}\n * @memberof HellewiCourse\n */\n keywords?: Array;\n /**\n * Lessons\n * @type {Array}\n * @memberof HellewiCourse\n */\n lessons?: Array;\n /**\n * Metakeywords\n * @type {string}\n * @memberof HellewiCourse\n */\n metakeywords?: string;\n /**\n * \n * @type {HellewiParticipantCount}\n * @memberof HellewiCourse\n */\n participantcount?: HellewiParticipantCount;\n /**\n * Link for course registration\n * \n * This opens Hellewi registration application registration form with this\n * course added to cart.\n * @type {string}\n * @memberof HellewiCourse\n */\n registrationlink?: string;\n /**\n * Files\n * @type {Array}\n * @memberof HellewiCourse\n */\n files?: Array;\n /**\n * Images\n * @type {Array}\n * @memberof HellewiCourse\n */\n images?: Array;\n}\n\nexport function HellewiCourseFromJSON(json: any): HellewiCourse {\n return HellewiCourseFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourse {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n 'code': !exists(json, 'code') ? undefined : json['code'],\n 'name': !exists(json, 'name') ? undefined : json['name'],\n 'tenant': json['tenant'],\n 'statuses': ((json['statuses'] as Array).map(HellewiCourseStatusFromJSON)),\n 'begins': !exists(json, 'begins') ? undefined : (new Date(json['begins'])),\n 'ends': !exists(json, 'ends') ? undefined : (new Date(json['ends'])),\n 'registrationbegins': !exists(json, 'registrationbegins') ? undefined : (new Date(json['registrationbegins'])),\n 'registrationendssoft': !exists(json, 'registrationendssoft') ? undefined : (new Date(json['registrationendssoft'])),\n 'registrationendshard': !exists(json, 'registrationendshard') ? undefined : (new Date(json['registrationendshard'])),\n 'registrationopen': !exists(json, 'registrationopen') ? undefined : json['registrationopen'],\n 'teacher': !exists(json, 'teacher') ? undefined : json['teacher'],\n 'ectscredits': !exists(json, 'ectscredits') ? undefined : json['ectscredits'],\n 'topical': !exists(json, 'topical') ? undefined : json['topical'],\n 'days': !exists(json, 'days') ? undefined : ((json['days'] as Array).map(HellewiCourseDayFromJSON)),\n 'location': !exists(json, 'location') ? undefined : HellewiLocationFromJSON(json['location']),\n 'notifications': !exists(json, 'notifications') ? undefined : ((json['notifications'] as Array).map(HellewiCourseNotificationFromJSON)),\n 'periods': !exists(json, 'periods') ? undefined : ((json['periods'] as Array).map(HellewiCoursePeriodFromJSON)),\n 'language': !exists(json, 'language') ? undefined : HellewiLanguageFromJSON(json['language']),\n 'tags': !exists(json, 'tags') ? undefined : ((json['tags'] as Array).map(HellewiTagFromJSON)),\n 'prices': !exists(json, 'prices') ? undefined : ((json['prices'] as Array).map(HellewiCoursePriceFromJSON)),\n 'moduleparent': !exists(json, 'moduleparent') ? undefined : HellewiCoursePartialFromJSON(json['moduleparent']),\n 'modulechildren': !exists(json, 'modulechildren') ? undefined : ((json['modulechildren'] as Array).map(HellewiCoursePartialFromJSON)),\n 'catalogitems': ((json['catalogitems'] as Array).map(HellewiCatalogItemFromJSON)),\n 'ageLimits': !exists(json, 'ageLimits') ? undefined : HellewiAgeLimitsFromJSON(json['ageLimits']),\n 'courseProducts': !exists(json, 'courseProducts') ? undefined : ((json['courseProducts'] as Array).map(HellewiCourseProductFromJSON)),\n 'department': !exists(json, 'department') ? undefined : HellewiCatalogItemFromJSON(json['department']),\n 'category': !exists(json, 'category') ? undefined : HellewiCatalogItemFromJSON(json['category']),\n 'subject': !exists(json, 'subject') ? undefined : HellewiCatalogItemFromJSON(json['subject']),\n 'description': !exists(json, 'description') ? undefined : json['description'],\n 'additionalinfo': !exists(json, 'additionalinfo') ? undefined : json['additionalinfo'],\n 'askabout': !exists(json, 'askabout') ? undefined : json['askabout'],\n 'learningobjectives': !exists(json, 'learningobjectives') ? undefined : json['learningobjectives'],\n 'evaluationcriteria': !exists(json, 'evaluationcriteria') ? undefined : json['evaluationcriteria'],\n 'keywords': !exists(json, 'keywords') ? undefined : json['keywords'],\n 'lessons': !exists(json, 'lessons') ? undefined : ((json['lessons'] as Array).map(HellewiCourseLessonFromJSON)),\n 'metakeywords': !exists(json, 'metakeywords') ? undefined : json['metakeywords'],\n 'participantcount': !exists(json, 'participantcount') ? undefined : HellewiParticipantCountFromJSON(json['participantcount']),\n 'registrationlink': !exists(json, 'registrationlink') ? undefined : json['registrationlink'],\n 'files': !exists(json, 'files') ? undefined : ((json['files'] as Array).map(HellewiFileFromJSON)),\n 'images': !exists(json, 'images') ? undefined : ((json['images'] as Array).map(HellewiImageFromJSON)),\n };\n}\n\nexport function HellewiCourseToJSON(value?: HellewiCourse | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'code': value.code,\n 'name': value.name,\n 'tenant': value.tenant,\n 'statuses': ((value.statuses as Array).map(HellewiCourseStatusToJSON)),\n 'begins': value.begins === undefined ? undefined : (value.begins.toISOString().substr(0,10)),\n 'ends': value.ends === undefined ? undefined : (value.ends.toISOString().substr(0,10)),\n 'registrationbegins': value.registrationbegins === undefined ? undefined : (value.registrationbegins.toISOString()),\n 'registrationendssoft': value.registrationendssoft === undefined ? undefined : (value.registrationendssoft.toISOString()),\n 'registrationendshard': value.registrationendshard === undefined ? undefined : (value.registrationendshard.toISOString()),\n 'registrationopen': value.registrationopen,\n 'teacher': value.teacher,\n 'ectscredits': value.ectscredits,\n 'topical': value.topical,\n 'days': value.days === undefined ? undefined : ((value.days as Array).map(HellewiCourseDayToJSON)),\n 'location': HellewiLocationToJSON(value.location),\n 'notifications': value.notifications === undefined ? undefined : ((value.notifications as Array).map(HellewiCourseNotificationToJSON)),\n 'periods': value.periods === undefined ? undefined : ((value.periods as Array).map(HellewiCoursePeriodToJSON)),\n 'language': HellewiLanguageToJSON(value.language),\n 'tags': value.tags === undefined ? undefined : ((value.tags as Array).map(HellewiTagToJSON)),\n 'prices': value.prices === undefined ? undefined : ((value.prices as Array).map(HellewiCoursePriceToJSON)),\n 'moduleparent': HellewiCoursePartialToJSON(value.moduleparent),\n 'modulechildren': value.modulechildren === undefined ? undefined : ((value.modulechildren as Array).map(HellewiCoursePartialToJSON)),\n 'catalogitems': ((value.catalogitems as Array).map(HellewiCatalogItemToJSON)),\n 'ageLimits': HellewiAgeLimitsToJSON(value.ageLimits),\n 'courseProducts': value.courseProducts === undefined ? undefined : ((value.courseProducts as Array).map(HellewiCourseProductToJSON)),\n 'department': HellewiCatalogItemToJSON(value.department),\n 'category': HellewiCatalogItemToJSON(value.category),\n 'subject': HellewiCatalogItemToJSON(value.subject),\n 'description': value.description,\n 'additionalinfo': value.additionalinfo,\n 'askabout': value.askabout,\n 'learningobjectives': value.learningobjectives,\n 'evaluationcriteria': value.evaluationcriteria,\n 'keywords': value.keywords,\n 'lessons': value.lessons === undefined ? undefined : ((value.lessons as Array).map(HellewiCourseLessonToJSON)),\n 'metakeywords': value.metakeywords,\n 'participantcount': HellewiParticipantCountToJSON(value.participantcount),\n 'registrationlink': value.registrationlink,\n 'files': value.files === undefined ? undefined : ((value.files as Array).map(HellewiFileToJSON)),\n 'images': value.images === undefined ? undefined : ((value.images as Array).map(HellewiImageToJSON)),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiCourseCount\n */\nexport interface HellewiCourseCount {\n [key: string]: object | any;\n /**\n * Course count\n * @type {number}\n * @memberof HellewiCourseCount\n */\n count: number;\n}\n\nexport function HellewiCourseCountFromJSON(json: any): HellewiCourseCount {\n return HellewiCourseCountFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseCountFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourseCount {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'count': json['count'],\n };\n}\n\nexport function HellewiCourseCountToJSON(value?: HellewiCourseCount | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'count': value.count,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n Weekday,\n WeekdayFromJSON,\n WeekdayFromJSONTyped,\n WeekdayToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCourseDay\n */\nexport interface HellewiCourseDay {\n [key: string]: object | any;\n /**\n * \n * @type {Weekday}\n * @memberof HellewiCourseDay\n */\n weekday?: Weekday;\n /**\n * Time when course begins on this day, as \"HH:MM\"\n * @type {string}\n * @memberof HellewiCourseDay\n */\n begins?: string;\n /**\n * Time when course ends on this day, as \"HH:MM\"\n * @type {string}\n * @memberof HellewiCourseDay\n */\n ends?: string;\n /**\n * Keywords that can be used to match filters\n * @type {Array}\n * @memberof HellewiCourseDay\n */\n keywords?: Array;\n}\n\nexport function HellewiCourseDayFromJSON(json: any): HellewiCourseDay {\n return HellewiCourseDayFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseDayFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourseDay {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'weekday': !exists(json, 'weekday') ? undefined : WeekdayFromJSON(json['weekday']),\n 'begins': !exists(json, 'begins') ? undefined : json['begins'],\n 'ends': !exists(json, 'ends') ? undefined : json['ends'],\n 'keywords': !exists(json, 'keywords') ? undefined : json['keywords'],\n };\n}\n\nexport function HellewiCourseDayToJSON(value?: HellewiCourseDay | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'weekday': WeekdayToJSON(value.weekday),\n 'begins': value.begins,\n 'ends': value.ends,\n 'keywords': value.keywords,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiLessonParticipantCount,\n HellewiLessonParticipantCountFromJSON,\n HellewiLessonParticipantCountFromJSONTyped,\n HellewiLessonParticipantCountToJSON,\n HellewiLocation,\n HellewiLocationFromJSON,\n HellewiLocationFromJSONTyped,\n HellewiLocationToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCourseLesson\n */\nexport interface HellewiCourseLesson {\n [key: string]: object | any;\n /**\n * Lesson ID\n * \n * Used when adding lessons to cart, and registering to them\n * @type {number}\n * @memberof HellewiCourseLesson\n */\n id: number;\n /**\n * Lesson begins at\n * @type {Date}\n * @memberof HellewiCourseLesson\n */\n begins?: Date;\n /**\n * Lesson ends at\n * @type {Date}\n * @memberof HellewiCourseLesson\n */\n ends?: Date;\n /**\n * \n * @type {HellewiLocation}\n * @memberof HellewiCourseLesson\n */\n location?: HellewiLocation;\n /**\n * \n * @type {HellewiLessonParticipantCount}\n * @memberof HellewiCourseLesson\n */\n participantcount?: HellewiLessonParticipantCount;\n}\n\nexport function HellewiCourseLessonFromJSON(json: any): HellewiCourseLesson {\n return HellewiCourseLessonFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseLessonFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourseLesson {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n 'begins': !exists(json, 'begins') ? undefined : (new Date(json['begins'])),\n 'ends': !exists(json, 'ends') ? undefined : (new Date(json['ends'])),\n 'location': !exists(json, 'location') ? undefined : HellewiLocationFromJSON(json['location']),\n 'participantcount': !exists(json, 'participantcount') ? undefined : HellewiLessonParticipantCountFromJSON(json['participantcount']),\n };\n}\n\nexport function HellewiCourseLessonToJSON(value?: HellewiCourseLesson | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'begins': value.begins === undefined ? undefined : (value.begins.toISOString()),\n 'ends': value.ends === undefined ? undefined : (value.ends.toISOString()),\n 'location': HellewiLocationToJSON(value.location),\n 'participantcount': HellewiLessonParticipantCountToJSON(value.participantcount),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiCourseMinimal\n */\nexport interface HellewiCourseMinimal {\n [key: string]: object | any;\n /**\n * Course ID\n * \n * This is either\n * - a number as string (single-tenant endpoints, e.g. `'3'`)\n * - string with tenant id (in lowercase) and course id number (multi-tenant\n * endpoints, e.g. `'demo.opistopalvelut.fi-3'`)\n * \n * Validation pattern: `^(?:[a-z0-9-.]+-)?[0-9]+$`\n * @type {string}\n * @memberof HellewiCourseMinimal\n */\n id: string;\n /**\n * Code / koodi\n * @type {string}\n * @memberof HellewiCourseMinimal\n */\n code?: string;\n /**\n * Name\n * @type {string}\n * @memberof HellewiCourseMinimal\n */\n name?: string;\n /**\n * Tenant\n * @type {string}\n * @memberof HellewiCourseMinimal\n */\n tenant: string;\n}\n\nexport function HellewiCourseMinimalFromJSON(json: any): HellewiCourseMinimal {\n return HellewiCourseMinimalFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseMinimalFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourseMinimal {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n 'code': !exists(json, 'code') ? undefined : json['code'],\n 'name': !exists(json, 'name') ? undefined : json['name'],\n 'tenant': json['tenant'],\n };\n}\n\nexport function HellewiCourseMinimalToJSON(value?: HellewiCourseMinimal | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'code': value.code,\n 'name': value.name,\n 'tenant': value.tenant,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiCourseMinimalParent\n */\nexport interface HellewiCourseMinimalParent {\n [key: string]: object | any;\n /**\n * Course ID\n * \n * This is either\n * - a number as string (single-tenant endpoints, e.g. `'3'`)\n * - string with tenant id (in lowercase) and course id number (multi-tenant\n * endpoints, e.g. `'demo.opistopalvelut.fi-3'`)\n * \n * Validation pattern: `^(?:[a-z0-9-.]+-)?[0-9]+$`\n * @type {string}\n * @memberof HellewiCourseMinimalParent\n */\n id: string;\n /**\n * Code / koodi\n * @type {string}\n * @memberof HellewiCourseMinimalParent\n */\n code?: string;\n /**\n * Name\n * @type {string}\n * @memberof HellewiCourseMinimalParent\n */\n name?: string;\n /**\n * Tenant\n * @type {string}\n * @memberof HellewiCourseMinimalParent\n */\n tenant: string;\n /**\n * Keywords that can be used to match filters\n * @type {Array}\n * @memberof HellewiCourseMinimalParent\n */\n keywords?: Array;\n}\n\nexport function HellewiCourseMinimalParentFromJSON(json: any): HellewiCourseMinimalParent {\n return HellewiCourseMinimalParentFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseMinimalParentFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourseMinimalParent {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n 'code': !exists(json, 'code') ? undefined : json['code'],\n 'name': !exists(json, 'name') ? undefined : json['name'],\n 'tenant': json['tenant'],\n 'keywords': !exists(json, 'keywords') ? undefined : json['keywords'],\n };\n}\n\nexport function HellewiCourseMinimalParentToJSON(value?: HellewiCourseMinimalParent | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'code': value.code,\n 'name': value.name,\n 'tenant': value.tenant,\n 'keywords': value.keywords,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiCourseNotificationLabel,\n HellewiCourseNotificationLabelFromJSON,\n HellewiCourseNotificationLabelFromJSONTyped,\n HellewiCourseNotificationLabelToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCourseNotification\n */\nexport interface HellewiCourseNotification {\n [key: string]: object | any;\n /**\n * \n * @type {HellewiCourseNotificationLabel}\n * @memberof HellewiCourseNotification\n */\n label: HellewiCourseNotificationLabel;\n /**\n * Text\n * @type {string}\n * @memberof HellewiCourseNotification\n */\n text: string;\n}\n\nexport function HellewiCourseNotificationFromJSON(json: any): HellewiCourseNotification {\n return HellewiCourseNotificationFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseNotificationFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourseNotification {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'label': HellewiCourseNotificationLabelFromJSON(json['label']),\n 'text': json['text'],\n };\n}\n\nexport function HellewiCourseNotificationToJSON(value?: HellewiCourseNotification | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'label': HellewiCourseNotificationLabelToJSON(value.label),\n 'text': value.text,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiAgeLimits,\n HellewiAgeLimitsFromJSON,\n HellewiAgeLimitsFromJSONTyped,\n HellewiAgeLimitsToJSON,\n HellewiCatalogItem,\n HellewiCatalogItemFromJSON,\n HellewiCatalogItemFromJSONTyped,\n HellewiCatalogItemToJSON,\n HellewiCourseDay,\n HellewiCourseDayFromJSON,\n HellewiCourseDayFromJSONTyped,\n HellewiCourseDayToJSON,\n HellewiCourseMinimal,\n HellewiCourseMinimalFromJSON,\n HellewiCourseMinimalFromJSONTyped,\n HellewiCourseMinimalToJSON,\n HellewiCourseMinimalParent,\n HellewiCourseMinimalParentFromJSON,\n HellewiCourseMinimalParentFromJSONTyped,\n HellewiCourseMinimalParentToJSON,\n HellewiCourseNotification,\n HellewiCourseNotificationFromJSON,\n HellewiCourseNotificationFromJSONTyped,\n HellewiCourseNotificationToJSON,\n HellewiCoursePeriod,\n HellewiCoursePeriodFromJSON,\n HellewiCoursePeriodFromJSONTyped,\n HellewiCoursePeriodToJSON,\n HellewiCoursePrice,\n HellewiCoursePriceFromJSON,\n HellewiCoursePriceFromJSONTyped,\n HellewiCoursePriceToJSON,\n HellewiCourseProduct,\n HellewiCourseProductFromJSON,\n HellewiCourseProductFromJSONTyped,\n HellewiCourseProductToJSON,\n HellewiCourseStatus,\n HellewiCourseStatusFromJSON,\n HellewiCourseStatusFromJSONTyped,\n HellewiCourseStatusToJSON,\n HellewiLanguage,\n HellewiLanguageFromJSON,\n HellewiLanguageFromJSONTyped,\n HellewiLanguageToJSON,\n HellewiLocation,\n HellewiLocationFromJSON,\n HellewiLocationFromJSONTyped,\n HellewiLocationToJSON,\n HellewiTag,\n HellewiTagFromJSON,\n HellewiTagFromJSONTyped,\n HellewiTagToJSON,\n} from './';\n\n/**\n * Course with limited fields\n * @export\n * @interface HellewiCoursePartial\n */\nexport interface HellewiCoursePartial {\n [key: string]: object | any;\n /**\n * Course ID\n * \n * This is either\n * - a number as string (single-tenant endpoints, e.g. `'3'`)\n * - string with tenant id (in lowercase) and course id number (multi-tenant\n * endpoints, e.g. `'demo.opistopalvelut.fi-3'`)\n * \n * Validation pattern: `^(?:[a-z0-9-.]+-)?[0-9]+$`\n * @type {string}\n * @memberof HellewiCoursePartial\n */\n id: string;\n /**\n * Code / koodi\n * @type {string}\n * @memberof HellewiCoursePartial\n */\n code?: string;\n /**\n * Name\n * @type {string}\n * @memberof HellewiCoursePartial\n */\n name?: string;\n /**\n * Tenant\n * @type {string}\n * @memberof HellewiCoursePartial\n */\n tenant: string;\n /**\n * Course statuses\n * \n * - `NOT_YET_STARTED` Course has not yet started\n * - `IN_PROGRESS` Course is in progress\n * - `ENDED` Course has ended\n * - `CANCELLED` Course has been cancelled (before it started)\n * - `INTERRUPTED` Course has been interrupted (after it started)\n * \n * Currently only one of the statuses can be active at the same time, but\n * it is also possible that none of them are.\n * \n * Statuses can be added in the future, so the field is an array, and\n * implementation cannot make any assumptions that the requested status\n * would be always in index 0.\n * @type {Array}\n * @memberof HellewiCoursePartial\n */\n statuses: Array;\n /**\n * Course begins on date\n * @type {Date}\n * @memberof HellewiCoursePartial\n */\n begins?: Date;\n /**\n * Course ends on date\n * @type {Date}\n * @memberof HellewiCoursePartial\n */\n ends?: Date;\n /**\n * Registration begins on date/time\n * \n * null means that registration is not open\n * @type {Date}\n * @memberof HellewiCoursePartial\n */\n registrationbegins?: Date;\n /**\n * Registration ends on date/time, soft limit\n * \n * If this time is in the past, registrations are still accepted, but\n * user interfaces should show this field as time when registrations close\n * @type {Date}\n * @memberof HellewiCoursePartial\n */\n registrationendssoft?: Date;\n /**\n * Registration ends on date/time, hard limit\n * \n * null means that registration has no ending limit\n * @type {Date}\n * @memberof HellewiCoursePartial\n */\n registrationendshard?: Date;\n /**\n * Whether registration is open at the moment\n * \n * See rules in [Registration](#section/Registration)\n * @type {boolean}\n * @memberof HellewiCoursePartial\n */\n registrationopen?: boolean;\n /**\n * Teacher\n * @type {string}\n * @memberof HellewiCoursePartial\n */\n teacher?: string;\n /**\n * ECTS credits / opintopisteet\n * @type {number}\n * @memberof HellewiCoursePartial\n */\n ectscredits?: number;\n /**\n * Topical / ajankohtainen\n * \n * This course should be emphasized\n * @type {boolean}\n * @memberof HellewiCoursePartial\n */\n topical?: boolean;\n /**\n * Day(s) / kurssipäivät\n * @type {Array}\n * @memberof HellewiCoursePartial\n */\n days?: Array;\n /**\n * \n * @type {HellewiLocation}\n * @memberof HellewiCoursePartial\n */\n location?: HellewiLocation;\n /**\n * Notification\n * @type {Array}\n * @memberof HellewiCoursePartial\n */\n notifications?: Array;\n /**\n * Periods and terms / lukukaudet ja lukuvuodet\n * @type {Array}\n * @memberof HellewiCoursePartial\n */\n periods?: Array;\n /**\n * \n * @type {HellewiLanguage}\n * @memberof HellewiCoursePartial\n */\n language?: HellewiLanguage;\n /**\n * Tags\n * @type {Array}\n * @memberof HellewiCoursePartial\n */\n tags?: Array;\n /**\n * Prices\n * @type {Array}\n * @memberof HellewiCoursePartial\n */\n prices?: Array;\n /**\n * \n * @type {HellewiCourseMinimalParent}\n * @memberof HellewiCoursePartial\n */\n moduleparent?: HellewiCourseMinimalParent;\n /**\n * Child courses for a module parent course\n * @type {Array}\n * @memberof HellewiCoursePartial\n */\n modulechildren?: Array;\n /**\n * Catalog items\n * \n * Different item types are explained under [GetCatalog](#operation/GetCatalog)\n * response\n * @type {Array}\n * @memberof HellewiCoursePartial\n */\n catalogitems: Array;\n /**\n * \n * @type {HellewiAgeLimits}\n * @memberof HellewiCoursePartial\n */\n ageLimits?: HellewiAgeLimits;\n /**\n * Course products\n * @type {Array}\n * @memberof HellewiCoursePartial\n */\n courseProducts?: Array;\n}\n\nexport function HellewiCoursePartialFromJSON(json: any): HellewiCoursePartial {\n return HellewiCoursePartialFromJSONTyped(json, false);\n}\n\nexport function HellewiCoursePartialFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCoursePartial {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n 'code': !exists(json, 'code') ? undefined : json['code'],\n 'name': !exists(json, 'name') ? undefined : json['name'],\n 'tenant': json['tenant'],\n 'statuses': ((json['statuses'] as Array).map(HellewiCourseStatusFromJSON)),\n 'begins': !exists(json, 'begins') ? undefined : (new Date(json['begins'])),\n 'ends': !exists(json, 'ends') ? undefined : (new Date(json['ends'])),\n 'registrationbegins': !exists(json, 'registrationbegins') ? undefined : (new Date(json['registrationbegins'])),\n 'registrationendssoft': !exists(json, 'registrationendssoft') ? undefined : (new Date(json['registrationendssoft'])),\n 'registrationendshard': !exists(json, 'registrationendshard') ? undefined : (new Date(json['registrationendshard'])),\n 'registrationopen': !exists(json, 'registrationopen') ? undefined : json['registrationopen'],\n 'teacher': !exists(json, 'teacher') ? undefined : json['teacher'],\n 'ectscredits': !exists(json, 'ectscredits') ? undefined : json['ectscredits'],\n 'topical': !exists(json, 'topical') ? undefined : json['topical'],\n 'days': !exists(json, 'days') ? undefined : ((json['days'] as Array).map(HellewiCourseDayFromJSON)),\n 'location': !exists(json, 'location') ? undefined : HellewiLocationFromJSON(json['location']),\n 'notifications': !exists(json, 'notifications') ? undefined : ((json['notifications'] as Array).map(HellewiCourseNotificationFromJSON)),\n 'periods': !exists(json, 'periods') ? undefined : ((json['periods'] as Array).map(HellewiCoursePeriodFromJSON)),\n 'language': !exists(json, 'language') ? undefined : HellewiLanguageFromJSON(json['language']),\n 'tags': !exists(json, 'tags') ? undefined : ((json['tags'] as Array).map(HellewiTagFromJSON)),\n 'prices': !exists(json, 'prices') ? undefined : ((json['prices'] as Array).map(HellewiCoursePriceFromJSON)),\n 'moduleparent': !exists(json, 'moduleparent') ? undefined : HellewiCourseMinimalParentFromJSON(json['moduleparent']),\n 'modulechildren': !exists(json, 'modulechildren') ? undefined : ((json['modulechildren'] as Array).map(HellewiCourseMinimalFromJSON)),\n 'catalogitems': ((json['catalogitems'] as Array).map(HellewiCatalogItemFromJSON)),\n 'ageLimits': !exists(json, 'ageLimits') ? undefined : HellewiAgeLimitsFromJSON(json['ageLimits']),\n 'courseProducts': !exists(json, 'courseProducts') ? undefined : ((json['courseProducts'] as Array).map(HellewiCourseProductFromJSON)),\n };\n}\n\nexport function HellewiCoursePartialToJSON(value?: HellewiCoursePartial | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'code': value.code,\n 'name': value.name,\n 'tenant': value.tenant,\n 'statuses': ((value.statuses as Array).map(HellewiCourseStatusToJSON)),\n 'begins': value.begins === undefined ? undefined : (value.begins.toISOString().substr(0,10)),\n 'ends': value.ends === undefined ? undefined : (value.ends.toISOString().substr(0,10)),\n 'registrationbegins': value.registrationbegins === undefined ? undefined : (value.registrationbegins.toISOString()),\n 'registrationendssoft': value.registrationendssoft === undefined ? undefined : (value.registrationendssoft.toISOString()),\n 'registrationendshard': value.registrationendshard === undefined ? undefined : (value.registrationendshard.toISOString()),\n 'registrationopen': value.registrationopen,\n 'teacher': value.teacher,\n 'ectscredits': value.ectscredits,\n 'topical': value.topical,\n 'days': value.days === undefined ? undefined : ((value.days as Array).map(HellewiCourseDayToJSON)),\n 'location': HellewiLocationToJSON(value.location),\n 'notifications': value.notifications === undefined ? undefined : ((value.notifications as Array).map(HellewiCourseNotificationToJSON)),\n 'periods': value.periods === undefined ? undefined : ((value.periods as Array).map(HellewiCoursePeriodToJSON)),\n 'language': HellewiLanguageToJSON(value.language),\n 'tags': value.tags === undefined ? undefined : ((value.tags as Array).map(HellewiTagToJSON)),\n 'prices': value.prices === undefined ? undefined : ((value.prices as Array).map(HellewiCoursePriceToJSON)),\n 'moduleparent': HellewiCourseMinimalParentToJSON(value.moduleparent),\n 'modulechildren': value.modulechildren === undefined ? undefined : ((value.modulechildren as Array).map(HellewiCourseMinimalToJSON)),\n 'catalogitems': ((value.catalogitems as Array).map(HellewiCatalogItemToJSON)),\n 'ageLimits': HellewiAgeLimitsToJSON(value.ageLimits),\n 'courseProducts': value.courseProducts === undefined ? undefined : ((value.courseProducts as Array).map(HellewiCourseProductToJSON)),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiCourseLesson,\n HellewiCourseLessonFromJSON,\n HellewiCourseLessonFromJSONTyped,\n HellewiCourseLessonToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCoursePeriod\n */\nexport interface HellewiCoursePeriod {\n [key: string]: object | any;\n /**\n * Course period begins on date\n * @type {Date}\n * @memberof HellewiCoursePeriod\n */\n begins?: Date;\n /**\n * Course period ends on date\n * @type {Date}\n * @memberof HellewiCoursePeriod\n */\n ends?: Date;\n /**\n * Lesson count / kertoja\n * @type {number}\n * @memberof HellewiCoursePeriod\n */\n lessoncount?: number;\n /**\n * Lessons\n * \n * This is only present in [GetCourse](#operation/GetCourse) endpoint (not in ListCourses)\n * @type {Array}\n * @memberof HellewiCoursePeriod\n */\n lessons?: Array;\n /**\n * Name\n * @type {string}\n * @memberof HellewiCoursePeriod\n */\n name: string;\n /**\n * Keywords that can be used to match filters\n * @type {Array}\n * @memberof HellewiCoursePeriod\n */\n keywords?: Array;\n}\n\nexport function HellewiCoursePeriodFromJSON(json: any): HellewiCoursePeriod {\n return HellewiCoursePeriodFromJSONTyped(json, false);\n}\n\nexport function HellewiCoursePeriodFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCoursePeriod {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'begins': !exists(json, 'begins') ? undefined : (new Date(json['begins'])),\n 'ends': !exists(json, 'ends') ? undefined : (new Date(json['ends'])),\n 'lessoncount': !exists(json, 'lessoncount') ? undefined : json['lessoncount'],\n 'lessons': !exists(json, 'lessons') ? undefined : ((json['lessons'] as Array).map(HellewiCourseLessonFromJSON)),\n 'name': json['name'],\n 'keywords': !exists(json, 'keywords') ? undefined : json['keywords'],\n };\n}\n\nexport function HellewiCoursePeriodToJSON(value?: HellewiCoursePeriod | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'begins': value.begins === undefined ? undefined : (value.begins.toISOString().substr(0,10)),\n 'ends': value.ends === undefined ? undefined : (value.ends.toISOString().substr(0,10)),\n 'lessoncount': value.lessoncount,\n 'lessons': value.lessons === undefined ? undefined : ((value.lessons as Array).map(HellewiCourseLessonToJSON)),\n 'name': value.name,\n 'keywords': value.keywords,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiCoursePriceInstallment,\n HellewiCoursePriceInstallmentFromJSON,\n HellewiCoursePriceInstallmentFromJSONTyped,\n HellewiCoursePriceInstallmentToJSON,\n HellewiCourseProduct,\n HellewiCourseProductFromJSON,\n HellewiCourseProductFromJSONTyped,\n HellewiCourseProductToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCoursePrice\n */\nexport interface HellewiCoursePrice {\n [key: string]: object | any;\n /**\n * Price ID\n * @type {number}\n * @memberof HellewiCoursePrice\n */\n id: number;\n /**\n * Whether this price is the default one\n * \n * There is only one price with default=true per course\n * @type {boolean}\n * @memberof HellewiCoursePrice\n */\n _default?: boolean;\n /**\n * Price category name\n * @type {string}\n * @memberof HellewiCoursePrice\n */\n name?: string;\n /**\n * Price amount in euro cents\n * @type {number}\n * @memberof HellewiCoursePrice\n */\n amount?: number;\n /**\n * Payment can be done now\n * @type {boolean}\n * @memberof HellewiCoursePrice\n */\n paymentnow: boolean;\n /**\n * Payment can be done later\n * @type {boolean}\n * @memberof HellewiCoursePrice\n */\n paymentlater: boolean;\n /**\n * Payment with culture voucher\n * \n * This field is present only if culture vouchers are enabled\n * as payment options\n * @type {boolean}\n * @memberof HellewiCoursePrice\n */\n paymentwithculturevoucher?: boolean;\n /**\n * Payment with sports voucher\n * \n * This field is present only if sports vouchers are enabled\n * as payment options\n * @type {boolean}\n * @memberof HellewiCoursePrice\n */\n paymentwithsportsvoucher?: boolean;\n /**\n * Installment groups\n * \n * Normal course prices can be configured to be paid via installments,\n * lessons cannot.\n * @type {Array}\n * @memberof HellewiCoursePrice\n */\n installmentgroups?: Array;\n /**\n * Course products\n * \n * Products related to course are selectable in registration form\n * @type {Array}\n * @memberof HellewiCoursePrice\n */\n courseProducts?: Array;\n}\n\nexport function HellewiCoursePriceFromJSON(json: any): HellewiCoursePrice {\n return HellewiCoursePriceFromJSONTyped(json, false);\n}\n\nexport function HellewiCoursePriceFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCoursePrice {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n '_default': !exists(json, 'default') ? undefined : json['default'],\n 'name': !exists(json, 'name') ? undefined : json['name'],\n 'amount': !exists(json, 'amount') ? undefined : json['amount'],\n 'paymentnow': json['paymentnow'],\n 'paymentlater': json['paymentlater'],\n 'paymentwithculturevoucher': !exists(json, 'paymentwithculturevoucher') ? undefined : json['paymentwithculturevoucher'],\n 'paymentwithsportsvoucher': !exists(json, 'paymentwithsportsvoucher') ? undefined : json['paymentwithsportsvoucher'],\n 'installmentgroups': !exists(json, 'installmentgroups') ? undefined : ((json['installmentgroups'] as Array).map(HellewiCoursePriceInstallmentFromJSON)),\n 'courseProducts': !exists(json, 'courseProducts') ? undefined : ((json['courseProducts'] as Array).map(HellewiCourseProductFromJSON)),\n };\n}\n\nexport function HellewiCoursePriceToJSON(value?: HellewiCoursePrice | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'default': value._default,\n 'name': value.name,\n 'amount': value.amount,\n 'paymentnow': value.paymentnow,\n 'paymentlater': value.paymentlater,\n 'paymentwithculturevoucher': value.paymentwithculturevoucher,\n 'paymentwithsportsvoucher': value.paymentwithsportsvoucher,\n 'installmentgroups': value.installmentgroups === undefined ? undefined : ((value.installmentgroups as Array).map(HellewiCoursePriceInstallmentToJSON)),\n 'courseProducts': value.courseProducts === undefined ? undefined : ((value.courseProducts as Array).map(HellewiCourseProductToJSON)),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n HellewiCoursePriceInstallmentInstallments,\n HellewiCoursePriceInstallmentInstallmentsFromJSON,\n HellewiCoursePriceInstallmentInstallmentsFromJSONTyped,\n HellewiCoursePriceInstallmentInstallmentsToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiCoursePriceInstallment\n */\nexport interface HellewiCoursePriceInstallment {\n [key: string]: object | any;\n /**\n * Installment group name\n * @type {string}\n * @memberof HellewiCoursePriceInstallment\n */\n name: string;\n /**\n * \n * @type {Array}\n * @memberof HellewiCoursePriceInstallment\n */\n installments: Array;\n}\n\nexport function HellewiCoursePriceInstallmentFromJSON(json: any): HellewiCoursePriceInstallment {\n return HellewiCoursePriceInstallmentFromJSONTyped(json, false);\n}\n\nexport function HellewiCoursePriceInstallmentFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCoursePriceInstallment {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'name': json['name'],\n 'installments': ((json['installments'] as Array).map(HellewiCoursePriceInstallmentInstallmentsFromJSON)),\n };\n}\n\nexport function HellewiCoursePriceInstallmentToJSON(value?: HellewiCoursePriceInstallment | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'name': value.name,\n 'installments': ((value.installments as Array).map(HellewiCoursePriceInstallmentInstallmentsToJSON)),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiCoursePriceInstallmentInstallments\n */\nexport interface HellewiCoursePriceInstallmentInstallments {\n /**\n * Payment can be done later\n * @type {boolean}\n * @memberof HellewiCoursePriceInstallmentInstallments\n */\n paymentlater: boolean;\n /**\n * Payment can be done now\n * @type {boolean}\n * @memberof HellewiCoursePriceInstallmentInstallments\n */\n paymentnow: boolean;\n /**\n * Price amount in euro cents\n * @type {number}\n * @memberof HellewiCoursePriceInstallmentInstallments\n */\n amount: number;\n /**\n * Installment name\n * @type {string}\n * @memberof HellewiCoursePriceInstallmentInstallments\n */\n name?: string;\n /**\n * Installment ID\n * @type {number}\n * @memberof HellewiCoursePriceInstallmentInstallments\n */\n id: number;\n}\n\nexport function HellewiCoursePriceInstallmentInstallmentsFromJSON(json: any): HellewiCoursePriceInstallmentInstallments {\n return HellewiCoursePriceInstallmentInstallmentsFromJSONTyped(json, false);\n}\n\nexport function HellewiCoursePriceInstallmentInstallmentsFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCoursePriceInstallmentInstallments {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n 'paymentlater': json['paymentlater'],\n 'paymentnow': json['paymentnow'],\n 'amount': json['amount'],\n 'name': !exists(json, 'name') ? undefined : json['name'],\n 'id': json['id'],\n };\n}\n\nexport function HellewiCoursePriceInstallmentInstallmentsToJSON(value?: HellewiCoursePriceInstallmentInstallments | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n 'paymentlater': value.paymentlater,\n 'paymentnow': value.paymentnow,\n 'amount': value.amount,\n 'name': value.name,\n 'id': value.id,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiCourseProduct\n */\nexport interface HellewiCourseProduct {\n [key: string]: object | any;\n /**\n * \n * @type {number}\n * @memberof HellewiCourseProduct\n */\n courseProductId: number;\n /**\n * \n * @type {number}\n * @memberof HellewiCourseProduct\n */\n productId: number;\n /**\n * \n * @type {number}\n * @memberof HellewiCourseProduct\n */\n price: number;\n /**\n * \n * @type {string}\n * @memberof HellewiCourseProduct\n */\n name: string;\n /**\n * \n * @type {number}\n * @memberof HellewiCourseProduct\n */\n revenueAccountId?: number;\n /**\n * \n * @type {number}\n * @memberof HellewiCourseProduct\n */\n revenueAccountVat?: number;\n}\n\nexport function HellewiCourseProductFromJSON(json: any): HellewiCourseProduct {\n return HellewiCourseProductFromJSONTyped(json, false);\n}\n\nexport function HellewiCourseProductFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiCourseProduct {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'courseProductId': json['courseProductId'],\n 'productId': json['productId'],\n 'price': json['price'],\n 'name': json['name'],\n 'revenueAccountId': !exists(json, 'revenueAccountId') ? undefined : json['revenueAccountId'],\n 'revenueAccountVat': !exists(json, 'revenueAccountVat') ? undefined : json['revenueAccountVat'],\n };\n}\n\nexport function HellewiCourseProductToJSON(value?: HellewiCourseProduct | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'courseProductId': value.courseProductId,\n 'productId': value.productId,\n 'price': value.price,\n 'name': value.name,\n 'revenueAccountId': value.revenueAccountId,\n 'revenueAccountVat': value.revenueAccountVat,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiFile\n */\nexport interface HellewiFile {\n [key: string]: object | any;\n /**\n * Url for the file\n * @type {string}\n * @memberof HellewiFile\n */\n url: string;\n /**\n * File type\n * @type {string}\n * @memberof HellewiFile\n */\n contentType: string;\n /**\n * File text\n * @type {string}\n * @memberof HellewiFile\n */\n alt?: string;\n}\n\nexport function HellewiFileFromJSON(json: any): HellewiFile {\n return HellewiFileFromJSONTyped(json, false);\n}\n\nexport function HellewiFileFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiFile {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'url': json['url'],\n 'contentType': json['contentType'],\n 'alt': !exists(json, 'alt') ? undefined : json['alt'],\n };\n}\n\nexport function HellewiFileToJSON(value?: HellewiFile | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'url': value.url,\n 'contentType': value.contentType,\n 'alt': value.alt,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n MethodOfPayment,\n MethodOfPaymentFromJSON,\n MethodOfPaymentFromJSONTyped,\n MethodOfPaymentToJSON,\n PurchaseAmountNumber,\n PurchaseAmountNumberFromJSON,\n PurchaseAmountNumberFromJSONTyped,\n PurchaseAmountNumberToJSON,\n PurchaseInvoiceNumber,\n PurchaseInvoiceNumberFromJSON,\n PurchaseInvoiceNumberFromJSONTyped,\n PurchaseInvoiceNumberToJSON,\n PurchaseProductNumber,\n PurchaseProductNumberFromJSON,\n PurchaseProductNumberFromJSONTyped,\n PurchaseProductNumberToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiGetRegistrationResponse\n */\nexport interface HellewiGetRegistrationResponse {\n [key: string]: object | any;\n /**\n * Products\n * @type {Array}\n * @memberof HellewiGetRegistrationResponse\n */\n products: Array;\n /**\n * \n * @type {PurchaseAmountNumber}\n * @memberof HellewiGetRegistrationResponse\n */\n productstotal: PurchaseAmountNumber;\n /**\n * Total amounts for listed products grouped by VAT percentages.\n * \n * This can be used for constructing a VAT breakdown (ALV-erittely)\n * @type {Array}\n * @memberof HellewiGetRegistrationResponse\n */\n productsgroupedvatamounts: Array;\n /**\n * Invoices\n * @type {Array}\n * @memberof HellewiGetRegistrationResponse\n */\n invoices: Array;\n /**\n * \n * @type {PurchaseAmountNumber}\n * @memberof HellewiGetRegistrationResponse\n */\n invoicestotal?: PurchaseAmountNumber;\n /**\n * Total amounts for listed invoices grouped by VAT percentages.\n * \n * This can be used for constructing a VAT breakdown (ALV-erittely)\n * @type {Array}\n * @memberof HellewiGetRegistrationResponse\n */\n invoicesgroupedvatamounts: Array;\n /**\n * Cancellation text\n * \n * This is a text that is shown to the user when they cancel their registration.\n * @type {string}\n * @memberof HellewiGetRegistrationResponse\n */\n cancellationtext?: string;\n /**\n * Either **Payment form**\n * \n * ```typescript\n * PaymentForm {\n * action: string;\n * fields: PaymentFormField[];\n * method: 'post';\n * name: PaymentServiceName;\n * infotext: MethodOfPaymentInfotext;\n * }\n * ```\n * \n * This data is used for generating a HTML form for the payment of this registration.\n * \n * For example, the paytrail e2 form interface works like this with lots of hidden\n * fields and one button which the user can press to proceed to paytrail's website\n * for completing the payment.\n * \n * ```html\n *
\n * \n * \n * ...\n * \n *
\n * ```\n * \n * or **Payment Url**\n * \n * This data is used for forwarding browser to the payment page\n * \n * ```typescript\n * PaymentUrl {\n * identifier: string;\n * name: PaymentServiceName;\n * url: string;\n * infotext: MethodOfPaymentInfotext;\n * }\n * ```\n * @type {Array}\n * @memberof HellewiGetRegistrationResponse\n */\n methodsofpayment: Array;\n /**\n * Info text for payment provider selection\n * \n * Undefined if methodsofpayment is empty (i.e. user cannot pay anything).\n * @type {string}\n * @memberof HellewiGetRegistrationResponse\n */\n paymenttext?: string;\n}\n\nexport function HellewiGetRegistrationResponseFromJSON(json: any): HellewiGetRegistrationResponse {\n return HellewiGetRegistrationResponseFromJSONTyped(json, false);\n}\n\nexport function HellewiGetRegistrationResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiGetRegistrationResponse {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'products': ((json['products'] as Array).map(PurchaseProductNumberFromJSON)),\n 'productstotal': PurchaseAmountNumberFromJSON(json['productstotal']),\n 'productsgroupedvatamounts': ((json['productsgroupedvatamounts'] as Array).map(PurchaseAmountNumberFromJSON)),\n 'invoices': ((json['invoices'] as Array).map(PurchaseInvoiceNumberFromJSON)),\n 'invoicestotal': !exists(json, 'invoicestotal') ? undefined : PurchaseAmountNumberFromJSON(json['invoicestotal']),\n 'invoicesgroupedvatamounts': ((json['invoicesgroupedvatamounts'] as Array).map(PurchaseAmountNumberFromJSON)),\n 'cancellationtext': !exists(json, 'cancellationtext') ? undefined : json['cancellationtext'],\n 'methodsofpayment': ((json['methodsofpayment'] as Array).map(MethodOfPaymentFromJSON)),\n 'paymenttext': !exists(json, 'paymenttext') ? undefined : json['paymenttext'],\n };\n}\n\nexport function HellewiGetRegistrationResponseToJSON(value?: HellewiGetRegistrationResponse | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'products': ((value.products as Array).map(PurchaseProductNumberToJSON)),\n 'productstotal': PurchaseAmountNumberToJSON(value.productstotal),\n 'productsgroupedvatamounts': ((value.productsgroupedvatamounts as Array).map(PurchaseAmountNumberToJSON)),\n 'invoices': ((value.invoices as Array).map(PurchaseInvoiceNumberToJSON)),\n 'invoicestotal': PurchaseAmountNumberToJSON(value.invoicestotal),\n 'invoicesgroupedvatamounts': ((value.invoicesgroupedvatamounts as Array).map(PurchaseAmountNumberToJSON)),\n 'cancellationtext': value.cancellationtext,\n 'methodsofpayment': ((value.methodsofpayment as Array).map(MethodOfPaymentToJSON)),\n 'paymenttext': value.paymenttext,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiImage\n */\nexport interface HellewiImage {\n [key: string]: object | any;\n /**\n * Url for the image\n * @type {string}\n * @memberof HellewiImage\n */\n url: string;\n /**\n * Image type\n * @type {string}\n * @memberof HellewiImage\n */\n contentType: string;\n /**\n * Alt text for the image\n * @type {string}\n * @memberof HellewiImage\n */\n alt?: string;\n}\n\nexport function HellewiImageFromJSON(json: any): HellewiImage {\n return HellewiImageFromJSONTyped(json, false);\n}\n\nexport function HellewiImageFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiImage {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'url': json['url'],\n 'contentType': json['contentType'],\n 'alt': !exists(json, 'alt') ? undefined : json['alt'],\n };\n}\n\nexport function HellewiImageToJSON(value?: HellewiImage | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'url': value.url,\n 'contentType': value.contentType,\n 'alt': value.alt,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiLanguage\n */\nexport interface HellewiLanguage {\n [key: string]: object | any;\n /**\n * Name\n * @type {string}\n * @memberof HellewiLanguage\n */\n name: string;\n /**\n * Keywords that can be used to match filters\n * @type {Array}\n * @memberof HellewiLanguage\n */\n keywords?: Array;\n}\n\nexport function HellewiLanguageFromJSON(json: any): HellewiLanguage {\n return HellewiLanguageFromJSONTyped(json, false);\n}\n\nexport function HellewiLanguageFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiLanguage {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'name': json['name'],\n 'keywords': !exists(json, 'keywords') ? undefined : json['keywords'],\n };\n}\n\nexport function HellewiLanguageToJSON(value?: HellewiLanguage | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'name': value.name,\n 'keywords': value.keywords,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiLessonParticipantCount\n */\nexport interface HellewiLessonParticipantCount {\n [key: string]: object | any;\n /**\n * Course ID\n * \n * This is either\n * - a number as string (single-tenant endpoints, e.g. `'3'`)\n * - string with tenant id (in lowercase) and course id number (multi-tenant\n * endpoints, e.g. `'demo.opistopalvelut.fi-3'`)\n * \n * Validation pattern: `^(?:[a-z0-9-.]+-)?[0-9]+$`\n * @type {string}\n * @memberof HellewiLessonParticipantCount\n */\n id: string;\n /**\n * Course is almost full: less than 10% of places available\n * @type {boolean}\n * @memberof HellewiLessonParticipantCount\n */\n almostfull: boolean;\n /**\n * Course is full\n * \n * You might still be able to register for queueing\n * @type {boolean}\n * @memberof HellewiLessonParticipantCount\n */\n full: boolean;\n /**\n * Maximum number of participants\n * @type {number}\n * @memberof HellewiLessonParticipantCount\n */\n max?: number;\n /**\n * Available places for registration\n * @type {number}\n * @memberof HellewiLessonParticipantCount\n */\n available?: number;\n /**\n * How many times course can be added to cart\n * \n * undefined if there is no limit\n * @type {number}\n * @memberof HellewiLessonParticipantCount\n */\n cartlimit?: number;\n /**\n * Minimum number of participants\n * @type {number}\n * @memberof HellewiLessonParticipantCount\n */\n min?: number;\n /**\n * Actual registrations\n * @type {number}\n * @memberof HellewiLessonParticipantCount\n */\n registrations?: number;\n /**\n * Registration is open\n * @type {boolean}\n * @memberof HellewiLessonParticipantCount\n */\n registrationopen: boolean;\n /**\n * Lesson id\n * @type {number}\n * @memberof HellewiLessonParticipantCount\n */\n lessonId: number;\n}\n\nexport function HellewiLessonParticipantCountFromJSON(json: any): HellewiLessonParticipantCount {\n return HellewiLessonParticipantCountFromJSONTyped(json, false);\n}\n\nexport function HellewiLessonParticipantCountFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiLessonParticipantCount {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n 'almostfull': json['almostfull'],\n 'full': json['full'],\n 'max': !exists(json, 'max') ? undefined : json['max'],\n 'available': !exists(json, 'available') ? undefined : json['available'],\n 'cartlimit': !exists(json, 'cartlimit') ? undefined : json['cartlimit'],\n 'min': !exists(json, 'min') ? undefined : json['min'],\n 'registrations': !exists(json, 'registrations') ? undefined : json['registrations'],\n 'registrationopen': json['registrationopen'],\n 'lessonId': json['lessonId'],\n };\n}\n\nexport function HellewiLessonParticipantCountToJSON(value?: HellewiLessonParticipantCount | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'almostfull': value.almostfull,\n 'full': value.full,\n 'max': value.max,\n 'available': value.available,\n 'cartlimit': value.cartlimit,\n 'min': value.min,\n 'registrations': value.registrations,\n 'registrationopen': value.registrationopen,\n 'lessonId': value.lessonId,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n Geopoint,\n GeopointFromJSON,\n GeopointFromJSONTyped,\n GeopointToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiLocation\n */\nexport interface HellewiLocation {\n [key: string]: object | any;\n /**\n * Address\n * @type {string}\n * @memberof HellewiLocation\n */\n address?: string;\n /**\n * City\n * @type {string}\n * @memberof HellewiLocation\n */\n city?: string;\n /**\n * ID in Hellewi\n * \n * This is in always included [/locations](#tag/Location) response\n * but never in [/courses](#tag/Course)\n * @type {number}\n * @memberof HellewiLocation\n */\n id?: number;\n /**\n * \n * @type {Geopoint}\n * @memberof HellewiLocation\n */\n latlon?: Geopoint;\n /**\n * Name\n * @type {string}\n * @memberof HellewiLocation\n */\n name: string;\n /**\n * Postal code\n * @type {string}\n * @memberof HellewiLocation\n */\n postalcode?: string;\n /**\n * Keywords that can be used to match course filters\n * @type {Array}\n * @memberof HellewiLocation\n */\n keywords?: Array;\n /**\n * Accessibility from the perspective of general mobility\n * @type {string}\n * @memberof HellewiLocation\n */\n accessibility?: string;\n /**\n * External ID\n * \n * Optional ID that can be used to match the location to\n * an external system instead of auto-increment ID\n * @type {string}\n * @memberof HellewiLocation\n */\n externalid?: string;\n}\n\nexport function HellewiLocationFromJSON(json: any): HellewiLocation {\n return HellewiLocationFromJSONTyped(json, false);\n}\n\nexport function HellewiLocationFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiLocation {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'address': !exists(json, 'address') ? undefined : json['address'],\n 'city': !exists(json, 'city') ? undefined : json['city'],\n 'id': !exists(json, 'id') ? undefined : json['id'],\n 'latlon': !exists(json, 'latlon') ? undefined : GeopointFromJSON(json['latlon']),\n 'name': json['name'],\n 'postalcode': !exists(json, 'postalcode') ? undefined : json['postalcode'],\n 'keywords': !exists(json, 'keywords') ? undefined : json['keywords'],\n 'accessibility': !exists(json, 'accessibility') ? undefined : json['accessibility'],\n 'externalid': !exists(json, 'externalid') ? undefined : json['externalid'],\n };\n}\n\nexport function HellewiLocationToJSON(value?: HellewiLocation | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'address': value.address,\n 'city': value.city,\n 'id': value.id,\n 'latlon': GeopointToJSON(value.latlon),\n 'name': value.name,\n 'postalcode': value.postalcode,\n 'keywords': value.keywords,\n 'accessibility': value.accessibility,\n 'externalid': value.externalid,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n PurchaseAmountNumber,\n PurchaseAmountNumberFromJSON,\n PurchaseAmountNumberFromJSONTyped,\n PurchaseAmountNumberToJSON,\n PurchaseInvoiceNumber,\n PurchaseInvoiceNumberFromJSON,\n PurchaseInvoiceNumberFromJSONTyped,\n PurchaseInvoiceNumberToJSON,\n PurchaseProductNumber,\n PurchaseProductNumberFromJSON,\n PurchaseProductNumberFromJSONTyped,\n PurchaseProductNumberToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiMyRegistrationsResponse\n */\nexport interface HellewiMyRegistrationsResponse {\n [key: string]: object | any;\n /**\n * Products\n * @type {Array}\n * @memberof HellewiMyRegistrationsResponse\n */\n products: Array;\n /**\n * \n * @type {PurchaseAmountNumber}\n * @memberof HellewiMyRegistrationsResponse\n */\n productstotal: PurchaseAmountNumber;\n /**\n * Total amounts for listed products grouped by VAT percentages.\n * \n * This can be used for constructing a VAT breakdown (ALV-erittely)\n * @type {Array}\n * @memberof HellewiMyRegistrationsResponse\n */\n productsgroupedvatamounts: Array;\n /**\n * Invoices\n * @type {Array}\n * @memberof HellewiMyRegistrationsResponse\n */\n invoices: Array;\n /**\n * \n * @type {PurchaseAmountNumber}\n * @memberof HellewiMyRegistrationsResponse\n */\n invoicestotal?: PurchaseAmountNumber;\n /**\n * Total amounts for listed invoices grouped by VAT percentages.\n * \n * This can be used for constructing a VAT breakdown (ALV-erittely)\n * @type {Array}\n * @memberof HellewiMyRegistrationsResponse\n */\n invoicesgroupedvatamounts: Array;\n /**\n * Cancellation text\n * \n * This is a text that is shown to the user when they cancel their registration.\n * @type {string}\n * @memberof HellewiMyRegistrationsResponse\n */\n cancellationtext?: string;\n}\n\nexport function HellewiMyRegistrationsResponseFromJSON(json: any): HellewiMyRegistrationsResponse {\n return HellewiMyRegistrationsResponseFromJSONTyped(json, false);\n}\n\nexport function HellewiMyRegistrationsResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiMyRegistrationsResponse {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'products': ((json['products'] as Array).map(PurchaseProductNumberFromJSON)),\n 'productstotal': PurchaseAmountNumberFromJSON(json['productstotal']),\n 'productsgroupedvatamounts': ((json['productsgroupedvatamounts'] as Array).map(PurchaseAmountNumberFromJSON)),\n 'invoices': ((json['invoices'] as Array).map(PurchaseInvoiceNumberFromJSON)),\n 'invoicestotal': !exists(json, 'invoicestotal') ? undefined : PurchaseAmountNumberFromJSON(json['invoicestotal']),\n 'invoicesgroupedvatamounts': ((json['invoicesgroupedvatamounts'] as Array).map(PurchaseAmountNumberFromJSON)),\n 'cancellationtext': !exists(json, 'cancellationtext') ? undefined : json['cancellationtext'],\n };\n}\n\nexport function HellewiMyRegistrationsResponseToJSON(value?: HellewiMyRegistrationsResponse | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'products': ((value.products as Array).map(PurchaseProductNumberToJSON)),\n 'productstotal': PurchaseAmountNumberToJSON(value.productstotal),\n 'productsgroupedvatamounts': ((value.productsgroupedvatamounts as Array).map(PurchaseAmountNumberToJSON)),\n 'invoices': ((value.invoices as Array).map(PurchaseInvoiceNumberToJSON)),\n 'invoicestotal': PurchaseAmountNumberToJSON(value.invoicestotal),\n 'invoicesgroupedvatamounts': ((value.invoicesgroupedvatamounts as Array).map(PurchaseAmountNumberToJSON)),\n 'cancellationtext': value.cancellationtext,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * Participant count in a single course\n * \n * Optional fields will always be there if global parameter\n * registrationsettings.showseatcount is set to true, or if\n * course parameter showplacecount is set to true\n * @export\n * @interface HellewiParticipantCount\n */\nexport interface HellewiParticipantCount {\n [key: string]: object | any;\n /**\n * Course ID\n * \n * This is either\n * - a number as string (single-tenant endpoints, e.g. `'3'`)\n * - string with tenant id (in lowercase) and course id number (multi-tenant\n * endpoints, e.g. `'demo.opistopalvelut.fi-3'`)\n * \n * Validation pattern: `^(?:[a-z0-9-.]+-)?[0-9]+$`\n * @type {string}\n * @memberof HellewiParticipantCount\n */\n id: string;\n /**\n * Course is almost full: less than 10% of places available\n * @type {boolean}\n * @memberof HellewiParticipantCount\n */\n almostfull: boolean;\n /**\n * Course is full\n * \n * You might still be able to register for queueing\n * @type {boolean}\n * @memberof HellewiParticipantCount\n */\n full: boolean;\n /**\n * Maximum number of participants\n * @type {number}\n * @memberof HellewiParticipantCount\n */\n max?: number;\n /**\n * Available places for registration\n * @type {number}\n * @memberof HellewiParticipantCount\n */\n available?: number;\n /**\n * How many times course can be added to cart\n * \n * undefined if there is no limit\n * @type {number}\n * @memberof HellewiParticipantCount\n */\n cartlimit?: number;\n /**\n * Minimum number of participants\n * @type {number}\n * @memberof HellewiParticipantCount\n */\n min?: number;\n /**\n * Actual registrations\n * @type {number}\n * @memberof HellewiParticipantCount\n */\n registrations?: number;\n /**\n * Registration is open\n * @type {boolean}\n * @memberof HellewiParticipantCount\n */\n registrationopen: boolean;\n /**\n * Spares are full\n * @type {boolean}\n * @memberof HellewiParticipantCount\n */\n sparefull: boolean;\n /**\n * Participants queuing for available places\n * @type {number}\n * @memberof HellewiParticipantCount\n */\n spare?: number;\n /**\n * Number of places available in queue\n * @type {number}\n * @memberof HellewiParticipantCount\n */\n spareavailable?: number;\n /**\n * Maximum number of participants in queue\n * @type {number}\n * @memberof HellewiParticipantCount\n */\n sparemax?: number;\n}\n\nexport function HellewiParticipantCountFromJSON(json: any): HellewiParticipantCount {\n return HellewiParticipantCountFromJSONTyped(json, false);\n}\n\nexport function HellewiParticipantCountFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiParticipantCount {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'id': json['id'],\n 'almostfull': json['almostfull'],\n 'full': json['full'],\n 'max': !exists(json, 'max') ? undefined : json['max'],\n 'available': !exists(json, 'available') ? undefined : json['available'],\n 'cartlimit': !exists(json, 'cartlimit') ? undefined : json['cartlimit'],\n 'min': !exists(json, 'min') ? undefined : json['min'],\n 'registrations': !exists(json, 'registrations') ? undefined : json['registrations'],\n 'registrationopen': json['registrationopen'],\n 'sparefull': json['sparefull'],\n 'spare': !exists(json, 'spare') ? undefined : json['spare'],\n 'spareavailable': !exists(json, 'spareavailable') ? undefined : json['spareavailable'],\n 'sparemax': !exists(json, 'sparemax') ? undefined : json['sparemax'],\n };\n}\n\nexport function HellewiParticipantCountToJSON(value?: HellewiParticipantCount | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'id': value.id,\n 'almostfull': value.almostfull,\n 'full': value.full,\n 'max': value.max,\n 'available': value.available,\n 'cartlimit': value.cartlimit,\n 'min': value.min,\n 'registrations': value.registrations,\n 'registrationopen': value.registrationopen,\n 'sparefull': value.sparefull,\n 'spare': value.spare,\n 'spareavailable': value.spareavailable,\n 'sparemax': value.sparemax,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n MethodOfPayment,\n MethodOfPaymentFromJSON,\n MethodOfPaymentFromJSONTyped,\n MethodOfPaymentToJSON,\n PurchaseAmountNumber,\n PurchaseAmountNumberFromJSON,\n PurchaseAmountNumberFromJSONTyped,\n PurchaseAmountNumberToJSON,\n PurchaseInvoiceNumber,\n PurchaseInvoiceNumberFromJSON,\n PurchaseInvoiceNumberFromJSONTyped,\n PurchaseInvoiceNumberToJSON,\n PurchaseProductNumber,\n PurchaseProductNumberFromJSON,\n PurchaseProductNumberFromJSONTyped,\n PurchaseProductNumberToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface HellewiPostRegistrationResponse\n */\nexport interface HellewiPostRegistrationResponse {\n [key: string]: object | any;\n /**\n * Products\n * @type {Array}\n * @memberof HellewiPostRegistrationResponse\n */\n products: Array;\n /**\n * \n * @type {PurchaseAmountNumber}\n * @memberof HellewiPostRegistrationResponse\n */\n productstotal: PurchaseAmountNumber;\n /**\n * Total amounts for listed products grouped by VAT percentages.\n * \n * This can be used for constructing a VAT breakdown (ALV-erittely)\n * @type {Array}\n * @memberof HellewiPostRegistrationResponse\n */\n productsgroupedvatamounts: Array;\n /**\n * Invoices\n * @type {Array}\n * @memberof HellewiPostRegistrationResponse\n */\n invoices: Array;\n /**\n * \n * @type {PurchaseAmountNumber}\n * @memberof HellewiPostRegistrationResponse\n */\n invoicestotal?: PurchaseAmountNumber;\n /**\n * Total amounts for listed invoices grouped by VAT percentages.\n * \n * This can be used for constructing a VAT breakdown (ALV-erittely)\n * @type {Array}\n * @memberof HellewiPostRegistrationResponse\n */\n invoicesgroupedvatamounts: Array;\n /**\n * Cancellation text\n * \n * This is a text that is shown to the user when they cancel their registration.\n * @type {string}\n * @memberof HellewiPostRegistrationResponse\n */\n cancellationtext?: string;\n /**\n * Either **Payment form**\n * \n * ```typescript\n * PaymentForm {\n * action: string;\n * fields: PaymentFormField[];\n * method: 'post';\n * name: PaymentServiceName;\n * infotext: MethodOfPaymentInfotext;\n * }\n * ```\n * \n * This data is used for generating a HTML form for the payment of this registration.\n * \n * For example, the paytrail e2 form interface works like this with lots of hidden\n * fields and one button which the user can press to proceed to paytrail's website\n * for completing the payment.\n * \n * ```html\n *
\n * \n * \n * ...\n * \n *
\n * ```\n * \n * or **Payment Url**\n * \n * This data is used for forwarding browser to the payment page\n * \n * ```typescript\n * PaymentUrl {\n * identifier: string;\n * name: PaymentServiceName;\n * url: string;\n * infotext: MethodOfPaymentInfotext;\n * }\n * ```\n * @type {Array}\n * @memberof HellewiPostRegistrationResponse\n */\n methodsofpayment: Array;\n /**\n * Info text for payment provider selection\n * \n * Undefined if methodsofpayment is empty (i.e. user cannot pay anything).\n * @type {string}\n * @memberof HellewiPostRegistrationResponse\n */\n paymenttext?: string;\n /**\n * Info text to be shown after a successful registration\n * @type {string}\n * @memberof HellewiPostRegistrationResponse\n */\n confirmationtext: string;\n /**\n * Info text to be shown if any registration is in the queue\n * @type {string}\n * @memberof HellewiPostRegistrationResponse\n */\n queuetext?: string;\n /**\n * Boolean indicating if the registration must be paid on registration\n * @type {boolean}\n * @memberof HellewiPostRegistrationResponse\n */\n paymentrequired?: boolean;\n}\n\nexport function HellewiPostRegistrationResponseFromJSON(json: any): HellewiPostRegistrationResponse {\n return HellewiPostRegistrationResponseFromJSONTyped(json, false);\n}\n\nexport function HellewiPostRegistrationResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiPostRegistrationResponse {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'products': ((json['products'] as Array).map(PurchaseProductNumberFromJSON)),\n 'productstotal': PurchaseAmountNumberFromJSON(json['productstotal']),\n 'productsgroupedvatamounts': ((json['productsgroupedvatamounts'] as Array).map(PurchaseAmountNumberFromJSON)),\n 'invoices': ((json['invoices'] as Array).map(PurchaseInvoiceNumberFromJSON)),\n 'invoicestotal': !exists(json, 'invoicestotal') ? undefined : PurchaseAmountNumberFromJSON(json['invoicestotal']),\n 'invoicesgroupedvatamounts': ((json['invoicesgroupedvatamounts'] as Array).map(PurchaseAmountNumberFromJSON)),\n 'cancellationtext': !exists(json, 'cancellationtext') ? undefined : json['cancellationtext'],\n 'methodsofpayment': ((json['methodsofpayment'] as Array).map(MethodOfPaymentFromJSON)),\n 'paymenttext': !exists(json, 'paymenttext') ? undefined : json['paymenttext'],\n 'confirmationtext': json['confirmationtext'],\n 'queuetext': !exists(json, 'queuetext') ? undefined : json['queuetext'],\n 'paymentrequired': !exists(json, 'paymentrequired') ? undefined : json['paymentrequired'],\n };\n}\n\nexport function HellewiPostRegistrationResponseToJSON(value?: HellewiPostRegistrationResponse | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'products': ((value.products as Array).map(PurchaseProductNumberToJSON)),\n 'productstotal': PurchaseAmountNumberToJSON(value.productstotal),\n 'productsgroupedvatamounts': ((value.productsgroupedvatamounts as Array).map(PurchaseAmountNumberToJSON)),\n 'invoices': ((value.invoices as Array).map(PurchaseInvoiceNumberToJSON)),\n 'invoicestotal': PurchaseAmountNumberToJSON(value.invoicestotal),\n 'invoicesgroupedvatamounts': ((value.invoicesgroupedvatamounts as Array).map(PurchaseAmountNumberToJSON)),\n 'cancellationtext': value.cancellationtext,\n 'methodsofpayment': ((value.methodsofpayment as Array).map(MethodOfPaymentToJSON)),\n 'paymenttext': value.paymenttext,\n 'confirmationtext': value.confirmationtext,\n 'queuetext': value.queuetext,\n 'paymentrequired': value.paymentrequired,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiPromotion\n */\nexport interface HellewiPromotion {\n [key: string]: object | any;\n /**\n * \n * @type {string}\n * @memberof HellewiPromotion\n */\n text?: string;\n /**\n * \n * @type {string}\n * @memberof HellewiPromotion\n */\n color?: string;\n /**\n * \n * @type {string}\n * @memberof HellewiPromotion\n */\n url?: string;\n /**\n * \n * @type {string}\n * @memberof HellewiPromotion\n */\n image?: string;\n}\n\nexport function HellewiPromotionFromJSON(json: any): HellewiPromotion {\n return HellewiPromotionFromJSONTyped(json, false);\n}\n\nexport function HellewiPromotionFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiPromotion {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'text': !exists(json, 'text') ? undefined : json['text'],\n 'color': !exists(json, 'color') ? undefined : json['color'],\n 'url': !exists(json, 'url') ? undefined : json['url'],\n 'image': !exists(json, 'image') ? undefined : json['image'],\n };\n}\n\nexport function HellewiPromotionToJSON(value?: HellewiPromotion | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'text': value.text,\n 'color': value.color,\n 'url': value.url,\n 'image': value.image,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum HellewiTenantType {\n CommunityCollege = 'COMMUNITY_COLLEGE',\n SportsAndCulture = 'SPORTS_AND_CULTURE',\n ArtsEducation = 'ARTS_EDUCATION',\n SummerSchool = 'SUMMER_SCHOOL',\n FolkHighSchool = 'FOLK_HIGH_SCHOOL',\n VocationalSchool = 'VOCATIONAL_SCHOOL',\n Company = 'COMPANY',\n Association = 'ASSOCIATION',\n Museum = 'MUSEUM',\n EmploymentServices = 'EMPLOYMENT_SERVICES'\n}\n\nexport function HellewiTenantTypeFromJSON(json: any): HellewiTenantType {\n return HellewiTenantTypeFromJSONTyped(json, false);\n}\n\nexport function HellewiTenantTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiTenantType {\n return json as HellewiTenantType;\n}\n\nexport function HellewiTenantTypeToJSON(value?: HellewiTenantType | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n CpuPayment,\n CpuPaymentFromJSON,\n CpuPaymentFromJSONTyped,\n CpuPaymentToJSON,\n MethodOfPaymentInfotext,\n MethodOfPaymentInfotextFromJSON,\n MethodOfPaymentInfotextFromJSONTyped,\n MethodOfPaymentInfotextToJSON,\n PaymentFormField,\n PaymentFormFieldFromJSON,\n PaymentFormFieldFromJSONTyped,\n PaymentFormFieldToJSON,\n PaymentServiceName,\n PaymentServiceNameFromJSON,\n PaymentServiceNameFromJSONTyped,\n PaymentServiceNameToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface MethodOfPayment\n */\nexport interface MethodOfPayment {\n [key: string]: object | any;\n /**\n * \n * @type {PaymentServiceName}\n * @memberof MethodOfPayment\n */\n name: PaymentServiceName;\n /**\n * Payment form action\n * \n * URL to endpoint where the form should be posted\n * @type {string}\n * @memberof MethodOfPayment\n */\n action?: string;\n /**\n * Form fields\n * \n * \" id=\"name\" required>\n * @type {Array}\n * @memberof MethodOfPayment\n */\n fields?: Array;\n /**\n * payment identifier\n * @type {string}\n * @memberof MethodOfPayment\n */\n identifier?: string;\n /**\n * \n * @type {MethodOfPaymentInfotext}\n * @memberof MethodOfPayment\n */\n infotext: MethodOfPaymentInfotext;\n /**\n * Payment form method\n * @type {string}\n * @memberof MethodOfPayment\n */\n method?: MethodOfPaymentMethodEnum;\n /**\n * Payment url\n * \n * Browser should be forwarded to this url for payment\n * @type {string}\n * @memberof MethodOfPayment\n */\n url?: string;\n /**\n * \n * @type {CpuPayment}\n * @memberof MethodOfPayment\n */\n requestdata?: CpuPayment;\n}\n\n/**\n* @export\n* @enum {string}\n*/\nexport enum MethodOfPaymentMethodEnum {\n Post = 'post'\n}\n\nexport function MethodOfPaymentFromJSON(json: any): MethodOfPayment {\n return MethodOfPaymentFromJSONTyped(json, false);\n}\n\nexport function MethodOfPaymentFromJSONTyped(json: any, ignoreDiscriminator: boolean): MethodOfPayment {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'name': PaymentServiceNameFromJSON(json['name']),\n 'action': !exists(json, 'action') ? undefined : json['action'],\n 'fields': !exists(json, 'fields') ? undefined : ((json['fields'] as Array).map(PaymentFormFieldFromJSON)),\n 'identifier': !exists(json, 'identifier') ? undefined : json['identifier'],\n 'infotext': MethodOfPaymentInfotextFromJSON(json['infotext']),\n 'method': !exists(json, 'method') ? undefined : json['method'],\n 'url': !exists(json, 'url') ? undefined : json['url'],\n 'requestdata': !exists(json, 'requestdata') ? undefined : CpuPaymentFromJSON(json['requestdata']),\n };\n}\n\nexport function MethodOfPaymentToJSON(value?: MethodOfPayment | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'name': PaymentServiceNameToJSON(value.name),\n 'action': value.action,\n 'fields': value.fields === undefined ? undefined : ((value.fields as Array).map(PaymentFormFieldToJSON)),\n 'identifier': value.identifier,\n 'infotext': MethodOfPaymentInfotextToJSON(value.infotext),\n 'method': value.method,\n 'url': value.url,\n 'requestdata': CpuPaymentToJSON(value.requestdata),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum MethodOfPaymentInfotext {\n Cpu = 'cpu',\n Kulttuuripassi = 'kulttuuripassi',\n Paytrail = 'paytrail',\n PaytrailApi = 'paytrailApi',\n SmartumCulture = 'smartumCulture',\n SmartumExercise = 'smartumExercise',\n Sporttipassi = 'sporttipassi',\n Turku = 'turku',\n Vismapay = 'vismapay'\n}\n\nexport function MethodOfPaymentInfotextFromJSON(json: any): MethodOfPaymentInfotext {\n return MethodOfPaymentInfotextFromJSONTyped(json, false);\n}\n\nexport function MethodOfPaymentInfotextFromJSONTyped(json: any, ignoreDiscriminator: boolean): MethodOfPaymentInfotext {\n return json as MethodOfPaymentInfotext;\n}\n\nexport function MethodOfPaymentInfotextToJSON(value?: MethodOfPaymentInfotext | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum PaymentServiceName {\n Cpu = 'CPU',\n KulttuuriPassi = 'KulttuuriPassi',\n Paytrail = 'Paytrail',\n PaytrailApi = 'Paytrail API',\n Smartum = 'Smartum',\n SporttiPassi = 'SporttiPassi',\n Turku = 'Turku',\n Bambora = 'Bambora'\n}\n\nexport function PaymentServiceNameFromJSON(json: any): PaymentServiceName {\n return PaymentServiceNameFromJSONTyped(json, false);\n}\n\nexport function PaymentServiceNameFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaymentServiceName {\n return json as PaymentServiceName;\n}\n\nexport function PaymentServiceNameToJSON(value?: PaymentServiceName | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum PaytrailApiAlgorithm {\n Sha256 = 'sha256',\n Sha512 = 'sha512'\n}\n\nexport function PaytrailApiAlgorithmFromJSON(json: any): PaytrailApiAlgorithm {\n return PaytrailApiAlgorithmFromJSONTyped(json, false);\n}\n\nexport function PaytrailApiAlgorithmFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaytrailApiAlgorithm {\n return json as PaytrailApiAlgorithm;\n}\n\nexport function PaytrailApiAlgorithmToJSON(value?: PaytrailApiAlgorithm | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum PaytrailApiReceiptStatus {\n Delayed = 'delayed',\n Fail = 'fail',\n New = 'new',\n Ok = 'ok',\n Pending = 'pending'\n}\n\nexport function PaytrailApiReceiptStatusFromJSON(json: any): PaytrailApiReceiptStatus {\n return PaytrailApiReceiptStatusFromJSONTyped(json, false);\n}\n\nexport function PaytrailApiReceiptStatusFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaytrailApiReceiptStatus {\n return json as PaytrailApiReceiptStatus;\n}\n\nexport function PaytrailApiReceiptStatusToJSON(value?: PaytrailApiReceiptStatus | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum PurchaseProductItemType {\n Cancel = 'cancel',\n Cancellationcancelled = 'cancellationcancelled',\n Discount = 'discount',\n Epayment = 'epayment',\n Installment = 'installment',\n Invoiced = 'invoiced',\n Moved = 'moved',\n Payment = 'payment',\n Product = 'product',\n Rebate = 'rebate',\n Rebated = 'rebated',\n Refund = 'refund',\n Registrationcancelled = 'registrationcancelled',\n Registrationremoved = 'registrationremoved'\n}\n\nexport function PurchaseProductItemTypeFromJSON(json: any): PurchaseProductItemType {\n return PurchaseProductItemTypeFromJSONTyped(json, false);\n}\n\nexport function PurchaseProductItemTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean): PurchaseProductItemType {\n return json as PurchaseProductItemType;\n}\n\nexport function PurchaseProductItemTypeToJSON(value?: PurchaseProductItemType | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum PurchaseProductStatus {\n Cancelled = 'CANCELLED',\n Interrupted = 'INTERRUPTED',\n Spare = 'SPARE',\n RegistrationToLessons = 'REGISTRATION_TO_LESSONS'\n}\n\nexport function PurchaseProductStatusFromJSON(json: any): PurchaseProductStatus {\n return PurchaseProductStatusFromJSONTyped(json, false);\n}\n\nexport function PurchaseProductStatusFromJSONTyped(json: any, ignoreDiscriminator: boolean): PurchaseProductStatus {\n return json as PurchaseProductStatus;\n}\n\nexport function PurchaseProductStatusToJSON(value?: PurchaseProductStatus | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum PurchaseProductType {\n Course = 'course',\n Discount = 'discount'\n}\n\nexport function PurchaseProductTypeFromJSON(json: any): PurchaseProductType {\n return PurchaseProductTypeFromJSONTyped(json, false);\n}\n\nexport function PurchaseProductTypeFromJSONTyped(json: any, ignoreDiscriminator: boolean): PurchaseProductType {\n return json as PurchaseProductType;\n}\n\nexport function PurchaseProductTypeToJSON(value?: PurchaseProductType | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * Sorting direction to be used with sequelize\n * @export\n * @enum {string}\n */\nexport enum Sortdir {\n Asc = 'asc',\n Desc = 'desc'\n}\n\nexport function SortdirFromJSON(json: any): Sortdir {\n return SortdirFromJSONTyped(json, false);\n}\n\nexport function SortdirFromJSONTyped(json: any, ignoreDiscriminator: boolean): Sortdir {\n return json as Sortdir;\n}\n\nexport function SortdirToJSON(value?: Sortdir | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * \n * @export\n * @enum {string}\n */\nexport enum UpdatableCourseProperties {\n Summary = 'summary',\n Lessontopic = 'lessontopic'\n}\n\nexport function UpdatableCoursePropertiesFromJSON(json: any): UpdatableCourseProperties {\n return UpdatableCoursePropertiesFromJSONTyped(json, false);\n}\n\nexport function UpdatableCoursePropertiesFromJSONTyped(json: any, ignoreDiscriminator: boolean): UpdatableCourseProperties {\n return json as UpdatableCourseProperties;\n}\n\nexport function UpdatableCoursePropertiesToJSON(value?: UpdatableCourseProperties | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n/**\n * Weekday as a number, 1 = Monday, 7 = Sunday\n * @export\n * @enum {string}\n */\nexport enum Weekday {\n NUMBER_1 = 1,\n NUMBER_2 = 2,\n NUMBER_3 = 3,\n NUMBER_4 = 4,\n NUMBER_5 = 5,\n NUMBER_6 = 6,\n NUMBER_7 = 7\n}\n\nexport function WeekdayFromJSON(json: any): Weekday {\n return WeekdayFromJSONTyped(json, false);\n}\n\nexport function WeekdayFromJSONTyped(json: any, ignoreDiscriminator: boolean): Weekday {\n return json as Weekday;\n}\n\nexport function WeekdayToJSON(value?: Weekday | null): any {\n return value as any;\n}\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiTag\n */\nexport interface HellewiTag {\n [key: string]: object | any;\n /**\n * Name\n * @type {string}\n * @memberof HellewiTag\n */\n name: string;\n /**\n * Description\n * @type {string}\n * @memberof HellewiTag\n */\n description?: string;\n /**\n * Background color\n * @type {string}\n * @memberof HellewiTag\n */\n color?: string;\n /**\n * Font color\n * @type {string}\n * @memberof HellewiTag\n */\n fontcolor?: string;\n /**\n * Keywords that can be used to match filters\n * @type {Array}\n * @memberof HellewiTag\n */\n keywords?: Array;\n}\n\nexport function HellewiTagFromJSON(json: any): HellewiTag {\n return HellewiTagFromJSONTyped(json, false);\n}\n\nexport function HellewiTagFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiTag {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'name': json['name'],\n 'description': !exists(json, 'description') ? undefined : json['description'],\n 'color': !exists(json, 'color') ? undefined : json['color'],\n 'fontcolor': !exists(json, 'fontcolor') ? undefined : json['fontcolor'],\n 'keywords': !exists(json, 'keywords') ? undefined : json['keywords'],\n };\n}\n\nexport function HellewiTagToJSON(value?: HellewiTag | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'name': value.name,\n 'description': value.description,\n 'color': value.color,\n 'fontcolor': value.fontcolor,\n 'keywords': value.keywords,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface HellewiText\n */\nexport interface HellewiText {\n [key: string]: object | any;\n /**\n * \n * @type {string}\n * @memberof HellewiText\n */\n text?: string;\n}\n\nexport function HellewiTextFromJSON(json: any): HellewiText {\n return HellewiTextFromJSONTyped(json, false);\n}\n\nexport function HellewiTextFromJSONTyped(json: any, ignoreDiscriminator: boolean): HellewiText {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'text': !exists(json, 'text') ? undefined : json['text'],\n };\n}\n\nexport function HellewiTextToJSON(value?: HellewiText | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'text': value.text,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface PaymentFormField\n */\nexport interface PaymentFormField {\n [key: string]: object | any;\n /**\n * Field name\n * @type {string}\n * @memberof PaymentFormField\n */\n name: string;\n /**\n * Field value\n * @type {string}\n * @memberof PaymentFormField\n */\n value: string;\n}\n\nexport function PaymentFormFieldFromJSON(json: any): PaymentFormField {\n return PaymentFormFieldFromJSONTyped(json, false);\n}\n\nexport function PaymentFormFieldFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaymentFormField {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'name': json['name'],\n 'value': json['value'],\n };\n}\n\nexport function PaymentFormFieldToJSON(value?: PaymentFormField | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'name': value.name,\n 'value': value.value,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\n/**\n * \n * @export\n * @interface PurchaseAmountNumber\n */\nexport interface PurchaseAmountNumber {\n [key: string]: object | any;\n /**\n * Gross amount (verollinen hinta) in euro cents\n * @type {number}\n * @memberof PurchaseAmountNumber\n */\n amount: number;\n /**\n * Value-added tax (arvonlisävero) percentage\n * \n * This is undefined if the amount is total combined from different\n * vat percentages\n * @type {number}\n * @memberof PurchaseAmountNumber\n */\n vatpercentage?: number;\n /**\n * Value-added tax (arvonlisävero) amount in euro cents\n * @type {number}\n * @memberof PurchaseAmountNumber\n */\n vat: number;\n /**\n * Net amount (veroton hinta) in euro cents\n * @type {number}\n * @memberof PurchaseAmountNumber\n */\n net: number;\n /**\n * Gross amount that has to be paid now in euro cents\n * \n * mustpaynow and canpaynow are are mutually exclusive so that if one has a number,\n * the other will be zero (if something has to be paid now, everything must be paid now)\n * \n * After payment has been done, this is not included anymore.\n * @type {number}\n * @memberof PurchaseAmountNumber\n */\n mustpaynow?: number;\n /**\n * Gross amount that can to be paid now in euro cents\n * \n * mustpaynow and canpaynow are are mutually exclusive so that if one has a number,\n * the other will be zero (if something has to be paid now, everything must be paid now)\n * \n * After payment has been done, this is not included anymore.\n * @type {number}\n * @memberof PurchaseAmountNumber\n */\n canpaynow?: number;\n /**\n * Gross amount that can be paid with culture voucher in euro cents\n * \n * After payment has been done, this is not included anymore.\n * @type {number}\n * @memberof PurchaseAmountNumber\n */\n culturevoucher?: number;\n /**\n * Gross amount that can be paid with sports voucher in euro cents\n * \n * After payment has been done, this is not included anymore.\n * @type {number}\n * @memberof PurchaseAmountNumber\n */\n sportsvoucher?: number;\n /**\n * If registration is for a spare place, the price is zero at the\n * time of registration, but will be this gross amount if an actual\n * place is eventually found. Amount in euro cents.\n * @type {number}\n * @memberof PurchaseAmountNumber\n */\n spareamount?: number;\n}\n\nexport function PurchaseAmountNumberFromJSON(json: any): PurchaseAmountNumber {\n return PurchaseAmountNumberFromJSONTyped(json, false);\n}\n\nexport function PurchaseAmountNumberFromJSONTyped(json: any, ignoreDiscriminator: boolean): PurchaseAmountNumber {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'amount': json['amount'],\n 'vatpercentage': !exists(json, 'vatpercentage') ? undefined : json['vatpercentage'],\n 'vat': json['vat'],\n 'net': json['net'],\n 'mustpaynow': !exists(json, 'mustpaynow') ? undefined : json['mustpaynow'],\n 'canpaynow': !exists(json, 'canpaynow') ? undefined : json['canpaynow'],\n 'culturevoucher': !exists(json, 'culturevoucher') ? undefined : json['culturevoucher'],\n 'sportsvoucher': !exists(json, 'sportsvoucher') ? undefined : json['sportsvoucher'],\n 'spareamount': !exists(json, 'spareamount') ? undefined : json['spareamount'],\n };\n}\n\nexport function PurchaseAmountNumberToJSON(value?: PurchaseAmountNumber | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'amount': value.amount,\n 'vatpercentage': value.vatpercentage,\n 'vat': value.vat,\n 'net': value.net,\n 'mustpaynow': value.mustpaynow,\n 'canpaynow': value.canpaynow,\n 'culturevoucher': value.culturevoucher,\n 'sportsvoucher': value.sportsvoucher,\n 'spareamount': value.spareamount,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n Contact,\n ContactFromJSON,\n ContactFromJSONTyped,\n ContactToJSON,\n PurchaseAmountNumber,\n PurchaseAmountNumberFromJSON,\n PurchaseAmountNumberFromJSONTyped,\n PurchaseAmountNumberToJSON,\n PurchaseProductItemNumber,\n PurchaseProductItemNumberFromJSON,\n PurchaseProductItemNumberFromJSONTyped,\n PurchaseProductItemNumberToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface PurchaseInvoiceNumber\n */\nexport interface PurchaseInvoiceNumber {\n [key: string]: object | any;\n /**\n * Reference number\n * @type {string}\n * @memberof PurchaseInvoiceNumber\n */\n referencenumber?: string;\n /**\n * Registration code\n * \n * Used for accessing the registration's details after registration is completed\n * @type {string}\n * @memberof PurchaseInvoiceNumber\n */\n registrationcode?: string;\n /**\n * \n * @type {Contact}\n * @memberof PurchaseInvoiceNumber\n */\n payer?: Contact;\n /**\n * Items (rows) for the invoice\n * @type {Array}\n * @memberof PurchaseInvoiceNumber\n */\n items: Array;\n /**\n * Invoice creation timestamp\n * @type {Date}\n * @memberof PurchaseInvoiceNumber\n */\n timestamp: Date;\n /**\n * \n * @type {PurchaseAmountNumber}\n * @memberof PurchaseInvoiceNumber\n */\n total: PurchaseAmountNumber;\n /**\n * This invoice has been sent to accounting (cannot be paid online)\n * @type {boolean}\n * @memberof PurchaseInvoiceNumber\n */\n senttoaccounting: boolean;\n /**\n * Payment URL, defined if this invoice can be paid by Epayment\n * @type {string}\n * @memberof PurchaseInvoiceNumber\n */\n paymenturl?: string;\n}\n\nexport function PurchaseInvoiceNumberFromJSON(json: any): PurchaseInvoiceNumber {\n return PurchaseInvoiceNumberFromJSONTyped(json, false);\n}\n\nexport function PurchaseInvoiceNumberFromJSONTyped(json: any, ignoreDiscriminator: boolean): PurchaseInvoiceNumber {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'referencenumber': !exists(json, 'referencenumber') ? undefined : json['referencenumber'],\n 'registrationcode': !exists(json, 'registrationcode') ? undefined : json['registrationcode'],\n 'payer': !exists(json, 'payer') ? undefined : ContactFromJSON(json['payer']),\n 'items': ((json['items'] as Array).map(PurchaseProductItemNumberFromJSON)),\n 'timestamp': (new Date(json['timestamp'])),\n 'total': PurchaseAmountNumberFromJSON(json['total']),\n 'senttoaccounting': json['senttoaccounting'],\n 'paymenturl': !exists(json, 'paymenturl') ? undefined : json['paymenturl'],\n };\n}\n\nexport function PurchaseInvoiceNumberToJSON(value?: PurchaseInvoiceNumber | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'referencenumber': value.referencenumber,\n 'registrationcode': value.registrationcode,\n 'payer': ContactToJSON(value.payer),\n 'items': ((value.items as Array).map(PurchaseProductItemNumberToJSON)),\n 'timestamp': (value.timestamp.toISOString()),\n 'total': PurchaseAmountNumberToJSON(value.total),\n 'senttoaccounting': value.senttoaccounting,\n 'paymenturl': value.paymenturl,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n Contact,\n ContactFromJSON,\n ContactFromJSONTyped,\n ContactToJSON,\n HellewiCourseMinimal,\n HellewiCourseMinimalFromJSON,\n HellewiCourseMinimalFromJSONTyped,\n HellewiCourseMinimalToJSON,\n PurchaseProductItemType,\n PurchaseProductItemTypeFromJSON,\n PurchaseProductItemTypeFromJSONTyped,\n PurchaseProductItemTypeToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface PurchaseProductItemNumber\n */\nexport interface PurchaseProductItemNumber {\n [key: string]: object | any;\n /**\n * Gross amount (verollinen hinta) in euro cents\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n amount: number;\n /**\n * Value-added tax (arvonlisävero) percentage\n * \n * This is undefined if the amount is total combined from different\n * vat percentages\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n vatpercentage?: number;\n /**\n * Value-added tax (arvonlisävero) amount in euro cents\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n vat: number;\n /**\n * Net amount (veroton hinta) in euro cents\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n net: number;\n /**\n * Gross amount that has to be paid now in euro cents\n * \n * mustpaynow and canpaynow are are mutually exclusive so that if one has a number,\n * the other will be zero (if something has to be paid now, everything must be paid now)\n * \n * After payment has been done, this is not included anymore.\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n mustpaynow?: number;\n /**\n * Gross amount that can to be paid now in euro cents\n * \n * mustpaynow and canpaynow are are mutually exclusive so that if one has a number,\n * the other will be zero (if something has to be paid now, everything must be paid now)\n * \n * After payment has been done, this is not included anymore.\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n canpaynow?: number;\n /**\n * Gross amount that can be paid with culture voucher in euro cents\n * \n * After payment has been done, this is not included anymore.\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n culturevoucher?: number;\n /**\n * Gross amount that can be paid with sports voucher in euro cents\n * \n * After payment has been done, this is not included anymore.\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n sportsvoucher?: number;\n /**\n * If registration is for a spare place, the price is zero at the\n * time of registration, but will be this gross amount if an actual\n * place is eventually found. Amount in euro cents.\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n spareamount?: number;\n /**\n * Item name\n * \n * e.g. course's selected price class, installment name or lesson date\n * @type {string}\n * @memberof PurchaseProductItemNumber\n */\n name: string;\n /**\n * \n * @type {PurchaseProductItemType}\n * @memberof PurchaseProductItemNumber\n */\n type: PurchaseProductItemType;\n /**\n * Item ID, if applicable\n * \n * For whole-course prices, this is the price id number as string\n * For installments, this is the installment's id number as string\n * For lessons, this is the lesson's id number as string\n * For courseProducts, this is the corresponding product's id number as string (not coureProductId)\n * @type {string}\n * @memberof PurchaseProductItemNumber\n */\n id?: string;\n /**\n * Quantity\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n quantity: number;\n /**\n * Unit price\n * @type {number}\n * @memberof PurchaseProductItemNumber\n */\n unitprice: number;\n /**\n * Payment can be done now\n * \n * Not applicable (undefined) if registration is for a spare place\n * @type {boolean}\n * @memberof PurchaseProductItemNumber\n */\n paymentnow?: boolean;\n /**\n * Payment can be done later\n * \n * Not applicable (undefined) if registration is for a spare place\n * @type {boolean}\n * @memberof PurchaseProductItemNumber\n */\n paymentlater?: boolean;\n /**\n * Payment can be done with a culture voucher\n * @type {boolean}\n * @memberof PurchaseProductItemNumber\n */\n paymentwithculturevoucher?: boolean;\n /**\n * Payment can be done with a sports voucher\n * @type {boolean}\n * @memberof PurchaseProductItemNumber\n */\n paymentwithsportsvoucher?: boolean;\n /**\n * Timestamp\n * @type {Date}\n * @memberof PurchaseProductItemNumber\n */\n timestamp?: Date;\n /**\n * \n * @type {Contact}\n * @memberof PurchaseProductItemNumber\n */\n client?: Contact;\n /**\n * \n * @type {HellewiCourseMinimal}\n * @memberof PurchaseProductItemNumber\n */\n course?: HellewiCourseMinimal;\n /**\n * RevenueAccountId\n * \n * Used with courseProducts which may have differing revenueaccounts from what course has\n * @type {string}\n * @memberof PurchaseProductItemNumber\n */\n revenueAccountId?: string;\n}\n\nexport function PurchaseProductItemNumberFromJSON(json: any): PurchaseProductItemNumber {\n return PurchaseProductItemNumberFromJSONTyped(json, false);\n}\n\nexport function PurchaseProductItemNumberFromJSONTyped(json: any, ignoreDiscriminator: boolean): PurchaseProductItemNumber {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'amount': json['amount'],\n 'vatpercentage': !exists(json, 'vatpercentage') ? undefined : json['vatpercentage'],\n 'vat': json['vat'],\n 'net': json['net'],\n 'mustpaynow': !exists(json, 'mustpaynow') ? undefined : json['mustpaynow'],\n 'canpaynow': !exists(json, 'canpaynow') ? undefined : json['canpaynow'],\n 'culturevoucher': !exists(json, 'culturevoucher') ? undefined : json['culturevoucher'],\n 'sportsvoucher': !exists(json, 'sportsvoucher') ? undefined : json['sportsvoucher'],\n 'spareamount': !exists(json, 'spareamount') ? undefined : json['spareamount'],\n 'name': json['name'],\n 'type': PurchaseProductItemTypeFromJSON(json['type']),\n 'id': !exists(json, 'id') ? undefined : json['id'],\n 'quantity': json['quantity'],\n 'unitprice': json['unitprice'],\n 'paymentnow': !exists(json, 'paymentnow') ? undefined : json['paymentnow'],\n 'paymentlater': !exists(json, 'paymentlater') ? undefined : json['paymentlater'],\n 'paymentwithculturevoucher': !exists(json, 'paymentwithculturevoucher') ? undefined : json['paymentwithculturevoucher'],\n 'paymentwithsportsvoucher': !exists(json, 'paymentwithsportsvoucher') ? undefined : json['paymentwithsportsvoucher'],\n 'timestamp': !exists(json, 'timestamp') ? undefined : (new Date(json['timestamp'])),\n 'client': !exists(json, 'client') ? undefined : ContactFromJSON(json['client']),\n 'course': !exists(json, 'course') ? undefined : HellewiCourseMinimalFromJSON(json['course']),\n 'revenueAccountId': !exists(json, 'revenueAccountId') ? undefined : json['revenueAccountId'],\n };\n}\n\nexport function PurchaseProductItemNumberToJSON(value?: PurchaseProductItemNumber | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'amount': value.amount,\n 'vatpercentage': value.vatpercentage,\n 'vat': value.vat,\n 'net': value.net,\n 'mustpaynow': value.mustpaynow,\n 'canpaynow': value.canpaynow,\n 'culturevoucher': value.culturevoucher,\n 'sportsvoucher': value.sportsvoucher,\n 'spareamount': value.spareamount,\n 'name': value.name,\n 'type': PurchaseProductItemTypeToJSON(value.type),\n 'id': value.id,\n 'quantity': value.quantity,\n 'unitprice': value.unitprice,\n 'paymentnow': value.paymentnow,\n 'paymentlater': value.paymentlater,\n 'paymentwithculturevoucher': value.paymentwithculturevoucher,\n 'paymentwithsportsvoucher': value.paymentwithsportsvoucher,\n 'timestamp': value.timestamp === undefined ? undefined : (value.timestamp.toISOString()),\n 'client': ContactToJSON(value.client),\n 'course': HellewiCourseMinimalToJSON(value.course),\n 'revenueAccountId': value.revenueAccountId,\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n Contact,\n ContactFromJSON,\n ContactFromJSONTyped,\n ContactToJSON,\n HellewiCourseLesson,\n HellewiCourseLessonFromJSON,\n HellewiCourseLessonFromJSONTyped,\n HellewiCourseLessonToJSON,\n HellewiCoursePartial,\n HellewiCoursePartialFromJSON,\n HellewiCoursePartialFromJSONTyped,\n HellewiCoursePartialToJSON,\n PurchaseAmountNumber,\n PurchaseAmountNumberFromJSON,\n PurchaseAmountNumberFromJSONTyped,\n PurchaseAmountNumberToJSON,\n PurchaseProductItemNumber,\n PurchaseProductItemNumberFromJSON,\n PurchaseProductItemNumberFromJSONTyped,\n PurchaseProductItemNumberToJSON,\n PurchaseProductStatus,\n PurchaseProductStatusFromJSON,\n PurchaseProductStatusFromJSONTyped,\n PurchaseProductStatusToJSON,\n PurchaseProductType,\n PurchaseProductTypeFromJSON,\n PurchaseProductTypeFromJSONTyped,\n PurchaseProductTypeToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface PurchaseProductNumber\n */\nexport interface PurchaseProductNumber {\n [key: string]: object | any;\n /**\n * Product name\n * @type {string}\n * @memberof PurchaseProductNumber\n */\n name: string;\n /**\n * \n * @type {PurchaseProductType}\n * @memberof PurchaseProductNumber\n */\n type: PurchaseProductType;\n /**\n * ID for the product. If this is a course, then course id.\n * \n * For single-tenant courses, this is a number as string.\n * @type {string}\n * @memberof PurchaseProductNumber\n */\n id?: string;\n /**\n * Items for this product.\n * \n * For example lessons or installments for a course.\n * @type {Array}\n * @memberof PurchaseProductNumber\n */\n items: Array;\n /**\n * \n * @type {PurchaseAmountNumber}\n * @memberof PurchaseProductNumber\n */\n total: PurchaseAmountNumber;\n /**\n * Cart item ID\n * \n * Present if this price is calculated for a cart.\n * @type {number}\n * @memberof PurchaseProductNumber\n */\n cartItemId?: number;\n /**\n * Is this product on spare place\n * @type {boolean}\n * @memberof PurchaseProductNumber\n */\n spare?: boolean;\n /**\n * This products place in spare queue\n * @type {number}\n * @memberof PurchaseProductNumber\n */\n sparecounter?: number;\n /**\n * URL to be called to cancel this course\n * @type {string}\n * @memberof PurchaseProductNumber\n */\n cancellationurl?: string;\n /**\n * Last cancellation date\n * @type {Date}\n * @memberof PurchaseProductNumber\n */\n lastcancellationdate?: Date;\n /**\n * \n * @type {Contact}\n * @memberof PurchaseProductNumber\n */\n client?: Contact;\n /**\n * \n * @type {HellewiCoursePartial}\n * @memberof PurchaseProductNumber\n */\n course?: HellewiCoursePartial;\n /**\n * Lessons if the product is a registration for lessons\n * @type {Array}\n * @memberof PurchaseProductNumber\n */\n lessons?: Array;\n /**\n * Product statuses\n * \n * - `CANCELLED` registration has been cancelled (before course started)\n * - `INTERRUPTED` registration has been interrupted (after course started)\n * - `SPARE` Registration is for a spare place\n * - `LESSONS` registration is for one or more lessons\n * @type {Array}\n * @memberof PurchaseProductNumber\n */\n statuses: Array;\n}\n\nexport function PurchaseProductNumberFromJSON(json: any): PurchaseProductNumber {\n return PurchaseProductNumberFromJSONTyped(json, false);\n}\n\nexport function PurchaseProductNumberFromJSONTyped(json: any, ignoreDiscriminator: boolean): PurchaseProductNumber {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'name': json['name'],\n 'type': PurchaseProductTypeFromJSON(json['type']),\n 'id': !exists(json, 'id') ? undefined : json['id'],\n 'items': ((json['items'] as Array).map(PurchaseProductItemNumberFromJSON)),\n 'total': PurchaseAmountNumberFromJSON(json['total']),\n 'cartItemId': !exists(json, 'cartItemId') ? undefined : json['cartItemId'],\n 'spare': !exists(json, 'spare') ? undefined : json['spare'],\n 'sparecounter': !exists(json, 'sparecounter') ? undefined : json['sparecounter'],\n 'cancellationurl': !exists(json, 'cancellationurl') ? undefined : json['cancellationurl'],\n 'lastcancellationdate': !exists(json, 'lastcancellationdate') ? undefined : (new Date(json['lastcancellationdate'])),\n 'client': !exists(json, 'client') ? undefined : ContactFromJSON(json['client']),\n 'course': !exists(json, 'course') ? undefined : HellewiCoursePartialFromJSON(json['course']),\n 'lessons': !exists(json, 'lessons') ? undefined : ((json['lessons'] as Array).map(HellewiCourseLessonFromJSON)),\n 'statuses': ((json['statuses'] as Array).map(PurchaseProductStatusFromJSON)),\n };\n}\n\nexport function PurchaseProductNumberToJSON(value?: PurchaseProductNumber | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'name': value.name,\n 'type': PurchaseProductTypeToJSON(value.type),\n 'id': value.id,\n 'items': ((value.items as Array).map(PurchaseProductItemNumberToJSON)),\n 'total': PurchaseAmountNumberToJSON(value.total),\n 'cartItemId': value.cartItemId,\n 'spare': value.spare,\n 'sparecounter': value.sparecounter,\n 'cancellationurl': value.cancellationurl,\n 'lastcancellationdate': value.lastcancellationdate === undefined ? undefined : (value.lastcancellationdate.toISOString()),\n 'client': ContactToJSON(value.client),\n 'course': HellewiCoursePartialToJSON(value.course),\n 'lessons': value.lessons === undefined ? undefined : ((value.lessons as Array).map(HellewiCourseLessonToJSON)),\n 'statuses': ((value.statuses as Array).map(PurchaseProductStatusToJSON)),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nimport { exists, mapValues } from '../runtime';\nimport {\n PurchaseAmountNumber,\n PurchaseAmountNumberFromJSON,\n PurchaseAmountNumberFromJSONTyped,\n PurchaseAmountNumberToJSON,\n PurchaseProductNumber,\n PurchaseProductNumberFromJSON,\n PurchaseProductNumberFromJSONTyped,\n PurchaseProductNumberToJSON,\n} from './';\n\n/**\n * \n * @export\n * @interface RegistrationPriceNumber\n */\nexport interface RegistrationPriceNumber {\n [key: string]: object | any;\n /**\n * Products\n * @type {Array}\n * @memberof RegistrationPriceNumber\n */\n products: Array;\n /**\n * \n * @type {PurchaseAmountNumber}\n * @memberof RegistrationPriceNumber\n */\n productstotal: PurchaseAmountNumber;\n /**\n * Total amounts for listed products grouped by VAT percentages.\n * \n * This can be used for constructing a VAT breakdown (ALV-erittely)\n * @type {Array}\n * @memberof RegistrationPriceNumber\n */\n productsgroupedvatamounts: Array;\n}\n\nexport function RegistrationPriceNumberFromJSON(json: any): RegistrationPriceNumber {\n return RegistrationPriceNumberFromJSONTyped(json, false);\n}\n\nexport function RegistrationPriceNumberFromJSONTyped(json: any, ignoreDiscriminator: boolean): RegistrationPriceNumber {\n if ((json === undefined) || (json === null)) {\n return json;\n }\n return {\n \n ...json,\n 'products': ((json['products'] as Array).map(PurchaseProductNumberFromJSON)),\n 'productstotal': PurchaseAmountNumberFromJSON(json['productstotal']),\n 'productsgroupedvatamounts': ((json['productsgroupedvatamounts'] as Array).map(PurchaseAmountNumberFromJSON)),\n };\n}\n\nexport function RegistrationPriceNumberToJSON(value?: RegistrationPriceNumber | null): any {\n if (value === undefined) {\n return undefined;\n }\n if (value === null) {\n return null;\n }\n return {\n \n ...value,\n 'products': ((value.products as Array).map(PurchaseProductNumberToJSON)),\n 'productstotal': PurchaseAmountNumberToJSON(value.productstotal),\n 'productsgroupedvatamounts': ((value.productsgroupedvatamounts as Array).map(PurchaseAmountNumberToJSON)),\n };\n}\n\n\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport * as runtime from '../runtime';\nimport {\n CatalogItemNotSupportedError,\n CatalogItemNotSupportedErrorFromJSON,\n CatalogItemNotSupportedErrorToJSON,\n ErrorResponse,\n ErrorResponseFromJSON,\n ErrorResponseToJSON,\n HellewiBrand,\n HellewiBrandFromJSON,\n HellewiBrandToJSON,\n HellewiCatalogItemText,\n HellewiCatalogItemTextFromJSON,\n HellewiCatalogItemTextToJSON,\n HellewiPromotion,\n HellewiPromotionFromJSON,\n HellewiPromotionToJSON,\n HellewiText,\n HellewiTextFromJSON,\n HellewiTextToJSON,\n} from '../models';\n\nexport interface GetCatalogItemDetailsRequest {\n keyword: string;\n}\n\n/**\n * BrandApi - interface\n * \n * @export\n * @interface BrandApiInterface\n */\nexport interface BrandApiInterface {\n /**\n * Tenant\\'s brand information\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof BrandApiInterface\n */\n getBrandRaw(): Promise>;\n\n /**\n * Tenant\\'s brand information\n */\n getBrand(): Promise;\n\n /**\n * Get name and description for catalog-item\n * @param {string} keyword \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof BrandApiInterface\n */\n getCatalogItemDetailsRaw(requestParameters: GetCatalogItemDetailsRequest): Promise>;\n\n /**\n * Get name and description for catalog-item\n */\n getCatalogItemDetails(requestParameters: GetCatalogItemDetailsRequest): Promise;\n\n /**\n * Tenant\\'s help page text and privacy statement\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof BrandApiInterface\n */\n getHelpRaw(): Promise>;\n\n /**\n * Tenant\\'s help page text and privacy statement\n */\n getHelp(): Promise;\n\n /**\n * Tenant\\'s brand information to hero element\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof BrandApiInterface\n */\n listPromotionsRaw(): Promise>>;\n\n /**\n * Tenant\\'s brand information to hero element\n */\n listPromotions(): Promise>;\n\n}\n\n/**\n * \n */\nexport class BrandApi extends runtime.BaseAPI implements BrandApiInterface {\n\n /**\n * Tenant\\'s brand information\n */\n async getBrandRaw(): Promise> {\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/brand`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiBrandFromJSON(jsonValue));\n }\n\n /**\n * Tenant\\'s brand information\n */\n async getBrand(): Promise {\n const response = await this.getBrandRaw();\n return await response.value();\n }\n\n /**\n * Get name and description for catalog-item\n */\n async getCatalogItemDetailsRaw(requestParameters: GetCatalogItemDetailsRequest): Promise> {\n if (requestParameters.keyword === null || requestParameters.keyword === undefined) {\n throw new runtime.RequiredError('keyword','Required parameter requestParameters.keyword was null or undefined when calling getCatalogItemDetails.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/brand/catalog-item/{keyword}`.replace(`{${\"keyword\"}}`, encodeURIComponent(String(requestParameters.keyword))),\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCatalogItemTextFromJSON(jsonValue));\n }\n\n /**\n * Get name and description for catalog-item\n */\n async getCatalogItemDetails(requestParameters: GetCatalogItemDetailsRequest): Promise {\n const response = await this.getCatalogItemDetailsRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Tenant\\'s help page text and privacy statement\n */\n async getHelpRaw(): Promise> {\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/brand/help`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiTextFromJSON(jsonValue));\n }\n\n /**\n * Tenant\\'s help page text and privacy statement\n */\n async getHelp(): Promise {\n const response = await this.getHelpRaw();\n return await response.value();\n }\n\n /**\n * Tenant\\'s brand information to hero element\n */\n async listPromotionsRaw(): Promise>> {\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/brand/promotions`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(HellewiPromotionFromJSON));\n }\n\n /**\n * Tenant\\'s brand information to hero element\n */\n async listPromotions(): Promise> {\n const response = await this.listPromotionsRaw();\n return await response.value();\n }\n\n}\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport * as runtime from '../runtime';\nimport {\n ErrorResponse,\n ErrorResponseFromJSON,\n ErrorResponseToJSON,\n HellewiLocation,\n HellewiLocationFromJSON,\n HellewiLocationToJSON,\n HellewiLocationSortfields,\n HellewiLocationSortfieldsFromJSON,\n HellewiLocationSortfieldsToJSON,\n HellewiReservation,\n HellewiReservationFromJSON,\n HellewiReservationToJSON,\n HellewiReservationSortfields,\n HellewiReservationSortfieldsFromJSON,\n HellewiReservationSortfieldsToJSON,\n Sortdir,\n SortdirFromJSON,\n SortdirToJSON,\n} from '../models';\n\nexport interface GetLocationReservationsRequest {\n id: number;\n startdate?: Date;\n enddate?: Date;\n page?: number;\n limit?: number;\n sort?: Array;\n sortdir?: Sortdir;\n searchkey?: GetLocationReservationsSearchkeyEnum;\n}\n\nexport interface GetLocationsRequest {\n page?: number;\n limit?: number;\n sort?: Array;\n sortdir?: Sortdir;\n}\n\n/**\n * LocationApi - interface\n * \n * @export\n * @interface LocationApiInterface\n */\nexport interface LocationApiInterface {\n /**\n * Get reservations for given location\n * @param {number} id Location id\n * @param {Date} [startdate] Query for reservations starting from this date, default: start of current week\n * @param {Date} [enddate] Query for reservations ending on this date, default: end of current week\n * @param {number} [page] Pagination: wanted page starting from 1\n * @param {number} [limit] Pagination: how many items there are per page (maximum 100)\n * @param {Array} [sort] Result sorting: sort according to these fields\n * @param {Sortdir} [sortdir] Result sorting: sort direction, ascending or descending\n * @param {'id' | 'externalid'} [searchkey] \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof LocationApiInterface\n */\n getLocationReservationsRaw(requestParameters: GetLocationReservationsRequest): Promise>>;\n\n /**\n * Get reservations for given location\n */\n getLocationReservations(requestParameters: GetLocationReservationsRequest): Promise>;\n\n /**\n * Get locations\n * @param {number} [page] Pagination: wanted page starting from 1\n * @param {number} [limit] Pagination: how many items there are per page (maximum 100)\n * @param {Array} [sort] Result sorting: sort according to these fields\n * @param {Sortdir} [sortdir] Result sorting: sort direction, ascending or descending\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof LocationApiInterface\n */\n getLocationsRaw(requestParameters: GetLocationsRequest): Promise>>;\n\n /**\n * Get locations\n */\n getLocations(requestParameters: GetLocationsRequest): Promise>;\n\n}\n\n/**\n * \n */\nexport class LocationApi extends runtime.BaseAPI implements LocationApiInterface {\n\n /**\n * Get reservations for given location\n */\n async getLocationReservationsRaw(requestParameters: GetLocationReservationsRequest): Promise>> {\n if (requestParameters.id === null || requestParameters.id === undefined) {\n throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling getLocationReservations.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.startdate !== undefined) {\n queryParameters['startdate'] = (requestParameters.startdate as any).toISOString().substr(0,10);\n }\n\n if (requestParameters.enddate !== undefined) {\n queryParameters['enddate'] = (requestParameters.enddate as any).toISOString().substr(0,10);\n }\n\n if (requestParameters.page !== undefined) {\n queryParameters['page'] = requestParameters.page;\n }\n\n if (requestParameters.limit !== undefined) {\n queryParameters['limit'] = requestParameters.limit;\n }\n\n if (requestParameters.sort) {\n queryParameters['sort'] = requestParameters.sort;\n }\n\n if (requestParameters.sortdir !== undefined) {\n queryParameters['sortdir'] = requestParameters.sortdir;\n }\n\n if (requestParameters.searchkey !== undefined) {\n queryParameters['searchkey'] = requestParameters.searchkey;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/locations/{id}/reservations`.replace(`{${\"id\"}}`, encodeURIComponent(String(requestParameters.id))),\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(HellewiReservationFromJSON));\n }\n\n /**\n * Get reservations for given location\n */\n async getLocationReservations(requestParameters: GetLocationReservationsRequest): Promise> {\n const response = await this.getLocationReservationsRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Get locations\n */\n async getLocationsRaw(requestParameters: GetLocationsRequest): Promise>> {\n const queryParameters: any = {};\n\n if (requestParameters.page !== undefined) {\n queryParameters['page'] = requestParameters.page;\n }\n\n if (requestParameters.limit !== undefined) {\n queryParameters['limit'] = requestParameters.limit;\n }\n\n if (requestParameters.sort) {\n queryParameters['sort'] = requestParameters.sort;\n }\n\n if (requestParameters.sortdir !== undefined) {\n queryParameters['sortdir'] = requestParameters.sortdir;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/locations`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(HellewiLocationFromJSON));\n }\n\n /**\n * Get locations\n */\n async getLocations(requestParameters: GetLocationsRequest): Promise> {\n const response = await this.getLocationsRaw(requestParameters);\n return await response.value();\n }\n\n}\n\n/**\n * @export\n * @enum {string}\n */\nexport enum GetLocationReservationsSearchkeyEnum {\n Id = 'id',\n Externalid = 'externalid'\n}\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport * as runtime from '../runtime';\nimport {\n HellewiCallout,\n HellewiCalloutFromJSON,\n HellewiCalloutToJSON,\n} from '../models';\n\n/**\n * CalloutsApi - interface\n * \n * @export\n * @interface CalloutsApiInterface\n */\nexport interface CalloutsApiInterface {\n /**\n * Tenant\\'s callouts\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CalloutsApiInterface\n */\n getLocalCalloutsRaw(): Promise>>;\n\n /**\n * Tenant\\'s callouts\n */\n getLocalCallouts(): Promise>;\n\n}\n\n/**\n * \n */\nexport class CalloutsApi extends runtime.BaseAPI implements CalloutsApiInterface {\n\n /**\n * Tenant\\'s callouts\n */\n async getLocalCalloutsRaw(): Promise>> {\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/callouts/local`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(HellewiCalloutFromJSON));\n }\n\n /**\n * Tenant\\'s callouts\n */\n async getLocalCallouts(): Promise> {\n const response = await this.getLocalCalloutsRaw();\n return await response.value();\n }\n\n}\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport * as runtime from '../runtime';\nimport {\n ErrorResponse,\n ErrorResponseFromJSON,\n ErrorResponseToJSON,\n HellewiCartItem,\n HellewiCartItemFromJSON,\n HellewiCartItemToJSON,\n HellewiCartItemId,\n HellewiCartItemIdFromJSON,\n HellewiCartItemIdToJSON,\n HellewiCartStatus,\n HellewiCartStatusFromJSON,\n HellewiCartStatusToJSON,\n} from '../models';\n\nexport interface AddCartItemRequest {\n hellewiCartItemId: Array;\n}\n\nexport interface DeleteCartItemRequest {\n id: number;\n}\n\nexport interface GetCartItemRequest {\n id: number;\n}\n\n/**\n * CartApi - interface\n * \n * @export\n * @interface CartApiInterface\n */\nexport interface CartApiInterface {\n /**\n * Add course to cart Supports adding multiple courses, or multiple instances of the same course to cart at once. If adding fails for one item, none are added to cart. Adding a course to cart will refresh the cart expiry time. Maximum number that a single course can be in an active cart is limited (`course.cartcount` field). **Module courses** If added course is the parent of a module, all child courses are added as well. If the cart or current request has the same child courses, they are removed and re-added along with the parent. This means that you can register only one person at a time to module courses. Also, if you add a module parent course to cart, the child courses will get an actual place even if the course was full, or even if its registration wasn\\'t open. **Courses with registration to lessons**\n * @param {Array} hellewiCartItemId \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CartApiInterface\n */\n addCartItemRaw(requestParameters: AddCartItemRequest): Promise>;\n\n /**\n * Add course to cart Supports adding multiple courses, or multiple instances of the same course to cart at once. If adding fails for one item, none are added to cart. Adding a course to cart will refresh the cart expiry time. Maximum number that a single course can be in an active cart is limited (`course.cartcount` field). **Module courses** If added course is the parent of a module, all child courses are added as well. If the cart or current request has the same child courses, they are removed and re-added along with the parent. This means that you can register only one person at a time to module courses. Also, if you add a module parent course to cart, the child courses will get an actual place even if the course was full, or even if its registration wasn\\'t open. **Courses with registration to lessons**\n */\n addCartItem(requestParameters: AddCartItemRequest): Promise;\n\n /**\n * Remove item from cart\n * @param {number} id \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CartApiInterface\n */\n deleteCartItemRaw(requestParameters: DeleteCartItemRequest): Promise>;\n\n /**\n * Remove item from cart\n */\n deleteCartItem(requestParameters: DeleteCartItemRequest): Promise;\n\n /**\n * Get cart item\n * @param {number} id \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CartApiInterface\n */\n getCartItemRaw(requestParameters: GetCartItemRequest): Promise>;\n\n /**\n * Get cart item\n */\n getCartItem(requestParameters: GetCartItemRequest): Promise;\n\n /**\n * Get cart information\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CartApiInterface\n */\n getCartStatusRaw(): Promise>;\n\n /**\n * Get cart information\n */\n getCartStatus(): Promise;\n\n /**\n * List cart items\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CartApiInterface\n */\n listCartItemsRaw(): Promise>>;\n\n /**\n * List cart items\n */\n listCartItems(): Promise>;\n\n}\n\n/**\n * \n */\nexport class CartApi extends runtime.BaseAPI implements CartApiInterface {\n\n /**\n * Add course to cart Supports adding multiple courses, or multiple instances of the same course to cart at once. If adding fails for one item, none are added to cart. Adding a course to cart will refresh the cart expiry time. Maximum number that a single course can be in an active cart is limited (`course.cartcount` field). **Module courses** If added course is the parent of a module, all child courses are added as well. If the cart or current request has the same child courses, they are removed and re-added along with the parent. This means that you can register only one person at a time to module courses. Also, if you add a module parent course to cart, the child courses will get an actual place even if the course was full, or even if its registration wasn\\'t open. **Courses with registration to lessons**\n */\n async addCartItemRaw(requestParameters: AddCartItemRequest): Promise> {\n if (requestParameters.hellewiCartItemId === null || requestParameters.hellewiCartItemId === undefined) {\n throw new runtime.RequiredError('hellewiCartItemId','Required parameter requestParameters.hellewiCartItemId was null or undefined when calling addCartItem.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/carts`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.hellewiCartItemId.map(HellewiCartItemIdToJSON),\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCartStatusFromJSON(jsonValue));\n }\n\n /**\n * Add course to cart Supports adding multiple courses, or multiple instances of the same course to cart at once. If adding fails for one item, none are added to cart. Adding a course to cart will refresh the cart expiry time. Maximum number that a single course can be in an active cart is limited (`course.cartcount` field). **Module courses** If added course is the parent of a module, all child courses are added as well. If the cart or current request has the same child courses, they are removed and re-added along with the parent. This means that you can register only one person at a time to module courses. Also, if you add a module parent course to cart, the child courses will get an actual place even if the course was full, or even if its registration wasn\\'t open. **Courses with registration to lessons**\n */\n async addCartItem(requestParameters: AddCartItemRequest): Promise {\n const response = await this.addCartItemRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Remove item from cart\n */\n async deleteCartItemRaw(requestParameters: DeleteCartItemRequest): Promise> {\n if (requestParameters.id === null || requestParameters.id === undefined) {\n throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling deleteCartItem.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/carts/{id}`.replace(`{${\"id\"}}`, encodeURIComponent(String(requestParameters.id))),\n method: 'DELETE',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCartStatusFromJSON(jsonValue));\n }\n\n /**\n * Remove item from cart\n */\n async deleteCartItem(requestParameters: DeleteCartItemRequest): Promise {\n const response = await this.deleteCartItemRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Get cart item\n */\n async getCartItemRaw(requestParameters: GetCartItemRequest): Promise> {\n if (requestParameters.id === null || requestParameters.id === undefined) {\n throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling getCartItem.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/carts/{id}`.replace(`{${\"id\"}}`, encodeURIComponent(String(requestParameters.id))),\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCartItemFromJSON(jsonValue));\n }\n\n /**\n * Get cart item\n */\n async getCartItem(requestParameters: GetCartItemRequest): Promise {\n const response = await this.getCartItemRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Get cart information\n */\n async getCartStatusRaw(): Promise> {\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/cart-status`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCartStatusFromJSON(jsonValue));\n }\n\n /**\n * Get cart information\n */\n async getCartStatus(): Promise {\n const response = await this.getCartStatusRaw();\n return await response.value();\n }\n\n /**\n * List cart items\n */\n async listCartItemsRaw(): Promise>> {\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/carts`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(HellewiCartItemFromJSON));\n }\n\n /**\n * List cart items\n */\n async listCartItems(): Promise> {\n const response = await this.listCartItemsRaw();\n return await response.value();\n }\n\n}\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport * as runtime from '../runtime';\nimport {\n ErrorResponse,\n ErrorResponseFromJSON,\n ErrorResponseToJSON,\n HellewiCatalog,\n HellewiCatalogFromJSON,\n HellewiCatalogToJSON,\n HellewiCatalogSettings,\n HellewiCatalogSettingsFromJSON,\n HellewiCatalogSettingsToJSON,\n} from '../models';\n\nexport interface GetCatalogRequest {\n q?: string;\n}\n\n/**\n * CatalogApi - interface\n * \n * @export\n * @interface CatalogApiInterface\n */\nexport interface CatalogApiInterface {\n /**\n * List of catalog items (e.g. subject) that can be used for filtering courses. Different items are grouped together in the response. ```markup Subject - [ ] Philosophy, 3 courses - [ ] French, 3 courses - [ ] German, 1 course Location - [ ] Ahjola, 2 courses - [ ] Sampola, 4 courses Weekday (*1) - [ ] Monday, 1 course - [ ] Tuesday, 2 courses - [ ] Thursday, 1 course - [ ] Sunday, 3 courses ``` - **1:** A course can have multiple catalog items of some catalog item categories, such as weekdays (e.g. a single course can be held on Mondays and Tuesdays). ### Catalog filtering The response is filtered according to given search query: course count matches the query, and catalog items that don\\'t have any matching courses are omitted. There is one exception to the rule above: if a subject is selected for example, the response subject includes *all* subjects and not just the selected ones. This allows to create the following kind of catalog structure (philosophy is selected, query something like `q=subject:5`): ```markup Subject - [x] Philosophy, 3 courses - [ ] French, 3 courses (*1) - [ ] German, 1 course (*1) Location - [ ] Ahjola, 1 course - [ ] Sampola, 2 courses Weekday - [ ] Tuesday, 2 courses - [ ] Sunday, 1 courses ``` - **1:** Here French and German are included in the results, even though the three courses that match philosophy don\\'t match the languages. If multiple items from different categories are selected, result will be filtered so that for a single catalog item group, all other filters except its own apply to that group (`q:subject:5+location:2`): ```markup Subject - [x] Philosophy, 2 courses (*1) Location - [ ] Ahjola, 1 course (*2) - [x] Sampola, 2 courses Weekday - [ ] Tuesday, 1 course - [ ] Sunday, 1 course ``` - **1:** French and German are omitted because all courses in Sampola are philosophy, and philosophy has only two courses that are in Sampola. - **2:** Ahjola is included because there would be a third philosophy course. ### Tree structure in catalog All the catalog item groups are arrays of depth one, but HellewiCatalogItem objects contain parent information that can be used to create a tree structure of those catalog items. ```markup Department, Category, Subject - [ ] Mäntsälä, 4 courses - [ ] Psychology and pedagogy, 1 course - [ ] Psychology, 1 course - [ ] Languages, 4 courses - [ ] French, 3 courses - [ ] German, 1 course - [ ] Pornainen, 2 courses - [ ] Psychology and pedagogy, 2 courses - [ ] Psychology, 2 courses ``` This can be created so that for each item in departments, search for all items in categories that have that department\\'s keyword as parent. And continue with each of those categories matching parents from subjects. Following parent-child -chains are possible: - Department - Category - Subject - Category - Subject - Term - Period - Classification - Education sector Term - Period and Classification - Education sector are simple so that the relationship is always 1:n, i.e. a period will always have a term as parent, and there cannot be duplicate periods with different parents. Department - Category - Subject is complex. Relationships are n:m, i.e. a category can have different departments as parent (for example language courses are taught both in Mäntsälä and Pornainen). Furthermore, the response includes information so that depth 1 (subject-only), 2 (category-subject) and 3 (department-category-subject) catalog trees can be built. The keywords are also different for these different depth options. For example subject for: - depth 1 will have one id: `subject:34`, - depth 2 will have two ids: `subject:7/34`, this also implies parent `category:7` - depth 3 will have three: `subject:2/7/34`, this also implies parent `category:2/7`, and furthermore category\\'s parent `department:2` (but this is not included in the subject catalog item, only in the parent category) Categories are also different for depth 2 and 3. The simple algorithm for building these is to start from the top and select those items that have parent: null, and then continue with the children that have matching parents. ### Tree structure and filtering The rule that filtering applies to all other catalog item groups except the selected one has some implications when building a tree structured catalog. For example, selecting Mäntsälä - Psycholog and pedagogy - Psychology, `q=subject:1/1/1`: ```markup Department, Category, Subject - [ ] Mäntsälä, 1 course (*1) - [ ] Psychology and pedagogy, 1 course (*3) - [x] Psychology, 1 course - [ ] Languages, 0 courses (*4) - [ ] French, 3 courses (*6) - [ ] German, 1 courses - [ ] Pornainen, 0 courses (*2) - [ ] Psychology and pedagogy, 0 courses (*5) - [ ] Psychology, 2 courses (*7) ``` - **1:** Mäntsälä (`department:1`) is included in the response, as the psychology course matches this department. Course count is 1 as there is one psychology course. - **2:** Pornainen (`department:2`) is *not included* in the response `subject:1/1/1`-courses don\\'t match Pornainen. - **3:** Mäntsälä\\'s Psychology and pedagogy (`category:1/1`) is included in the response with course count 1. - **4:** Mäntsälä\\'s Languages (`category:1/2`) is *not included* in the response. - **5:** Pornainen\\'s Psychology and pedagogy (`category:2/1`) is *not included* in the response. - **6:** Mäntsälä-Languages-French and (`subject:1/2/1`) and German (`subject:1/2/2`) *are included* in the response, as the subject filtering doesn\\'t apply to other subjects. This point can be counter-intuitive when used in tree structure. - **7:** Pornainen-Psychology and pedagogy-Psychology (`subject:2/1/1`) also *is included* in the response. Depending on which kind of user interface is being built, this tree probably cannot be built only from a filtered response, but rather the filtered and unfiltered response have to be combined. For this kind of depth 3 catalog, it would probably be the simplest to add only catalog items with type subject to search query (e.g. selecting Mäntsälä selects all subjects under it and doesn\\'t add the department or its categories). Then also the course counts for deparments and categories could be calculated from response subjects. And the departments and categores would have to be saved from the unfiltered response as the filtered responses don\\'t have the unmatching ones included.\n * @param {string} [q] Search query, see instructions in [Search](#section/Search).\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CatalogApiInterface\n */\n getCatalogRaw(requestParameters: GetCatalogRequest): Promise>;\n\n /**\n * List of catalog items (e.g. subject) that can be used for filtering courses. Different items are grouped together in the response. ```markup Subject - [ ] Philosophy, 3 courses - [ ] French, 3 courses - [ ] German, 1 course Location - [ ] Ahjola, 2 courses - [ ] Sampola, 4 courses Weekday (*1) - [ ] Monday, 1 course - [ ] Tuesday, 2 courses - [ ] Thursday, 1 course - [ ] Sunday, 3 courses ``` - **1:** A course can have multiple catalog items of some catalog item categories, such as weekdays (e.g. a single course can be held on Mondays and Tuesdays). ### Catalog filtering The response is filtered according to given search query: course count matches the query, and catalog items that don\\'t have any matching courses are omitted. There is one exception to the rule above: if a subject is selected for example, the response subject includes *all* subjects and not just the selected ones. This allows to create the following kind of catalog structure (philosophy is selected, query something like `q=subject:5`): ```markup Subject - [x] Philosophy, 3 courses - [ ] French, 3 courses (*1) - [ ] German, 1 course (*1) Location - [ ] Ahjola, 1 course - [ ] Sampola, 2 courses Weekday - [ ] Tuesday, 2 courses - [ ] Sunday, 1 courses ``` - **1:** Here French and German are included in the results, even though the three courses that match philosophy don\\'t match the languages. If multiple items from different categories are selected, result will be filtered so that for a single catalog item group, all other filters except its own apply to that group (`q:subject:5+location:2`): ```markup Subject - [x] Philosophy, 2 courses (*1) Location - [ ] Ahjola, 1 course (*2) - [x] Sampola, 2 courses Weekday - [ ] Tuesday, 1 course - [ ] Sunday, 1 course ``` - **1:** French and German are omitted because all courses in Sampola are philosophy, and philosophy has only two courses that are in Sampola. - **2:** Ahjola is included because there would be a third philosophy course. ### Tree structure in catalog All the catalog item groups are arrays of depth one, but HellewiCatalogItem objects contain parent information that can be used to create a tree structure of those catalog items. ```markup Department, Category, Subject - [ ] Mäntsälä, 4 courses - [ ] Psychology and pedagogy, 1 course - [ ] Psychology, 1 course - [ ] Languages, 4 courses - [ ] French, 3 courses - [ ] German, 1 course - [ ] Pornainen, 2 courses - [ ] Psychology and pedagogy, 2 courses - [ ] Psychology, 2 courses ``` This can be created so that for each item in departments, search for all items in categories that have that department\\'s keyword as parent. And continue with each of those categories matching parents from subjects. Following parent-child -chains are possible: - Department - Category - Subject - Category - Subject - Term - Period - Classification - Education sector Term - Period and Classification - Education sector are simple so that the relationship is always 1:n, i.e. a period will always have a term as parent, and there cannot be duplicate periods with different parents. Department - Category - Subject is complex. Relationships are n:m, i.e. a category can have different departments as parent (for example language courses are taught both in Mäntsälä and Pornainen). Furthermore, the response includes information so that depth 1 (subject-only), 2 (category-subject) and 3 (department-category-subject) catalog trees can be built. The keywords are also different for these different depth options. For example subject for: - depth 1 will have one id: `subject:34`, - depth 2 will have two ids: `subject:7/34`, this also implies parent `category:7` - depth 3 will have three: `subject:2/7/34`, this also implies parent `category:2/7`, and furthermore category\\'s parent `department:2` (but this is not included in the subject catalog item, only in the parent category) Categories are also different for depth 2 and 3. The simple algorithm for building these is to start from the top and select those items that have parent: null, and then continue with the children that have matching parents. ### Tree structure and filtering The rule that filtering applies to all other catalog item groups except the selected one has some implications when building a tree structured catalog. For example, selecting Mäntsälä - Psycholog and pedagogy - Psychology, `q=subject:1/1/1`: ```markup Department, Category, Subject - [ ] Mäntsälä, 1 course (*1) - [ ] Psychology and pedagogy, 1 course (*3) - [x] Psychology, 1 course - [ ] Languages, 0 courses (*4) - [ ] French, 3 courses (*6) - [ ] German, 1 courses - [ ] Pornainen, 0 courses (*2) - [ ] Psychology and pedagogy, 0 courses (*5) - [ ] Psychology, 2 courses (*7) ``` - **1:** Mäntsälä (`department:1`) is included in the response, as the psychology course matches this department. Course count is 1 as there is one psychology course. - **2:** Pornainen (`department:2`) is *not included* in the response `subject:1/1/1`-courses don\\'t match Pornainen. - **3:** Mäntsälä\\'s Psychology and pedagogy (`category:1/1`) is included in the response with course count 1. - **4:** Mäntsälä\\'s Languages (`category:1/2`) is *not included* in the response. - **5:** Pornainen\\'s Psychology and pedagogy (`category:2/1`) is *not included* in the response. - **6:** Mäntsälä-Languages-French and (`subject:1/2/1`) and German (`subject:1/2/2`) *are included* in the response, as the subject filtering doesn\\'t apply to other subjects. This point can be counter-intuitive when used in tree structure. - **7:** Pornainen-Psychology and pedagogy-Psychology (`subject:2/1/1`) also *is included* in the response. Depending on which kind of user interface is being built, this tree probably cannot be built only from a filtered response, but rather the filtered and unfiltered response have to be combined. For this kind of depth 3 catalog, it would probably be the simplest to add only catalog items with type subject to search query (e.g. selecting Mäntsälä selects all subjects under it and doesn\\'t add the department or its categories). Then also the course counts for deparments and categories could be calculated from response subjects. And the departments and categores would have to be saved from the unfiltered response as the filtered responses don\\'t have the unmatching ones included.\n */\n getCatalog(requestParameters: GetCatalogRequest): Promise;\n\n /**\n * Catalog settings\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CatalogApiInterface\n */\n getCatalogSettingsRaw(): Promise>;\n\n /**\n * Catalog settings\n */\n getCatalogSettings(): Promise;\n\n}\n\n/**\n * \n */\nexport class CatalogApi extends runtime.BaseAPI implements CatalogApiInterface {\n\n /**\n * List of catalog items (e.g. subject) that can be used for filtering courses. Different items are grouped together in the response. ```markup Subject - [ ] Philosophy, 3 courses - [ ] French, 3 courses - [ ] German, 1 course Location - [ ] Ahjola, 2 courses - [ ] Sampola, 4 courses Weekday (*1) - [ ] Monday, 1 course - [ ] Tuesday, 2 courses - [ ] Thursday, 1 course - [ ] Sunday, 3 courses ``` - **1:** A course can have multiple catalog items of some catalog item categories, such as weekdays (e.g. a single course can be held on Mondays and Tuesdays). ### Catalog filtering The response is filtered according to given search query: course count matches the query, and catalog items that don\\'t have any matching courses are omitted. There is one exception to the rule above: if a subject is selected for example, the response subject includes *all* subjects and not just the selected ones. This allows to create the following kind of catalog structure (philosophy is selected, query something like `q=subject:5`): ```markup Subject - [x] Philosophy, 3 courses - [ ] French, 3 courses (*1) - [ ] German, 1 course (*1) Location - [ ] Ahjola, 1 course - [ ] Sampola, 2 courses Weekday - [ ] Tuesday, 2 courses - [ ] Sunday, 1 courses ``` - **1:** Here French and German are included in the results, even though the three courses that match philosophy don\\'t match the languages. If multiple items from different categories are selected, result will be filtered so that for a single catalog item group, all other filters except its own apply to that group (`q:subject:5+location:2`): ```markup Subject - [x] Philosophy, 2 courses (*1) Location - [ ] Ahjola, 1 course (*2) - [x] Sampola, 2 courses Weekday - [ ] Tuesday, 1 course - [ ] Sunday, 1 course ``` - **1:** French and German are omitted because all courses in Sampola are philosophy, and philosophy has only two courses that are in Sampola. - **2:** Ahjola is included because there would be a third philosophy course. ### Tree structure in catalog All the catalog item groups are arrays of depth one, but HellewiCatalogItem objects contain parent information that can be used to create a tree structure of those catalog items. ```markup Department, Category, Subject - [ ] Mäntsälä, 4 courses - [ ] Psychology and pedagogy, 1 course - [ ] Psychology, 1 course - [ ] Languages, 4 courses - [ ] French, 3 courses - [ ] German, 1 course - [ ] Pornainen, 2 courses - [ ] Psychology and pedagogy, 2 courses - [ ] Psychology, 2 courses ``` This can be created so that for each item in departments, search for all items in categories that have that department\\'s keyword as parent. And continue with each of those categories matching parents from subjects. Following parent-child -chains are possible: - Department - Category - Subject - Category - Subject - Term - Period - Classification - Education sector Term - Period and Classification - Education sector are simple so that the relationship is always 1:n, i.e. a period will always have a term as parent, and there cannot be duplicate periods with different parents. Department - Category - Subject is complex. Relationships are n:m, i.e. a category can have different departments as parent (for example language courses are taught both in Mäntsälä and Pornainen). Furthermore, the response includes information so that depth 1 (subject-only), 2 (category-subject) and 3 (department-category-subject) catalog trees can be built. The keywords are also different for these different depth options. For example subject for: - depth 1 will have one id: `subject:34`, - depth 2 will have two ids: `subject:7/34`, this also implies parent `category:7` - depth 3 will have three: `subject:2/7/34`, this also implies parent `category:2/7`, and furthermore category\\'s parent `department:2` (but this is not included in the subject catalog item, only in the parent category) Categories are also different for depth 2 and 3. The simple algorithm for building these is to start from the top and select those items that have parent: null, and then continue with the children that have matching parents. ### Tree structure and filtering The rule that filtering applies to all other catalog item groups except the selected one has some implications when building a tree structured catalog. For example, selecting Mäntsälä - Psycholog and pedagogy - Psychology, `q=subject:1/1/1`: ```markup Department, Category, Subject - [ ] Mäntsälä, 1 course (*1) - [ ] Psychology and pedagogy, 1 course (*3) - [x] Psychology, 1 course - [ ] Languages, 0 courses (*4) - [ ] French, 3 courses (*6) - [ ] German, 1 courses - [ ] Pornainen, 0 courses (*2) - [ ] Psychology and pedagogy, 0 courses (*5) - [ ] Psychology, 2 courses (*7) ``` - **1:** Mäntsälä (`department:1`) is included in the response, as the psychology course matches this department. Course count is 1 as there is one psychology course. - **2:** Pornainen (`department:2`) is *not included* in the response `subject:1/1/1`-courses don\\'t match Pornainen. - **3:** Mäntsälä\\'s Psychology and pedagogy (`category:1/1`) is included in the response with course count 1. - **4:** Mäntsälä\\'s Languages (`category:1/2`) is *not included* in the response. - **5:** Pornainen\\'s Psychology and pedagogy (`category:2/1`) is *not included* in the response. - **6:** Mäntsälä-Languages-French and (`subject:1/2/1`) and German (`subject:1/2/2`) *are included* in the response, as the subject filtering doesn\\'t apply to other subjects. This point can be counter-intuitive when used in tree structure. - **7:** Pornainen-Psychology and pedagogy-Psychology (`subject:2/1/1`) also *is included* in the response. Depending on which kind of user interface is being built, this tree probably cannot be built only from a filtered response, but rather the filtered and unfiltered response have to be combined. For this kind of depth 3 catalog, it would probably be the simplest to add only catalog items with type subject to search query (e.g. selecting Mäntsälä selects all subjects under it and doesn\\'t add the department or its categories). Then also the course counts for deparments and categories could be calculated from response subjects. And the departments and categores would have to be saved from the unfiltered response as the filtered responses don\\'t have the unmatching ones included.\n */\n async getCatalogRaw(requestParameters: GetCatalogRequest): Promise> {\n const queryParameters: any = {};\n\n if (requestParameters.q !== undefined) {\n queryParameters['q'] = requestParameters.q;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/catalog`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCatalogFromJSON(jsonValue));\n }\n\n /**\n * List of catalog items (e.g. subject) that can be used for filtering courses. Different items are grouped together in the response. ```markup Subject - [ ] Philosophy, 3 courses - [ ] French, 3 courses - [ ] German, 1 course Location - [ ] Ahjola, 2 courses - [ ] Sampola, 4 courses Weekday (*1) - [ ] Monday, 1 course - [ ] Tuesday, 2 courses - [ ] Thursday, 1 course - [ ] Sunday, 3 courses ``` - **1:** A course can have multiple catalog items of some catalog item categories, such as weekdays (e.g. a single course can be held on Mondays and Tuesdays). ### Catalog filtering The response is filtered according to given search query: course count matches the query, and catalog items that don\\'t have any matching courses are omitted. There is one exception to the rule above: if a subject is selected for example, the response subject includes *all* subjects and not just the selected ones. This allows to create the following kind of catalog structure (philosophy is selected, query something like `q=subject:5`): ```markup Subject - [x] Philosophy, 3 courses - [ ] French, 3 courses (*1) - [ ] German, 1 course (*1) Location - [ ] Ahjola, 1 course - [ ] Sampola, 2 courses Weekday - [ ] Tuesday, 2 courses - [ ] Sunday, 1 courses ``` - **1:** Here French and German are included in the results, even though the three courses that match philosophy don\\'t match the languages. If multiple items from different categories are selected, result will be filtered so that for a single catalog item group, all other filters except its own apply to that group (`q:subject:5+location:2`): ```markup Subject - [x] Philosophy, 2 courses (*1) Location - [ ] Ahjola, 1 course (*2) - [x] Sampola, 2 courses Weekday - [ ] Tuesday, 1 course - [ ] Sunday, 1 course ``` - **1:** French and German are omitted because all courses in Sampola are philosophy, and philosophy has only two courses that are in Sampola. - **2:** Ahjola is included because there would be a third philosophy course. ### Tree structure in catalog All the catalog item groups are arrays of depth one, but HellewiCatalogItem objects contain parent information that can be used to create a tree structure of those catalog items. ```markup Department, Category, Subject - [ ] Mäntsälä, 4 courses - [ ] Psychology and pedagogy, 1 course - [ ] Psychology, 1 course - [ ] Languages, 4 courses - [ ] French, 3 courses - [ ] German, 1 course - [ ] Pornainen, 2 courses - [ ] Psychology and pedagogy, 2 courses - [ ] Psychology, 2 courses ``` This can be created so that for each item in departments, search for all items in categories that have that department\\'s keyword as parent. And continue with each of those categories matching parents from subjects. Following parent-child -chains are possible: - Department - Category - Subject - Category - Subject - Term - Period - Classification - Education sector Term - Period and Classification - Education sector are simple so that the relationship is always 1:n, i.e. a period will always have a term as parent, and there cannot be duplicate periods with different parents. Department - Category - Subject is complex. Relationships are n:m, i.e. a category can have different departments as parent (for example language courses are taught both in Mäntsälä and Pornainen). Furthermore, the response includes information so that depth 1 (subject-only), 2 (category-subject) and 3 (department-category-subject) catalog trees can be built. The keywords are also different for these different depth options. For example subject for: - depth 1 will have one id: `subject:34`, - depth 2 will have two ids: `subject:7/34`, this also implies parent `category:7` - depth 3 will have three: `subject:2/7/34`, this also implies parent `category:2/7`, and furthermore category\\'s parent `department:2` (but this is not included in the subject catalog item, only in the parent category) Categories are also different for depth 2 and 3. The simple algorithm for building these is to start from the top and select those items that have parent: null, and then continue with the children that have matching parents. ### Tree structure and filtering The rule that filtering applies to all other catalog item groups except the selected one has some implications when building a tree structured catalog. For example, selecting Mäntsälä - Psycholog and pedagogy - Psychology, `q=subject:1/1/1`: ```markup Department, Category, Subject - [ ] Mäntsälä, 1 course (*1) - [ ] Psychology and pedagogy, 1 course (*3) - [x] Psychology, 1 course - [ ] Languages, 0 courses (*4) - [ ] French, 3 courses (*6) - [ ] German, 1 courses - [ ] Pornainen, 0 courses (*2) - [ ] Psychology and pedagogy, 0 courses (*5) - [ ] Psychology, 2 courses (*7) ``` - **1:** Mäntsälä (`department:1`) is included in the response, as the psychology course matches this department. Course count is 1 as there is one psychology course. - **2:** Pornainen (`department:2`) is *not included* in the response `subject:1/1/1`-courses don\\'t match Pornainen. - **3:** Mäntsälä\\'s Psychology and pedagogy (`category:1/1`) is included in the response with course count 1. - **4:** Mäntsälä\\'s Languages (`category:1/2`) is *not included* in the response. - **5:** Pornainen\\'s Psychology and pedagogy (`category:2/1`) is *not included* in the response. - **6:** Mäntsälä-Languages-French and (`subject:1/2/1`) and German (`subject:1/2/2`) *are included* in the response, as the subject filtering doesn\\'t apply to other subjects. This point can be counter-intuitive when used in tree structure. - **7:** Pornainen-Psychology and pedagogy-Psychology (`subject:2/1/1`) also *is included* in the response. Depending on which kind of user interface is being built, this tree probably cannot be built only from a filtered response, but rather the filtered and unfiltered response have to be combined. For this kind of depth 3 catalog, it would probably be the simplest to add only catalog items with type subject to search query (e.g. selecting Mäntsälä selects all subjects under it and doesn\\'t add the department or its categories). Then also the course counts for deparments and categories could be calculated from response subjects. And the departments and categores would have to be saved from the unfiltered response as the filtered responses don\\'t have the unmatching ones included.\n */\n async getCatalog(requestParameters: GetCatalogRequest): Promise {\n const response = await this.getCatalogRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Catalog settings\n */\n async getCatalogSettingsRaw(): Promise> {\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/catalog/settings`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCatalogSettingsFromJSON(jsonValue));\n }\n\n /**\n * Catalog settings\n */\n async getCatalogSettings(): Promise {\n const response = await this.getCatalogSettingsRaw();\n return await response.value();\n }\n\n}\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport * as runtime from '../runtime';\nimport {\n CourseSortfield,\n CourseSortfieldFromJSON,\n CourseSortfieldToJSON,\n ErrorResponse,\n ErrorResponseFromJSON,\n ErrorResponseToJSON,\n HellewiCourse,\n HellewiCourseFromJSON,\n HellewiCourseToJSON,\n HellewiCourseCount,\n HellewiCourseCountFromJSON,\n HellewiCourseCountToJSON,\n HellewiCoursePartial,\n HellewiCoursePartialFromJSON,\n HellewiCoursePartialToJSON,\n HellewiParticipantCount,\n HellewiParticipantCountFromJSON,\n HellewiParticipantCountToJSON,\n Sortdir,\n SortdirFromJSON,\n SortdirToJSON,\n} from '../models';\n\nexport interface GetCourseRequest {\n id: string;\n unlistedid?: string;\n reqid?: string;\n expiry?: Date;\n hmac?: string;\n preview?: boolean;\n}\n\nexport interface GetCourseCountRequest {\n q?: string;\n}\n\nexport interface ListCourseParticipantCountsRequest {\n ids?: Array;\n}\n\nexport interface ListCoursesRequest {\n q?: string;\n page?: number;\n limit?: number;\n sort?: Array;\n sortdir?: Sortdir;\n}\n\n/**\n * CourseApi - interface\n * \n * @export\n * @interface CourseApiInterface\n */\nexport interface CourseApiInterface {\n /**\n * Course information\n * @param {string} id \n * @param {string} [unlistedid] Course ID, if it is unlisted, i.e. not public. This must be the same number as course ID in path parameters. Request must be signed with HMAC if this is present.\n * @param {string} [reqid] \n * @param {Date} [expiry] \n * @param {string} [hmac] \n * @param {boolean} [preview] \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CourseApiInterface\n */\n getCourseRaw(requestParameters: GetCourseRequest): Promise>;\n\n /**\n * Course information\n */\n getCourse(requestParameters: GetCourseRequest): Promise;\n\n /**\n * Course count Response includes also `x-total-count` header, which is explained in [Pagination](#section/Search/Pagination) (except that from this endpoint there is no 10000 limit).\n * @param {string} [q] Search query, see instructions in [Search](#section/Search).\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CourseApiInterface\n */\n getCourseCountRaw(requestParameters: GetCourseCountRequest): Promise>;\n\n /**\n * Course count Response includes also `x-total-count` header, which is explained in [Pagination](#section/Search/Pagination) (except that from this endpoint there is no 10000 limit).\n */\n getCourseCount(requestParameters: GetCourseCountRequest): Promise;\n\n /**\n * List participant counts in courses Optional fields in response will always be there if global parameter registrationsettings.showseatcount is set to true, or if course parameter showplacecount is set to true Note that the maximum length of an URL (recommendation is 2000 characters) can become a limitation here if there are 100 course ids in multi-tenant form. In this case, use multiple requests for fetching the participant counts.\n * @param {Array} [ids] Course ids\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CourseApiInterface\n */\n listCourseParticipantCountsRaw(requestParameters: ListCourseParticipantCountsRequest): Promise>>;\n\n /**\n * List participant counts in courses Optional fields in response will always be there if global parameter registrationsettings.showseatcount is set to true, or if course parameter showplacecount is set to true Note that the maximum length of an URL (recommendation is 2000 characters) can become a limitation here if there are 100 course ids in multi-tenant form. In this case, use multiple requests for fetching the participant counts.\n */\n listCourseParticipantCounts(requestParameters: ListCourseParticipantCountsRequest): Promise>;\n\n /**\n * Course listing Response includes also `link` and `x-total-count` headers, which are explained in [Pagination](#section/Search/Pagination).\n * @param {string} [q] Search query, see instructions in [Search](#section/Search).\n * @param {number} [page] Pagination: wanted page starting from 1\n * @param {number} [limit] Pagination: how many items there are per page (maximum 100)\n * @param {Array} [sort] Result sorting: sort according to these fields. If sort is not given but there is a keyword-search or distancesoft filter, sort best matching first. Otherwise sort ascending by `code`.\n * @param {Sortdir} [sortdir] Result sorting: sort direction, ascending or descending,\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof CourseApiInterface\n */\n listCoursesRaw(requestParameters: ListCoursesRequest): Promise>>;\n\n /**\n * Course listing Response includes also `link` and `x-total-count` headers, which are explained in [Pagination](#section/Search/Pagination).\n */\n listCourses(requestParameters: ListCoursesRequest): Promise>;\n\n}\n\n/**\n * \n */\nexport class CourseApi extends runtime.BaseAPI implements CourseApiInterface {\n\n /**\n * Course information\n */\n async getCourseRaw(requestParameters: GetCourseRequest): Promise> {\n if (requestParameters.id === null || requestParameters.id === undefined) {\n throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling getCourse.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.unlistedid !== undefined) {\n queryParameters['unlistedid'] = requestParameters.unlistedid;\n }\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n if (requestParameters.preview !== undefined) {\n queryParameters['preview'] = requestParameters.preview;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/courses/{id}`.replace(`{${\"id\"}}`, encodeURIComponent(String(requestParameters.id))),\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCourseFromJSON(jsonValue));\n }\n\n /**\n * Course information\n */\n async getCourse(requestParameters: GetCourseRequest): Promise {\n const response = await this.getCourseRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Course count Response includes also `x-total-count` header, which is explained in [Pagination](#section/Search/Pagination) (except that from this endpoint there is no 10000 limit).\n */\n async getCourseCountRaw(requestParameters: GetCourseCountRequest): Promise> {\n const queryParameters: any = {};\n\n if (requestParameters.q !== undefined) {\n queryParameters['q'] = requestParameters.q;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/course-count`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCourseCountFromJSON(jsonValue));\n }\n\n /**\n * Course count Response includes also `x-total-count` header, which is explained in [Pagination](#section/Search/Pagination) (except that from this endpoint there is no 10000 limit).\n */\n async getCourseCount(requestParameters: GetCourseCountRequest): Promise {\n const response = await this.getCourseCountRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * List participant counts in courses Optional fields in response will always be there if global parameter registrationsettings.showseatcount is set to true, or if course parameter showplacecount is set to true Note that the maximum length of an URL (recommendation is 2000 characters) can become a limitation here if there are 100 course ids in multi-tenant form. In this case, use multiple requests for fetching the participant counts.\n */\n async listCourseParticipantCountsRaw(requestParameters: ListCourseParticipantCountsRequest): Promise>> {\n const queryParameters: any = {};\n\n if (requestParameters.ids) {\n queryParameters['ids'] = requestParameters.ids;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/course-participants`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(HellewiParticipantCountFromJSON));\n }\n\n /**\n * List participant counts in courses Optional fields in response will always be there if global parameter registrationsettings.showseatcount is set to true, or if course parameter showplacecount is set to true Note that the maximum length of an URL (recommendation is 2000 characters) can become a limitation here if there are 100 course ids in multi-tenant form. In this case, use multiple requests for fetching the participant counts.\n */\n async listCourseParticipantCounts(requestParameters: ListCourseParticipantCountsRequest): Promise> {\n const response = await this.listCourseParticipantCountsRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Course listing Response includes also `link` and `x-total-count` headers, which are explained in [Pagination](#section/Search/Pagination).\n */\n async listCoursesRaw(requestParameters: ListCoursesRequest): Promise>> {\n const queryParameters: any = {};\n\n if (requestParameters.q !== undefined) {\n queryParameters['q'] = requestParameters.q;\n }\n\n if (requestParameters.page !== undefined) {\n queryParameters['page'] = requestParameters.page;\n }\n\n if (requestParameters.limit !== undefined) {\n queryParameters['limit'] = requestParameters.limit;\n }\n\n if (requestParameters.sort) {\n queryParameters['sort'] = requestParameters.sort;\n }\n\n if (requestParameters.sortdir !== undefined) {\n queryParameters['sortdir'] = requestParameters.sortdir;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/courses`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(HellewiCoursePartialFromJSON));\n }\n\n /**\n * Course listing Response includes also `link` and `x-total-count` headers, which are explained in [Pagination](#section/Search/Pagination).\n */\n async listCourses(requestParameters: ListCoursesRequest): Promise> {\n const response = await this.listCoursesRaw(requestParameters);\n return await response.value();\n }\n\n}\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport * as runtime from '../runtime';\nimport {\n ErrorResponse,\n ErrorResponseFromJSON,\n ErrorResponseToJSON,\n GetMessagesResponse,\n GetMessagesResponseFromJSON,\n GetMessagesResponseToJSON,\n HandleClientLessonResponse,\n HandleClientLessonResponseFromJSON,\n HandleClientLessonResponseToJSON,\n HellewiCourse,\n HellewiCourseFromJSON,\n HellewiCourseToJSON,\n HellewiCourseClientDetails,\n HellewiCourseClientDetailsFromJSON,\n HellewiCourseClientDetailsToJSON,\n HellewiUser,\n HellewiUserFromJSON,\n HellewiUserToJSON,\n} from '../models';\n\nexport interface GetCourseDetailsRequest {\n id: string;\n}\n\nexport interface GetMessagesRequest {\n page: number;\n limit?: number;\n}\n\nexport interface HandleClientLessonRequest {\n requestBody: { [key: string]: object; };\n}\n\nexport interface PostLoginRequest {\n requestBody: { [key: string]: object; };\n}\n\nexport interface UpdateCoursePropertiesRequest {\n id: string;\n requestBody: { [key: string]: object; };\n}\n\n/**\n * MobileApi - interface\n * \n * @export\n * @interface MobileApiInterface\n */\nexport interface MobileApiInterface {\n /**\n * Get the details of a single course, taught by the requesting teacher (id in JWT).\n * @param {string} id \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof MobileApiInterface\n */\n getCourseDetailsRaw(requestParameters: GetCourseDetailsRequest): Promise>;\n\n /**\n * Get the details of a single course, taught by the requesting teacher (id in JWT).\n */\n getCourseDetails(requestParameters: GetCourseDetailsRequest): Promise;\n\n /**\n * List of courses taught by the requesting teacher (id in JWT).\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof MobileApiInterface\n */\n getCoursesByTeacherRaw(): Promise>>;\n\n /**\n * List of courses taught by the requesting teacher (id in JWT).\n */\n getCoursesByTeacher(): Promise>;\n\n /**\n * Get messages from messageboard\n * @param {number} page Page number (starting from 0)\n * @param {number} [limit] Amount of messages per page, set to 15 by default\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof MobileApiInterface\n */\n getMessagesRaw(requestParameters: GetMessagesRequest): Promise>;\n\n /**\n * Get messages from messageboard\n */\n getMessages(requestParameters: GetMessagesRequest): Promise;\n\n /**\n * Handle changes to clientlessons\n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof MobileApiInterface\n */\n handleClientLessonRaw(requestParameters: HandleClientLessonRequest): Promise>;\n\n /**\n * Handle changes to clientlessons\n */\n handleClientLesson(requestParameters: HandleClientLessonRequest): Promise;\n\n /**\n * Login endpoint\n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof MobileApiInterface\n */\n postLoginRaw(requestParameters: PostLoginRequest): Promise>;\n\n /**\n * Login endpoint\n */\n postLogin(requestParameters: PostLoginRequest): Promise;\n\n /**\n * Handles updates to certain course/lesson properties. Currently: course summary, lesson topics\n * @param {string} id Course id\n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof MobileApiInterface\n */\n updateCoursePropertiesRaw(requestParameters: UpdateCoursePropertiesRequest): Promise>;\n\n /**\n * Handles updates to certain course/lesson properties. Currently: course summary, lesson topics\n */\n updateCourseProperties(requestParameters: UpdateCoursePropertiesRequest): Promise;\n\n}\n\n/**\n * \n */\nexport class MobileApi extends runtime.BaseAPI implements MobileApiInterface {\n\n /**\n * Get the details of a single course, taught by the requesting teacher (id in JWT).\n */\n async getCourseDetailsRaw(requestParameters: GetCourseDetailsRequest): Promise> {\n if (requestParameters.id === null || requestParameters.id === undefined) {\n throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling getCourseDetails.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/courses-by-teacher/{id}`.replace(`{${\"id\"}}`, encodeURIComponent(String(requestParameters.id))),\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiCourseClientDetailsFromJSON(jsonValue));\n }\n\n /**\n * Get the details of a single course, taught by the requesting teacher (id in JWT).\n */\n async getCourseDetails(requestParameters: GetCourseDetailsRequest): Promise {\n const response = await this.getCourseDetailsRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * List of courses taught by the requesting teacher (id in JWT).\n */\n async getCoursesByTeacherRaw(): Promise>> {\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/courses-by-teacher`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(HellewiCourseFromJSON));\n }\n\n /**\n * List of courses taught by the requesting teacher (id in JWT).\n */\n async getCoursesByTeacher(): Promise> {\n const response = await this.getCoursesByTeacherRaw();\n return await response.value();\n }\n\n /**\n * Get messages from messageboard\n */\n async getMessagesRaw(requestParameters: GetMessagesRequest): Promise> {\n if (requestParameters.page === null || requestParameters.page === undefined) {\n throw new runtime.RequiredError('page','Required parameter requestParameters.page was null or undefined when calling getMessages.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.page !== undefined) {\n queryParameters['page'] = requestParameters.page;\n }\n\n if (requestParameters.limit !== undefined) {\n queryParameters['limit'] = requestParameters.limit;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/messageboard`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => GetMessagesResponseFromJSON(jsonValue));\n }\n\n /**\n * Get messages from messageboard\n */\n async getMessages(requestParameters: GetMessagesRequest): Promise {\n const response = await this.getMessagesRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Handle changes to clientlessons\n */\n async handleClientLessonRaw(requestParameters: HandleClientLessonRequest): Promise> {\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling handleClientLesson.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/clientlesson`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HandleClientLessonResponseFromJSON(jsonValue));\n }\n\n /**\n * Handle changes to clientlessons\n */\n async handleClientLesson(requestParameters: HandleClientLessonRequest): Promise {\n const response = await this.handleClientLessonRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Login endpoint\n */\n async postLoginRaw(requestParameters: PostLoginRequest): Promise> {\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling postLogin.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/user/login`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiUserFromJSON(jsonValue));\n }\n\n /**\n * Login endpoint\n */\n async postLogin(requestParameters: PostLoginRequest): Promise {\n const response = await this.postLoginRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Handles updates to certain course/lesson properties. Currently: course summary, lesson topics\n */\n async updateCoursePropertiesRaw(requestParameters: UpdateCoursePropertiesRequest): Promise> {\n if (requestParameters.id === null || requestParameters.id === undefined) {\n throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling updateCourseProperties.');\n }\n\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling updateCourseProperties.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/courses-by-teacher/{id}/update`.replace(`{${\"id\"}}`, encodeURIComponent(String(requestParameters.id))),\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.VoidApiResponse(response);\n }\n\n /**\n * Handles updates to certain course/lesson properties. Currently: course summary, lesson topics\n */\n async updateCourseProperties(requestParameters: UpdateCoursePropertiesRequest): Promise {\n await this.updateCoursePropertiesRaw(requestParameters);\n }\n\n}\n","import { SnackbarProgrammatic as Snackbar, ToastProgrammatic as Toast } from 'buefy';\nimport { BNoticeComponent } from 'buefy/types/components';\nimport {\n ComputedRef,\n Ref,\n SetupContext,\n computed,\n onBeforeMount,\n ref,\n watch\n} from '@vue/composition-api';\nimport { Configuration } from '../api';\nimport { translate } from './misc-utils';\n\nexport enum RequestState {\n Uninitialized = 'UNINITIALIZED',\n Initialized = 'INITIALIZED',\n Loading = 'LOADING',\n Success = 'SUCCESS',\n Error = 'ERROR'\n}\n\nexport type Api = () => {\n api: Ref;\n changeConfiguration: (configuration: Configuration) => void;\n};\n\nexport type ApiEndpoint = () => {\n initial: O;\n state: Ref;\n response: Ref;\n execute: (input: I) => Promise;\n status?: Ref;\n errorMessage?: Ref;\n // hasError and isLoading would fit here, but unfortunately watchers' and\n // computed variables' lifecycle is limited to the component in which they\n // are loaded, so their watchers will be stopped when the component is\n // unmounted.\n //\n // So they have to be created for each component separately like this:\n // const { response: course, execute: getCourse, state } = useGetCourse();\n // const isLoading = stateIsLoading(state);\n};\n\nexport type ApiEndPointWithSetter = () => {\n initial: O;\n state: Ref;\n response: Ref;\n execute: (input: I) => void;\n setResponse: (val: O) => void;\n};\n\nexport const stateHasError = (state: Ref): ComputedRef =>\n computed(() => state.value === RequestState.Error);\n\nexport const stateIsLoading = (state: Ref): ComputedRef =>\n computed(() => state.value === RequestState.Loading);\n\nexport const stateIsSuccess = (state: Ref): ComputedRef =>\n computed(() => state.value === RequestState.Success);\n\nexport const ApiEndpointInitialization = (\n api: Ref,\n state: Ref,\n response: Ref,\n initial: O\n): void => {\n const initialize = () => {\n if (api.value) {\n state.value = RequestState.Initialized;\n } else {\n state.value = RequestState.Uninitialized;\n }\n response.value = initial;\n };\n\n onBeforeMount(() => {\n if (state.value === RequestState.Uninitialized) {\n initialize();\n }\n });\n watch(api, initialize); // if API is changed, reset everything\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const useToast = (\n ctx: SetupContext\n): {\n warnToast: (i18nKey: string, translateMessage?: boolean, indefinite?: boolean) => void;\n clearErrorToasts: () => void;\n successToast: (i18nKey: string) => void;\n} => {\n const warnComponents = ref([]);\n const warnToast = (i18nKey: string, translateMessage = true, indefinite = true) =>\n warnComponents.value.push(\n Snackbar.open({\n message: translateMessage ? translate(ctx, i18nKey) : i18nKey,\n duration: 5000,\n type: 'is-warning',\n position: 'is-bottom',\n actionText: 'OK',\n indefinite,\n queue: true\n })\n );\n\n const clearErrorToasts = () => {\n for (const component of warnComponents.value) {\n component.close();\n }\n warnComponents.value = [];\n };\n\n const successToast = (i18nKey: string) =>\n Toast.open({\n message: translate(ctx, i18nKey),\n duration: 5000,\n type: 'is-success',\n position: 'is-bottom'\n });\n\n return { warnToast, clearErrorToasts, successToast };\n};\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport * as runtime from '../runtime';\nimport {\n ErrorResponse,\n ErrorResponseFromJSON,\n ErrorResponseToJSON,\n HellewiMyRegistrationsResponse,\n HellewiMyRegistrationsResponseFromJSON,\n HellewiMyRegistrationsResponseToJSON,\n MethodOfPayment,\n MethodOfPaymentFromJSON,\n MethodOfPaymentToJSON,\n PaymentAmountMismatchError,\n PaymentAmountMismatchErrorFromJSON,\n PaymentAmountMismatchErrorToJSON,\n PaymentHashError,\n PaymentHashErrorFromJSON,\n PaymentHashErrorToJSON,\n PaymentStatusError,\n PaymentStatusErrorFromJSON,\n PaymentStatusErrorToJSON,\n PaymentValidationError,\n PaymentValidationErrorFromJSON,\n PaymentValidationErrorToJSON,\n PaytrailApiAlgorithm,\n PaytrailApiAlgorithmFromJSON,\n PaytrailApiAlgorithmToJSON,\n PaytrailApiReceiptStatus,\n PaytrailApiReceiptStatusFromJSON,\n PaytrailApiReceiptStatusToJSON,\n} from '../models';\n\nexport interface GetCpuOkRequest {\n id: string;\n status: number;\n reference: string;\n hash: string;\n paymentSum?: number;\n paymentMethod?: number;\n timestamp?: string;\n paymentDescription?: string;\n}\n\nexport interface GetEpassiOkRequest {\n amount: number;\n expiry: Date;\n products: Array;\n reqid: string;\n hmac: string;\n type: any;\n sTAMP: string;\n pAID: number;\n mAC: string;\n}\n\nexport interface GetPaymentTokenRequest {\n requestBody: { [key: string]: object; };\n}\n\nexport interface GetPaytrailApiNotifyRequest {\n checkoutAccount: string;\n checkoutAlgorithm: PaytrailApiAlgorithm;\n checkoutAmount: number;\n checkoutStamp: string;\n checkoutReference: string;\n checkoutTransactionId: string;\n checkoutStatus: PaytrailApiReceiptStatus;\n checkoutProvider: string;\n signature: string;\n}\n\nexport interface GetPaytrailApiOkRequest {\n checkoutAccount: string;\n checkoutAlgorithm: PaytrailApiAlgorithm;\n checkoutAmount: number;\n checkoutStamp: string;\n checkoutReference: string;\n checkoutTransactionId: string;\n checkoutStatus: PaytrailApiReceiptStatus;\n checkoutProvider: string;\n signature: string;\n}\n\nexport interface GetSmartumOkRequest {\n amount: number;\n expiry: Date;\n products: Array;\n referenceNumber: string;\n reqid: string;\n hmac: string;\n jwt: string;\n}\n\nexport interface GetTurkuOkRequest {\n checkoutAmount: number;\n checkoutStamp: string;\n checkoutReference: string;\n checkoutTransactionId: string;\n checkoutStatus: PaytrailApiReceiptStatus;\n checkoutProvider: string;\n authorization: string;\n xTURKUSP: string;\n xTURKUTS: string;\n}\n\nexport interface GetVismapayNotifyRequest {\n amount: number;\n expiry: Date;\n products: Array;\n reqid: string;\n hmac: string;\n rETURNCODE: string;\n oRDERNUMBER: string;\n sETTLED: number;\n aUTHCODE: string;\n}\n\nexport interface GetVismapayOkRequest {\n amount: number;\n expiry: Date;\n products: Array;\n reqid: string;\n hmac: string;\n rETURNCODE: string;\n oRDERNUMBER: string;\n sETTLED: number;\n aUTHCODE: string;\n}\n\nexport interface PostCpuNotifyRequest {\n requestBody: { [key: string]: object; };\n}\n\nexport interface PostEpassiNotifyRequest {\n amount: number;\n expiry: Date;\n products: Array;\n reqid: string;\n hmac: string;\n type: any;\n requestBody: { [key: string]: object; };\n}\n\nexport interface PostEpassiOkRequest {\n amount: number;\n expiry: Date;\n products: Array;\n reqid: string;\n hmac: string;\n type: any;\n requestBody: { [key: string]: object; };\n}\n\nexport interface PostTurkuNotifyRequest {\n requestBody: { [key: string]: object; };\n}\n\n/**\n * PaymentApi - interface\n * \n * @export\n * @interface PaymentApiInterface\n */\nexport interface PaymentApiInterface {\n /**\n * \n * @param {string} id \n * @param {number} status \n * @param {string} reference \n * @param {string} hash \n * @param {number} [paymentSum] \n * @param {number} [paymentMethod] \n * @param {string} [timestamp] \n * @param {string} [paymentDescription] \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n getCpuOkRaw(requestParameters: GetCpuOkRequest): Promise>;\n\n /**\n */\n getCpuOk(requestParameters: GetCpuOkRequest): Promise;\n\n /**\n * Epassi OK Epassi sends its OK response as POST, we then redirect this to GET with same parameters Payment handling is done in POST endpoint\n * @param {number} amount amount that has been paid, sent signed by by api\n * @param {Date} expiry hmac expiry date\n * @param {Array} products array of courseids that has been paid, sent and signed by api\n * @param {string} reqid hmac reqid\n * @param {string} hmac hmac signature\n * @param {any} type Benefit type, send and signed by api\n * @param {string} sTAMP Payment\\'s ID that is unique for each transaction\n * @param {number} pAID Security verification ID generated by ePassi\n * @param {string} mAC Epassi SHA-512 hash value\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n getEpassiOkRaw(requestParameters: GetEpassiOkRequest): Promise>;\n\n /**\n * Epassi OK Epassi sends its OK response as POST, we then redirect this to GET with same parameters Payment handling is done in POST endpoint\n */\n getEpassiOk(requestParameters: GetEpassiOkRequest): Promise;\n\n /**\n * Payment token\n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n getPaymentTokenRaw(requestParameters: GetPaymentTokenRequest): Promise>;\n\n /**\n * Payment token\n */\n getPaymentToken(requestParameters: GetPaymentTokenRequest): Promise;\n\n /**\n * \n * @param {string} checkoutAccount \n * @param {PaytrailApiAlgorithm} checkoutAlgorithm \n * @param {number} checkoutAmount \n * @param {string} checkoutStamp \n * @param {string} checkoutReference \n * @param {string} checkoutTransactionId \n * @param {PaytrailApiReceiptStatus} checkoutStatus \n * @param {string} checkoutProvider \n * @param {string} signature \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n getPaytrailApiNotifyRaw(requestParameters: GetPaytrailApiNotifyRequest): Promise>;\n\n /**\n */\n getPaytrailApiNotify(requestParameters: GetPaytrailApiNotifyRequest): Promise;\n\n /**\n * \n * @param {string} checkoutAccount \n * @param {PaytrailApiAlgorithm} checkoutAlgorithm \n * @param {number} checkoutAmount \n * @param {string} checkoutStamp \n * @param {string} checkoutReference \n * @param {string} checkoutTransactionId \n * @param {PaytrailApiReceiptStatus} checkoutStatus \n * @param {string} checkoutProvider \n * @param {string} signature \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n getPaytrailApiOkRaw(requestParameters: GetPaytrailApiOkRequest): Promise>;\n\n /**\n */\n getPaytrailApiOk(requestParameters: GetPaytrailApiOkRequest): Promise;\n\n /**\n * Smartum OK\n * @param {number} amount \n * @param {Date} expiry \n * @param {Array} products \n * @param {string} referenceNumber \n * @param {string} reqid \n * @param {string} hmac \n * @param {string} jwt \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n getSmartumOkRaw(requestParameters: GetSmartumOkRequest): Promise>;\n\n /**\n * Smartum OK\n */\n getSmartumOk(requestParameters: GetSmartumOkRequest): Promise;\n\n /**\n * \n * @param {number} checkoutAmount \n * @param {string} checkoutStamp \n * @param {string} checkoutReference \n * @param {string} checkoutTransactionId \n * @param {PaytrailApiReceiptStatus} checkoutStatus \n * @param {string} checkoutProvider \n * @param {string} authorization \n * @param {string} xTURKUSP \n * @param {string} xTURKUTS \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n getTurkuOkRaw(requestParameters: GetTurkuOkRequest): Promise>;\n\n /**\n */\n getTurkuOk(requestParameters: GetTurkuOkRequest): Promise;\n\n /**\n * Vismapay notify\n * @param {number} amount amount that has been paid, sent signed by by api\n * @param {Date} expiry hmac expiry date\n * @param {Array} products array of courseids that has been paid, sent and signed by api\n * @param {string} reqid hmac reqid\n * @param {string} hmac \n * @param {string} rETURNCODE \n * @param {string} oRDERNUMBER \n * @param {number} sETTLED \n * @param {string} aUTHCODE \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n getVismapayNotifyRaw(requestParameters: GetVismapayNotifyRequest): Promise>;\n\n /**\n * Vismapay notify\n */\n getVismapayNotify(requestParameters: GetVismapayNotifyRequest): Promise;\n\n /**\n * Vismapay OK\n * @param {number} amount amount that has been paid, sent signed by by api\n * @param {Date} expiry hmac expiry date\n * @param {Array} products array of courseids that has been paid, sent and signed by api\n * @param {string} reqid hmac reqid\n * @param {string} hmac \n * @param {string} rETURNCODE \n * @param {string} oRDERNUMBER \n * @param {number} sETTLED \n * @param {string} aUTHCODE \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n getVismapayOkRaw(requestParameters: GetVismapayOkRequest): Promise>;\n\n /**\n * Vismapay OK\n */\n getVismapayOk(requestParameters: GetVismapayOkRequest): Promise;\n\n /**\n * \n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n postCpuNotifyRaw(requestParameters: PostCpuNotifyRequest): Promise>;\n\n /**\n */\n postCpuNotify(requestParameters: PostCpuNotifyRequest): Promise;\n\n /**\n * Epassi notify\n * @param {number} amount amount that has been paid, sent signed by by api\n * @param {Date} expiry hmac expiry date\n * @param {Array} products array of courseids that has been paid, sent and signed by api\n * @param {string} reqid hmac reqid\n * @param {string} hmac hmac signature\n * @param {any} type Benefit type, send and signed by api\n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n postEpassiNotifyRaw(requestParameters: PostEpassiNotifyRequest): Promise>;\n\n /**\n * Epassi notify\n */\n postEpassiNotify(requestParameters: PostEpassiNotifyRequest): Promise;\n\n /**\n * Epassi OK Epassi sends its OK response as POST, we then redirect this to GET with same parameters\n * @param {number} amount amount that has been paid, sent signed by by api\n * @param {Date} expiry hmac expiry date\n * @param {Array} products array of courseids that has been paid, sent and signed by api\n * @param {string} reqid hmac reqid\n * @param {string} hmac hmac signature\n * @param {any} type Benefit type, send and signed by api\n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n postEpassiOkRaw(requestParameters: PostEpassiOkRequest): Promise>;\n\n /**\n * Epassi OK Epassi sends its OK response as POST, we then redirect this to GET with same parameters\n */\n postEpassiOk(requestParameters: PostEpassiOkRequest): Promise;\n\n /**\n * \n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof PaymentApiInterface\n */\n postTurkuNotifyRaw(requestParameters: PostTurkuNotifyRequest): Promise>;\n\n /**\n */\n postTurkuNotify(requestParameters: PostTurkuNotifyRequest): Promise;\n\n}\n\n/**\n * \n */\nexport class PaymentApi extends runtime.BaseAPI implements PaymentApiInterface {\n\n /**\n */\n async getCpuOkRaw(requestParameters: GetCpuOkRequest): Promise> {\n if (requestParameters.id === null || requestParameters.id === undefined) {\n throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling getCpuOk.');\n }\n\n if (requestParameters.status === null || requestParameters.status === undefined) {\n throw new runtime.RequiredError('status','Required parameter requestParameters.status was null or undefined when calling getCpuOk.');\n }\n\n if (requestParameters.reference === null || requestParameters.reference === undefined) {\n throw new runtime.RequiredError('reference','Required parameter requestParameters.reference was null or undefined when calling getCpuOk.');\n }\n\n if (requestParameters.hash === null || requestParameters.hash === undefined) {\n throw new runtime.RequiredError('hash','Required parameter requestParameters.hash was null or undefined when calling getCpuOk.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.id !== undefined) {\n queryParameters['Id'] = requestParameters.id;\n }\n\n if (requestParameters.status !== undefined) {\n queryParameters['Status'] = requestParameters.status;\n }\n\n if (requestParameters.reference !== undefined) {\n queryParameters['Reference'] = requestParameters.reference;\n }\n\n if (requestParameters.hash !== undefined) {\n queryParameters['Hash'] = requestParameters.hash;\n }\n\n if (requestParameters.paymentSum !== undefined) {\n queryParameters['PaymentSum'] = requestParameters.paymentSum;\n }\n\n if (requestParameters.paymentMethod !== undefined) {\n queryParameters['PaymentMethod'] = requestParameters.paymentMethod;\n }\n\n if (requestParameters.timestamp !== undefined) {\n queryParameters['Timestamp'] = requestParameters.timestamp;\n }\n\n if (requestParameters.paymentDescription !== undefined) {\n queryParameters['PaymentDescription'] = requestParameters.paymentDescription;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/cpu/ok`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiMyRegistrationsResponseFromJSON(jsonValue));\n }\n\n /**\n */\n async getCpuOk(requestParameters: GetCpuOkRequest): Promise {\n const response = await this.getCpuOkRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Epassi OK Epassi sends its OK response as POST, we then redirect this to GET with same parameters Payment handling is done in POST endpoint\n */\n async getEpassiOkRaw(requestParameters: GetEpassiOkRequest): Promise> {\n if (requestParameters.amount === null || requestParameters.amount === undefined) {\n throw new runtime.RequiredError('amount','Required parameter requestParameters.amount was null or undefined when calling getEpassiOk.');\n }\n\n if (requestParameters.expiry === null || requestParameters.expiry === undefined) {\n throw new runtime.RequiredError('expiry','Required parameter requestParameters.expiry was null or undefined when calling getEpassiOk.');\n }\n\n if (requestParameters.products === null || requestParameters.products === undefined) {\n throw new runtime.RequiredError('products','Required parameter requestParameters.products was null or undefined when calling getEpassiOk.');\n }\n\n if (requestParameters.reqid === null || requestParameters.reqid === undefined) {\n throw new runtime.RequiredError('reqid','Required parameter requestParameters.reqid was null or undefined when calling getEpassiOk.');\n }\n\n if (requestParameters.hmac === null || requestParameters.hmac === undefined) {\n throw new runtime.RequiredError('hmac','Required parameter requestParameters.hmac was null or undefined when calling getEpassiOk.');\n }\n\n if (requestParameters.type === null || requestParameters.type === undefined) {\n throw new runtime.RequiredError('type','Required parameter requestParameters.type was null or undefined when calling getEpassiOk.');\n }\n\n if (requestParameters.sTAMP === null || requestParameters.sTAMP === undefined) {\n throw new runtime.RequiredError('sTAMP','Required parameter requestParameters.sTAMP was null or undefined when calling getEpassiOk.');\n }\n\n if (requestParameters.pAID === null || requestParameters.pAID === undefined) {\n throw new runtime.RequiredError('pAID','Required parameter requestParameters.pAID was null or undefined when calling getEpassiOk.');\n }\n\n if (requestParameters.mAC === null || requestParameters.mAC === undefined) {\n throw new runtime.RequiredError('mAC','Required parameter requestParameters.mAC was null or undefined when calling getEpassiOk.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.amount !== undefined) {\n queryParameters['amount'] = requestParameters.amount;\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.products) {\n queryParameters['products'] = requestParameters.products;\n }\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n if (requestParameters.type !== undefined) {\n queryParameters['type'] = requestParameters.type;\n }\n\n if (requestParameters.sTAMP !== undefined) {\n queryParameters['STAMP'] = requestParameters.sTAMP;\n }\n\n if (requestParameters.pAID !== undefined) {\n queryParameters['PAID'] = requestParameters.pAID;\n }\n\n if (requestParameters.mAC !== undefined) {\n queryParameters['MAC'] = requestParameters.mAC;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/epassi/ok`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiMyRegistrationsResponseFromJSON(jsonValue));\n }\n\n /**\n * Epassi OK Epassi sends its OK response as POST, we then redirect this to GET with same parameters Payment handling is done in POST endpoint\n */\n async getEpassiOk(requestParameters: GetEpassiOkRequest): Promise {\n const response = await this.getEpassiOkRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Payment token\n */\n async getPaymentTokenRaw(requestParameters: GetPaymentTokenRequest): Promise> {\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling getPaymentToken.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/token`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => MethodOfPaymentFromJSON(jsonValue));\n }\n\n /**\n * Payment token\n */\n async getPaymentToken(requestParameters: GetPaymentTokenRequest): Promise {\n const response = await this.getPaymentTokenRaw(requestParameters);\n return await response.value();\n }\n\n /**\n */\n async getPaytrailApiNotifyRaw(requestParameters: GetPaytrailApiNotifyRequest): Promise> {\n if (requestParameters.checkoutAccount === null || requestParameters.checkoutAccount === undefined) {\n throw new runtime.RequiredError('checkoutAccount','Required parameter requestParameters.checkoutAccount was null or undefined when calling getPaytrailApiNotify.');\n }\n\n if (requestParameters.checkoutAlgorithm === null || requestParameters.checkoutAlgorithm === undefined) {\n throw new runtime.RequiredError('checkoutAlgorithm','Required parameter requestParameters.checkoutAlgorithm was null or undefined when calling getPaytrailApiNotify.');\n }\n\n if (requestParameters.checkoutAmount === null || requestParameters.checkoutAmount === undefined) {\n throw new runtime.RequiredError('checkoutAmount','Required parameter requestParameters.checkoutAmount was null or undefined when calling getPaytrailApiNotify.');\n }\n\n if (requestParameters.checkoutStamp === null || requestParameters.checkoutStamp === undefined) {\n throw new runtime.RequiredError('checkoutStamp','Required parameter requestParameters.checkoutStamp was null or undefined when calling getPaytrailApiNotify.');\n }\n\n if (requestParameters.checkoutReference === null || requestParameters.checkoutReference === undefined) {\n throw new runtime.RequiredError('checkoutReference','Required parameter requestParameters.checkoutReference was null or undefined when calling getPaytrailApiNotify.');\n }\n\n if (requestParameters.checkoutTransactionId === null || requestParameters.checkoutTransactionId === undefined) {\n throw new runtime.RequiredError('checkoutTransactionId','Required parameter requestParameters.checkoutTransactionId was null or undefined when calling getPaytrailApiNotify.');\n }\n\n if (requestParameters.checkoutStatus === null || requestParameters.checkoutStatus === undefined) {\n throw new runtime.RequiredError('checkoutStatus','Required parameter requestParameters.checkoutStatus was null or undefined when calling getPaytrailApiNotify.');\n }\n\n if (requestParameters.checkoutProvider === null || requestParameters.checkoutProvider === undefined) {\n throw new runtime.RequiredError('checkoutProvider','Required parameter requestParameters.checkoutProvider was null or undefined when calling getPaytrailApiNotify.');\n }\n\n if (requestParameters.signature === null || requestParameters.signature === undefined) {\n throw new runtime.RequiredError('signature','Required parameter requestParameters.signature was null or undefined when calling getPaytrailApiNotify.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.checkoutAccount !== undefined) {\n queryParameters['checkout-account'] = requestParameters.checkoutAccount;\n }\n\n if (requestParameters.checkoutAlgorithm !== undefined) {\n queryParameters['checkout-algorithm'] = requestParameters.checkoutAlgorithm;\n }\n\n if (requestParameters.checkoutAmount !== undefined) {\n queryParameters['checkout-amount'] = requestParameters.checkoutAmount;\n }\n\n if (requestParameters.checkoutStamp !== undefined) {\n queryParameters['checkout-stamp'] = requestParameters.checkoutStamp;\n }\n\n if (requestParameters.checkoutReference !== undefined) {\n queryParameters['checkout-reference'] = requestParameters.checkoutReference;\n }\n\n if (requestParameters.checkoutTransactionId !== undefined) {\n queryParameters['checkout-transaction-id'] = requestParameters.checkoutTransactionId;\n }\n\n if (requestParameters.checkoutStatus !== undefined) {\n queryParameters['checkout-status'] = requestParameters.checkoutStatus;\n }\n\n if (requestParameters.checkoutProvider !== undefined) {\n queryParameters['checkout-provider'] = requestParameters.checkoutProvider;\n }\n\n if (requestParameters.signature !== undefined) {\n queryParameters['signature'] = requestParameters.signature;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/paytrailapi/notify`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.VoidApiResponse(response);\n }\n\n /**\n */\n async getPaytrailApiNotify(requestParameters: GetPaytrailApiNotifyRequest): Promise {\n await this.getPaytrailApiNotifyRaw(requestParameters);\n }\n\n /**\n */\n async getPaytrailApiOkRaw(requestParameters: GetPaytrailApiOkRequest): Promise> {\n if (requestParameters.checkoutAccount === null || requestParameters.checkoutAccount === undefined) {\n throw new runtime.RequiredError('checkoutAccount','Required parameter requestParameters.checkoutAccount was null or undefined when calling getPaytrailApiOk.');\n }\n\n if (requestParameters.checkoutAlgorithm === null || requestParameters.checkoutAlgorithm === undefined) {\n throw new runtime.RequiredError('checkoutAlgorithm','Required parameter requestParameters.checkoutAlgorithm was null or undefined when calling getPaytrailApiOk.');\n }\n\n if (requestParameters.checkoutAmount === null || requestParameters.checkoutAmount === undefined) {\n throw new runtime.RequiredError('checkoutAmount','Required parameter requestParameters.checkoutAmount was null or undefined when calling getPaytrailApiOk.');\n }\n\n if (requestParameters.checkoutStamp === null || requestParameters.checkoutStamp === undefined) {\n throw new runtime.RequiredError('checkoutStamp','Required parameter requestParameters.checkoutStamp was null or undefined when calling getPaytrailApiOk.');\n }\n\n if (requestParameters.checkoutReference === null || requestParameters.checkoutReference === undefined) {\n throw new runtime.RequiredError('checkoutReference','Required parameter requestParameters.checkoutReference was null or undefined when calling getPaytrailApiOk.');\n }\n\n if (requestParameters.checkoutTransactionId === null || requestParameters.checkoutTransactionId === undefined) {\n throw new runtime.RequiredError('checkoutTransactionId','Required parameter requestParameters.checkoutTransactionId was null or undefined when calling getPaytrailApiOk.');\n }\n\n if (requestParameters.checkoutStatus === null || requestParameters.checkoutStatus === undefined) {\n throw new runtime.RequiredError('checkoutStatus','Required parameter requestParameters.checkoutStatus was null or undefined when calling getPaytrailApiOk.');\n }\n\n if (requestParameters.checkoutProvider === null || requestParameters.checkoutProvider === undefined) {\n throw new runtime.RequiredError('checkoutProvider','Required parameter requestParameters.checkoutProvider was null or undefined when calling getPaytrailApiOk.');\n }\n\n if (requestParameters.signature === null || requestParameters.signature === undefined) {\n throw new runtime.RequiredError('signature','Required parameter requestParameters.signature was null or undefined when calling getPaytrailApiOk.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.checkoutAccount !== undefined) {\n queryParameters['checkout-account'] = requestParameters.checkoutAccount;\n }\n\n if (requestParameters.checkoutAlgorithm !== undefined) {\n queryParameters['checkout-algorithm'] = requestParameters.checkoutAlgorithm;\n }\n\n if (requestParameters.checkoutAmount !== undefined) {\n queryParameters['checkout-amount'] = requestParameters.checkoutAmount;\n }\n\n if (requestParameters.checkoutStamp !== undefined) {\n queryParameters['checkout-stamp'] = requestParameters.checkoutStamp;\n }\n\n if (requestParameters.checkoutReference !== undefined) {\n queryParameters['checkout-reference'] = requestParameters.checkoutReference;\n }\n\n if (requestParameters.checkoutTransactionId !== undefined) {\n queryParameters['checkout-transaction-id'] = requestParameters.checkoutTransactionId;\n }\n\n if (requestParameters.checkoutStatus !== undefined) {\n queryParameters['checkout-status'] = requestParameters.checkoutStatus;\n }\n\n if (requestParameters.checkoutProvider !== undefined) {\n queryParameters['checkout-provider'] = requestParameters.checkoutProvider;\n }\n\n if (requestParameters.signature !== undefined) {\n queryParameters['signature'] = requestParameters.signature;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/paytrailapi/ok`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiMyRegistrationsResponseFromJSON(jsonValue));\n }\n\n /**\n */\n async getPaytrailApiOk(requestParameters: GetPaytrailApiOkRequest): Promise {\n const response = await this.getPaytrailApiOkRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Smartum OK\n */\n async getSmartumOkRaw(requestParameters: GetSmartumOkRequest): Promise> {\n if (requestParameters.amount === null || requestParameters.amount === undefined) {\n throw new runtime.RequiredError('amount','Required parameter requestParameters.amount was null or undefined when calling getSmartumOk.');\n }\n\n if (requestParameters.expiry === null || requestParameters.expiry === undefined) {\n throw new runtime.RequiredError('expiry','Required parameter requestParameters.expiry was null or undefined when calling getSmartumOk.');\n }\n\n if (requestParameters.products === null || requestParameters.products === undefined) {\n throw new runtime.RequiredError('products','Required parameter requestParameters.products was null or undefined when calling getSmartumOk.');\n }\n\n if (requestParameters.referenceNumber === null || requestParameters.referenceNumber === undefined) {\n throw new runtime.RequiredError('referenceNumber','Required parameter requestParameters.referenceNumber was null or undefined when calling getSmartumOk.');\n }\n\n if (requestParameters.reqid === null || requestParameters.reqid === undefined) {\n throw new runtime.RequiredError('reqid','Required parameter requestParameters.reqid was null or undefined when calling getSmartumOk.');\n }\n\n if (requestParameters.hmac === null || requestParameters.hmac === undefined) {\n throw new runtime.RequiredError('hmac','Required parameter requestParameters.hmac was null or undefined when calling getSmartumOk.');\n }\n\n if (requestParameters.jwt === null || requestParameters.jwt === undefined) {\n throw new runtime.RequiredError('jwt','Required parameter requestParameters.jwt was null or undefined when calling getSmartumOk.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.amount !== undefined) {\n queryParameters['amount'] = requestParameters.amount;\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.products) {\n queryParameters['products'] = requestParameters.products;\n }\n\n if (requestParameters.referenceNumber !== undefined) {\n queryParameters['referenceNumber'] = requestParameters.referenceNumber;\n }\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n if (requestParameters.jwt !== undefined) {\n queryParameters['jwt'] = requestParameters.jwt;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/smartum/ok`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiMyRegistrationsResponseFromJSON(jsonValue));\n }\n\n /**\n * Smartum OK\n */\n async getSmartumOk(requestParameters: GetSmartumOkRequest): Promise {\n const response = await this.getSmartumOkRaw(requestParameters);\n return await response.value();\n }\n\n /**\n */\n async getTurkuOkRaw(requestParameters: GetTurkuOkRequest): Promise> {\n if (requestParameters.checkoutAmount === null || requestParameters.checkoutAmount === undefined) {\n throw new runtime.RequiredError('checkoutAmount','Required parameter requestParameters.checkoutAmount was null or undefined when calling getTurkuOk.');\n }\n\n if (requestParameters.checkoutStamp === null || requestParameters.checkoutStamp === undefined) {\n throw new runtime.RequiredError('checkoutStamp','Required parameter requestParameters.checkoutStamp was null or undefined when calling getTurkuOk.');\n }\n\n if (requestParameters.checkoutReference === null || requestParameters.checkoutReference === undefined) {\n throw new runtime.RequiredError('checkoutReference','Required parameter requestParameters.checkoutReference was null or undefined when calling getTurkuOk.');\n }\n\n if (requestParameters.checkoutTransactionId === null || requestParameters.checkoutTransactionId === undefined) {\n throw new runtime.RequiredError('checkoutTransactionId','Required parameter requestParameters.checkoutTransactionId was null or undefined when calling getTurkuOk.');\n }\n\n if (requestParameters.checkoutStatus === null || requestParameters.checkoutStatus === undefined) {\n throw new runtime.RequiredError('checkoutStatus','Required parameter requestParameters.checkoutStatus was null or undefined when calling getTurkuOk.');\n }\n\n if (requestParameters.checkoutProvider === null || requestParameters.checkoutProvider === undefined) {\n throw new runtime.RequiredError('checkoutProvider','Required parameter requestParameters.checkoutProvider was null or undefined when calling getTurkuOk.');\n }\n\n if (requestParameters.authorization === null || requestParameters.authorization === undefined) {\n throw new runtime.RequiredError('authorization','Required parameter requestParameters.authorization was null or undefined when calling getTurkuOk.');\n }\n\n if (requestParameters.xTURKUSP === null || requestParameters.xTURKUSP === undefined) {\n throw new runtime.RequiredError('xTURKUSP','Required parameter requestParameters.xTURKUSP was null or undefined when calling getTurkuOk.');\n }\n\n if (requestParameters.xTURKUTS === null || requestParameters.xTURKUTS === undefined) {\n throw new runtime.RequiredError('xTURKUTS','Required parameter requestParameters.xTURKUTS was null or undefined when calling getTurkuOk.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.checkoutAmount !== undefined) {\n queryParameters['checkout-amount'] = requestParameters.checkoutAmount;\n }\n\n if (requestParameters.checkoutStamp !== undefined) {\n queryParameters['checkout-stamp'] = requestParameters.checkoutStamp;\n }\n\n if (requestParameters.checkoutReference !== undefined) {\n queryParameters['checkout-reference'] = requestParameters.checkoutReference;\n }\n\n if (requestParameters.checkoutTransactionId !== undefined) {\n queryParameters['checkout-transaction-id'] = requestParameters.checkoutTransactionId;\n }\n\n if (requestParameters.checkoutStatus !== undefined) {\n queryParameters['checkout-status'] = requestParameters.checkoutStatus;\n }\n\n if (requestParameters.checkoutProvider !== undefined) {\n queryParameters['checkout-provider'] = requestParameters.checkoutProvider;\n }\n\n if (requestParameters.authorization !== undefined) {\n queryParameters['Authorization'] = requestParameters.authorization;\n }\n\n if (requestParameters.xTURKUSP !== undefined) {\n queryParameters['X-TURKU-SP'] = requestParameters.xTURKUSP;\n }\n\n if (requestParameters.xTURKUTS !== undefined) {\n queryParameters['X-TURKU-TS'] = requestParameters.xTURKUTS;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/turku/ok`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiMyRegistrationsResponseFromJSON(jsonValue));\n }\n\n /**\n */\n async getTurkuOk(requestParameters: GetTurkuOkRequest): Promise {\n const response = await this.getTurkuOkRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Vismapay notify\n */\n async getVismapayNotifyRaw(requestParameters: GetVismapayNotifyRequest): Promise> {\n if (requestParameters.amount === null || requestParameters.amount === undefined) {\n throw new runtime.RequiredError('amount','Required parameter requestParameters.amount was null or undefined when calling getVismapayNotify.');\n }\n\n if (requestParameters.expiry === null || requestParameters.expiry === undefined) {\n throw new runtime.RequiredError('expiry','Required parameter requestParameters.expiry was null or undefined when calling getVismapayNotify.');\n }\n\n if (requestParameters.products === null || requestParameters.products === undefined) {\n throw new runtime.RequiredError('products','Required parameter requestParameters.products was null or undefined when calling getVismapayNotify.');\n }\n\n if (requestParameters.reqid === null || requestParameters.reqid === undefined) {\n throw new runtime.RequiredError('reqid','Required parameter requestParameters.reqid was null or undefined when calling getVismapayNotify.');\n }\n\n if (requestParameters.hmac === null || requestParameters.hmac === undefined) {\n throw new runtime.RequiredError('hmac','Required parameter requestParameters.hmac was null or undefined when calling getVismapayNotify.');\n }\n\n if (requestParameters.rETURNCODE === null || requestParameters.rETURNCODE === undefined) {\n throw new runtime.RequiredError('rETURNCODE','Required parameter requestParameters.rETURNCODE was null or undefined when calling getVismapayNotify.');\n }\n\n if (requestParameters.oRDERNUMBER === null || requestParameters.oRDERNUMBER === undefined) {\n throw new runtime.RequiredError('oRDERNUMBER','Required parameter requestParameters.oRDERNUMBER was null or undefined when calling getVismapayNotify.');\n }\n\n if (requestParameters.sETTLED === null || requestParameters.sETTLED === undefined) {\n throw new runtime.RequiredError('sETTLED','Required parameter requestParameters.sETTLED was null or undefined when calling getVismapayNotify.');\n }\n\n if (requestParameters.aUTHCODE === null || requestParameters.aUTHCODE === undefined) {\n throw new runtime.RequiredError('aUTHCODE','Required parameter requestParameters.aUTHCODE was null or undefined when calling getVismapayNotify.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.amount !== undefined) {\n queryParameters['amount'] = requestParameters.amount;\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.products) {\n queryParameters['products'] = requestParameters.products;\n }\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n if (requestParameters.rETURNCODE !== undefined) {\n queryParameters['RETURN_CODE'] = requestParameters.rETURNCODE;\n }\n\n if (requestParameters.oRDERNUMBER !== undefined) {\n queryParameters['ORDER_NUMBER'] = requestParameters.oRDERNUMBER;\n }\n\n if (requestParameters.sETTLED !== undefined) {\n queryParameters['SETTLED'] = requestParameters.sETTLED;\n }\n\n if (requestParameters.aUTHCODE !== undefined) {\n queryParameters['AUTHCODE'] = requestParameters.aUTHCODE;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/vismapay/notify`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.VoidApiResponse(response);\n }\n\n /**\n * Vismapay notify\n */\n async getVismapayNotify(requestParameters: GetVismapayNotifyRequest): Promise {\n await this.getVismapayNotifyRaw(requestParameters);\n }\n\n /**\n * Vismapay OK\n */\n async getVismapayOkRaw(requestParameters: GetVismapayOkRequest): Promise> {\n if (requestParameters.amount === null || requestParameters.amount === undefined) {\n throw new runtime.RequiredError('amount','Required parameter requestParameters.amount was null or undefined when calling getVismapayOk.');\n }\n\n if (requestParameters.expiry === null || requestParameters.expiry === undefined) {\n throw new runtime.RequiredError('expiry','Required parameter requestParameters.expiry was null or undefined when calling getVismapayOk.');\n }\n\n if (requestParameters.products === null || requestParameters.products === undefined) {\n throw new runtime.RequiredError('products','Required parameter requestParameters.products was null or undefined when calling getVismapayOk.');\n }\n\n if (requestParameters.reqid === null || requestParameters.reqid === undefined) {\n throw new runtime.RequiredError('reqid','Required parameter requestParameters.reqid was null or undefined when calling getVismapayOk.');\n }\n\n if (requestParameters.hmac === null || requestParameters.hmac === undefined) {\n throw new runtime.RequiredError('hmac','Required parameter requestParameters.hmac was null or undefined when calling getVismapayOk.');\n }\n\n if (requestParameters.rETURNCODE === null || requestParameters.rETURNCODE === undefined) {\n throw new runtime.RequiredError('rETURNCODE','Required parameter requestParameters.rETURNCODE was null or undefined when calling getVismapayOk.');\n }\n\n if (requestParameters.oRDERNUMBER === null || requestParameters.oRDERNUMBER === undefined) {\n throw new runtime.RequiredError('oRDERNUMBER','Required parameter requestParameters.oRDERNUMBER was null or undefined when calling getVismapayOk.');\n }\n\n if (requestParameters.sETTLED === null || requestParameters.sETTLED === undefined) {\n throw new runtime.RequiredError('sETTLED','Required parameter requestParameters.sETTLED was null or undefined when calling getVismapayOk.');\n }\n\n if (requestParameters.aUTHCODE === null || requestParameters.aUTHCODE === undefined) {\n throw new runtime.RequiredError('aUTHCODE','Required parameter requestParameters.aUTHCODE was null or undefined when calling getVismapayOk.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.amount !== undefined) {\n queryParameters['amount'] = requestParameters.amount;\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.products) {\n queryParameters['products'] = requestParameters.products;\n }\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n if (requestParameters.rETURNCODE !== undefined) {\n queryParameters['RETURN_CODE'] = requestParameters.rETURNCODE;\n }\n\n if (requestParameters.oRDERNUMBER !== undefined) {\n queryParameters['ORDER_NUMBER'] = requestParameters.oRDERNUMBER;\n }\n\n if (requestParameters.sETTLED !== undefined) {\n queryParameters['SETTLED'] = requestParameters.sETTLED;\n }\n\n if (requestParameters.aUTHCODE !== undefined) {\n queryParameters['AUTHCODE'] = requestParameters.aUTHCODE;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/vismapay/ok`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiMyRegistrationsResponseFromJSON(jsonValue));\n }\n\n /**\n * Vismapay OK\n */\n async getVismapayOk(requestParameters: GetVismapayOkRequest): Promise {\n const response = await this.getVismapayOkRaw(requestParameters);\n return await response.value();\n }\n\n /**\n */\n async postCpuNotifyRaw(requestParameters: PostCpuNotifyRequest): Promise> {\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling postCpuNotify.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/cpu/notify`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.VoidApiResponse(response);\n }\n\n /**\n */\n async postCpuNotify(requestParameters: PostCpuNotifyRequest): Promise {\n await this.postCpuNotifyRaw(requestParameters);\n }\n\n /**\n * Epassi notify\n */\n async postEpassiNotifyRaw(requestParameters: PostEpassiNotifyRequest): Promise> {\n if (requestParameters.amount === null || requestParameters.amount === undefined) {\n throw new runtime.RequiredError('amount','Required parameter requestParameters.amount was null or undefined when calling postEpassiNotify.');\n }\n\n if (requestParameters.expiry === null || requestParameters.expiry === undefined) {\n throw new runtime.RequiredError('expiry','Required parameter requestParameters.expiry was null or undefined when calling postEpassiNotify.');\n }\n\n if (requestParameters.products === null || requestParameters.products === undefined) {\n throw new runtime.RequiredError('products','Required parameter requestParameters.products was null or undefined when calling postEpassiNotify.');\n }\n\n if (requestParameters.reqid === null || requestParameters.reqid === undefined) {\n throw new runtime.RequiredError('reqid','Required parameter requestParameters.reqid was null or undefined when calling postEpassiNotify.');\n }\n\n if (requestParameters.hmac === null || requestParameters.hmac === undefined) {\n throw new runtime.RequiredError('hmac','Required parameter requestParameters.hmac was null or undefined when calling postEpassiNotify.');\n }\n\n if (requestParameters.type === null || requestParameters.type === undefined) {\n throw new runtime.RequiredError('type','Required parameter requestParameters.type was null or undefined when calling postEpassiNotify.');\n }\n\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling postEpassiNotify.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.amount !== undefined) {\n queryParameters['amount'] = requestParameters.amount;\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.products) {\n queryParameters['products'] = requestParameters.products;\n }\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n if (requestParameters.type !== undefined) {\n queryParameters['type'] = requestParameters.type;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/epassi/notify`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.JSONApiResponse(response);\n }\n\n /**\n * Epassi notify\n */\n async postEpassiNotify(requestParameters: PostEpassiNotifyRequest): Promise {\n const response = await this.postEpassiNotifyRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Epassi OK Epassi sends its OK response as POST, we then redirect this to GET with same parameters\n */\n async postEpassiOkRaw(requestParameters: PostEpassiOkRequest): Promise> {\n if (requestParameters.amount === null || requestParameters.amount === undefined) {\n throw new runtime.RequiredError('amount','Required parameter requestParameters.amount was null or undefined when calling postEpassiOk.');\n }\n\n if (requestParameters.expiry === null || requestParameters.expiry === undefined) {\n throw new runtime.RequiredError('expiry','Required parameter requestParameters.expiry was null or undefined when calling postEpassiOk.');\n }\n\n if (requestParameters.products === null || requestParameters.products === undefined) {\n throw new runtime.RequiredError('products','Required parameter requestParameters.products was null or undefined when calling postEpassiOk.');\n }\n\n if (requestParameters.reqid === null || requestParameters.reqid === undefined) {\n throw new runtime.RequiredError('reqid','Required parameter requestParameters.reqid was null or undefined when calling postEpassiOk.');\n }\n\n if (requestParameters.hmac === null || requestParameters.hmac === undefined) {\n throw new runtime.RequiredError('hmac','Required parameter requestParameters.hmac was null or undefined when calling postEpassiOk.');\n }\n\n if (requestParameters.type === null || requestParameters.type === undefined) {\n throw new runtime.RequiredError('type','Required parameter requestParameters.type was null or undefined when calling postEpassiOk.');\n }\n\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling postEpassiOk.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.amount !== undefined) {\n queryParameters['amount'] = requestParameters.amount;\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.products) {\n queryParameters['products'] = requestParameters.products;\n }\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n if (requestParameters.type !== undefined) {\n queryParameters['type'] = requestParameters.type;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/epassi/ok`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.VoidApiResponse(response);\n }\n\n /**\n * Epassi OK Epassi sends its OK response as POST, we then redirect this to GET with same parameters\n */\n async postEpassiOk(requestParameters: PostEpassiOkRequest): Promise {\n await this.postEpassiOkRaw(requestParameters);\n }\n\n /**\n */\n async postTurkuNotifyRaw(requestParameters: PostTurkuNotifyRequest): Promise> {\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling postTurkuNotify.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/payment/turku/notify`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.VoidApiResponse(response);\n }\n\n /**\n */\n async postTurkuNotify(requestParameters: PostTurkuNotifyRequest): Promise {\n await this.postTurkuNotifyRaw(requestParameters);\n }\n\n}\n","/* tslint:disable */\n/* eslint-disable */\n/**\n * API\n * This document specifies the application programming interface (API) for Hellewi. # URL format `https://api.////` e.g. `https://api.opistopalvelut.fi/v1/demo/fi/courses` - protocol: `https` ssl must be used. - domain: `opistopalvelut.fi` domain should be the same as with tenant\\'s admin and registration domain (i.e. not necessarily opistopalvelut.fi). - version: `v1` marks the API version, only v1 is supported. - tenant: `demo` is the tenant name, should be the same as with admin/registration url - language: `fi` is language selection, options fi/en/sv are supported - endpoint: `courses` api endpoint Tenant must be given also inside [JWT](#section/Authentication/JWT), and it must match the tenant/domain in URL. ## Multi-tenant request `https://api.linnunrata.fi///` e.g. `https://api.linnunrata.fi/v1/fi/courses` This will search for courses from all the tenants that the used api key can access. All endpoints do not support multi-tenancy, those that do are listed under tag [MultiTenant](#tag/MultiTenant). # Testing API - url: `https://api.opistopalvelut.fi/v1/demo/fi/` - api key: `demo` - secret: `salasana` Testing API can access only the demo tenant (\\\"database\\\" or \\\"client\\\"). You can however access multiple tenants with the same API key depending on your license. # Search Search query API follows [GitHub Search](https://developer.github.com/v3/search) closely. Search string is in format `?q=SEARCH_TERM_1+SEARCH_TERM_N+FILTER_1+FILTER_N`, e.g. search terms and filters combined with `+` signs or url-encoded spaces `%20`. Search terms are keywords to be searched. Disallowed characters are: `: + \\\" \\'`. Results are sorted by default based on matching: best matching entries first. ### Filters Filters are in format `field:value`. In this simplest case, the field must be equal to the given value. If there are multiple filters defined, results must match *all* different filters. If there are multiple filter values for the same field, results must match *one* of those. For example: `?q=department:3+subject:1+subject:2` means that all courses with department 3 and subject 1 or 2. ### Filter operators Supported operators in filters: - `:` equality, for example: `subject:1` - `:!` not, for example: `department:!2` - `:<` less than, for example: `distancesoft:<10km` - `:<=` less than or equal, for example: `ends:<=2020-02-01` - `:>` greater than, for example: `begins:>2019-01-01` - `:>=` greater than or equal, for example: `begins:>=2019-01-01` ## Pagination Pagination is handled with response headers. Unfortunately these are not included in swagger.json due to technical restrictions in the spec generation. ### link `link` follows [RFC5988](https://tools.ietf.org/html/rfc5988#section-5) and [GitHub pagination](https://developer.github.com/v3/guides/traversing-with-pagination/): there are at most four links: - `rel=\\\"first\\\"` gives the first page of results with current filters - `rel=\\\"prev\\\"` previous page - `rel=\\\"next\\\"` next page - `rel=\\\"last\\\"` last page For example (linefeeds added): GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 link: ; rel=\\\"first\\\", ; rel=\\\"prev\\\", ; rel=\\\"next\\\", ; rel=\\\"last\\\" If the query is for the first page, first and prev are omitted. Similarly, if the query is for the last page, last and next are omitted. If there are no results, all links are omitted. The link-header should be used for creating pagination functionality. ### x-total-count `x-total-count` is the total number of items in result set. For example: GET https://api.opistopalvelut.fi/v1/demo/fi/courses?page=5&limit=2 x-total-count: 29 The maximum number here is 10000. If you are expecting a bigger number, or are interested only in the total course count and not the results itself, you should be using [GetCourseCount](#operation/GetCourseCount). ## Course search Field explanations by name can be found from [ListCourses](#operation/ListCourses) response schema. ### Search term Search term matches text in following course fields with relative weights in parenthesis: - name (10) - code (7) - teacher (5) - subject (4) - location (3) - category (2) - description (1) Search terms can have multiple words combined with double quotation marks `\\\"`. For example: `?q=jooga` search term \\\"jooga\\\" (probably in course name) `?q=830107+\\\"dynaaminen jooga\\\"` search terms \\\"830107\\\" and \\\"dynaaminen jooga\\\" ### Equality filters by id - category - classification - department - educationsector - language - levelofstudy - location - moduleparent (also null works) - period - subject - tag - tenant - term - weekday Category can be queried with a path \\\"department/category\\\" and subject with \\\"department/category/subject\\\" or \\\"category/subject\\\". Tenant makes sense only in the context of multi-tenant requests. For example: `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `q=weekday:1` courses that have lessons on Monday `q=moduleparent:18` module child courses that have parent course 18 `q=moduleparent:null` all courses that are not module child courses ### Date filters - begins (date-only, YYYY-MM-DD) - ends (date-only, YYYY-MM-DD) - registrationbegins (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendssoft (date-time, YYYY-MM-DDTHH:MM:SSZ) - registrationendshard (date-time, YYYY-MM-DDTHH:MM:SSZ) For example: `?q=begins:>=2019-01-01`: courses that begin after or on 1.1.2019 ### Distance filters - distancefrom (origin coordinates: \\\\,\\\\) - distancehard (distance: \\\\km or \\\\m), if course\\'s location is farther, course will not be included in results - distancesoft (distance: \\\\km or \\\\m), will return all courses ordered by distance, courses farther than this distance will be ranked considerably lower (but if only this filter is used, value can be anything) distancefrom must be given if either filter is used. Only less than operator `:<` is supported for distancehard and distancesoft. For example: `?q=distancefrom:60.169,24.944+distancehard:<50km+distancesoft:<10km`: courses that are closer than 50km from 60.169,24.944, ordered by proximity ### Other filters - registrationopen (boolean, only true supported) registration is currently open. See rules in [Registration](#section/Registration). For example: `?q=registrationopen:true`: courses where registration is open currently ### Distance learning (etäopetus) Distance learning courses have a special location `distancelearning`: `?q=location:distancelearning`: only distance learning courses `?q=location:!distancelearning`: only non-distance learning (lähiopetus) courses ### Additional examples `?q=jooga+category:2`: search term \\\"jooga\\\" in category 2 courses `?q=department:3+subject:1+subject:2` all courses with department 3 *and* subject 1 *or* 2 `?q=jooga+begins:>=2019-01-01` search term \\\"jooga\\\" in courses that begin after or on 1.1.2019 # Registration Deciding whether you can register to a course follows these rules that are compared in order (i.e. when a rule matches, the rest are not evaluated): 1. current time < course.registrationbegins: **registration IS NOT open**
Registration is not yet started. 2. course.ends < current time: **registration IS NOT open**
Course has already ended. If course ending time is not defined, this step is skipped. 3. current time < course.registrationendshard: **registration IS open**
Registration is not yet ended. 4. otherwise: **registration IS NOT open** Admin interface has only one time for registration ending. It also has a global setting for making that ending time either a soft or hard deadline. API registrationendshard takes this global setting into account (it will be null if registrationends is a soft deadline). Also, if registration beginning or ending time is undefined in admin interface, they will be null in API (i.e. registrationbegins null => registration not open, registrationends null => registration is open). ## Registration to lessons Some courses can have registration to single or multiple lessons in addition to registration to the whole course. Registering to lessons can be enabled from admin interface, and is possible if registration to the course is open, and if there is available places in that lesson. Selecting a price means that registration is for the whole course, where selecting lessons means that registration is only for the specified lessons. Both options are not always present, they follow these rules: 1. If course has registration to lessons (\\\"registration practice\\\" / \\\"ilmoittautumistapa: ilmoittautuminen kerroille\\\") and lessons defined, lessons can be selected. 2. If registration to lessons is restricted (\\\"lesson registration limit\\\" / \\\"kuinka monelle seuraavalle kerralle voi ilmoittautua\\\"), prices cannot be selected. 3. If registration to lessons doesn\\'t cost anything (\\\"single lesson fee\\\" / \\\"kertamaksu\\\" 0,00 e) and all course prices are also zero, prices (and lessons) can be selected. 4. If registration to lessons cost something and all course prices are *not* zero, prices (and lessons) can be selected. 5. If course doesn\\'t have any prices and registration to lessons is free, leaving lessons empty is also allowed, which means that the registration is for the whole course. Note that even when prices and lessons are both selectable, user cannot select both at the same time. Registration to lessons\\' spare places is not possible. ## Selecting installments Prices can have one or more installment groups that can be selected as payment option. Installment options can also expire so that for example course has separate installments for fall and spring period, and fall installment is only valid until the change of period. Course can have prices with installments and prices without installments at the same time. If a course price has installments, selecting one is required. ## How price is calculated 1. Lessons selected: price will be the price of combined lessons 2. Spare place: price for a spare place course is always zero 3. Installments selected: price that can or has to be paid after registration will be the price of the first installment, other installments will be billed later 4. Otherwise: price will be the selected price # Rate limit Requests to API are rate limited to 100 requests per 10 seconds. When this rate limit is exceeded, API will respond with HTTP status code 429 too many requests. # JWT authentication Required fields for header are: - `typ` must always be \\'JWT\\' - `alg` JWT signing algorithm, supported algorithms are: \\'HS256\\', \\'HS384\\' and \\'HS512\\' - `kid` API key (*not* secret), \\'demo\\' can be used for testing with demo data Required fields for payload are: - `sub` subject, must be \\'hellewi-api-v1\\' for this API - `iat` issued at (unix timestamp) - `exp` expiration time (unix timestamp). Maximum expiration time is one week in the future. When using key id \\'demo\\', this is not enforced. - `tenant` tenant identifier, this is in format `.`, e.g. for opistopalvelut.fi/demo tenant is demo.opistopalvelut.fi - `jti` JWT id, unique identifier, used for identifying sessions. This must be a random string. More information about JWTs and an excellent tool for generating JWT tokens for testing can be found at [jwt.io](https://jwt.io). JWT formatting can be tested against [JWT status endpoint](#operation/GetStatusJwt) For simple authentication testing, here is a ready-made JWT: Header: ```json { \\\"alg\\\": \\\"HS256\\\", \\\"typ\\\": \\\"JWT\\\", \\\"kid\\\": \\\"demo\\\" } ``` Payload: ```json { \\\"iat\\\": 1516239022, \\\"exp\\\": 2524608000, \\\"sub\\\": \\\"hellewi-api-v1\\\", \\\"tenant\\\": \\\"demo.opistopalvelut.fi\\\", \\\"jti\\\": \\\"ahtu0aiWo4aMooshie9waethae7cuaj0ua2uichieshaevee3j\\\" } ``` Secret: `salasana` Encoded (linefeeds added): ```markup eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1 MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktYXBpLXYxIiwidGVuYW50IjoiZGVtby5vcGl zdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFodHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2 N1YWowdWEydWljaGllc2hhZXZlZTNqIiwiZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7Vm LCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g ``` Shell command: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" https://api.opistopalvelut.fi/v1/demo/fi/status/jwt ``` # HMAC request signing JWT authentication is used on server to server communication, but sometimes there is a need for authenticating requests that originate from user\\'s browser, such as viewing previously done registrations. Note that the signed request links must be generated in a backend service as the signing uses api secret which must not be given to users. Fields used for HMAC calculation, example from [GetStatusHmac](#operation/GetStatusHmac): - all request parameters *except* `hmac`: - `reqid`: random UUIDv4 string, e.g. \\'9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d\\' - `expiry`: expiration time in ISO8601 format, e.g. \\'2023-06-15T06:00:00Z\\' - there probably are also other parameters, depending on the endpoint - two additional parameters that are *not* in the actual request: - `scope`: endpoint HMAC scope, this is show in each endpoint\\'s authorizations-section and start with \\'hmac-\\', e.g. \\'hmac-status-required\\' - `tenant`: same tenant string as is used in JWT authentication, e.g. \\'demo.opistopalvelut.fi\\' All listed parameters are then ordered alphabetically and url-encoded as if they were normal query parameters (linefeed added): ```markup expiry=2023-06-15T06%3A00%3A00Z&reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d& scope=hmac-status-required&tenant=demo.opistopalvelut.fi ``` Then HMAC digest is calculated with SHA-256 using api secret. Above with secret `salasana`: ```markup 88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` This is used as the request `hmac` parameter value. So after all the request is (linefeeds added): ```markup https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a ``` HMAC can be tested against [HMAC status endpoint](#operation/GetStatusHmac). JWT must be used there as well: ```bash curl -v -H \\\"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXV CIsImtpZCI6ImRlbW8ifQ.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6ImhlbGxld2ktY XBpLXYxIiwidGVuYW50IjoiZGVtby5vcGlzdG9wYWx2ZWx1dC5maSIsImp0aSI6ImFo dHUwYWlXbzRhTW9vc2hpZTl3YWV0aGFlN2N1YWowdWEydWljaGllc2hhZXZlZTNqIiw iZXhwIjoyNTI0NjA4MDAwfQ.BhzoUPL7VmLCsrtTaZ2yyPIljYNZEiUyoayqRYchm3g\\\" \\\"https://api.opistopalvelut.fi/v1/demo/fi/status/hmac ?expiry=2023-06-15T06%3A00%3A00Z &reqid=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d &hmac=88290d424fa7bc11afc22140346ba3f3fa0552bff121fb6fb63c60deec7b796a\\\" ``` ## Node.js implementation Uses `date-fns` and `uuid` external libraries, and node.js `crypto`. ```javascript import { createHmac } from \\'crypto\\'; import { addYears } from \\'date-fns\\'; import { v4 as uuidv4 } from \\'uuid\\'; const HMAC_ALGORITHM = \\'sha256\\'; const API_SECRET = \\'salasana\\'; // use proper secret handling const queryParamsWithoutHmac = { // use shorter expiration expiry: addYears(new Date(), 2).toISOString(), reqid: uuidv4() }; const hmacCalculationParams = { ...queryParamsWithoutHmac, scope: \\'hmac-status-required\\', tenant: \\'demo.opistopalvelut.fi\\' }; const hmacCalculationURLSearchParams = new URLSearchParams( hmacCalculationParams ); hmacCalculationURLSearchParams.sort(); const hmac = createHmac(HMAC_ALGORITHM, API_SECRET) .update(hmacCalculationURLSearchParams.toString()) .digest(\\'hex\\'); const queryParams = { ...queryParamsWithoutHmac, hmac }; const urlSearchParams = new URLSearchParams(queryParams); console.log(queryParams); console.log(urlSearchParams.toString()); ``` Prints out: ```markup { reqid: \\'09d1eb20-1b84-4af6-935b-6de0db0b3e75\\', expiry: \\'2024-01-05T10:32:50.646Z\\', hmac: \\'27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987\\' } reqid=09d1eb20-1b84-4af6-935b-6de0db0b3e75&expiry=2024-01-05T10%3A32%3A50.646Z&hmac=27d7d2d4136f9e4962a6058296864f15985f81086d145ae687075ce60510c987 ``` \n *\n * The version of the OpenAPI document: 0.1.0\n * Contact: perttu.tikka@hellewi.fi\n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport * as runtime from '../runtime';\nimport {\n AjvErrorResponse,\n AjvErrorResponseFromJSON,\n AjvErrorResponseToJSON,\n ClientRegistrationDuplicateError,\n ClientRegistrationDuplicateErrorFromJSON,\n ClientRegistrationDuplicateErrorToJSON,\n ErrorResponse,\n ErrorResponseFromJSON,\n ErrorResponseToJSON,\n HellewiGetRegistrationResponse,\n HellewiGetRegistrationResponseFromJSON,\n HellewiGetRegistrationResponseToJSON,\n HellewiMyRegistrationsResponse,\n HellewiMyRegistrationsResponseFromJSON,\n HellewiMyRegistrationsResponseToJSON,\n HellewiPostRegistrationResponse,\n HellewiPostRegistrationResponseFromJSON,\n HellewiPostRegistrationResponseToJSON,\n RegistrationPriceNumber,\n RegistrationPriceNumberFromJSON,\n RegistrationPriceNumberToJSON,\n} from '../models';\n\nexport interface GetMyRegistrationsRequest {\n reqid: string;\n expiry: Date;\n hmac: string;\n email: string;\n}\n\nexport interface GetRegistrationRequest {\n reqid: string;\n expiry: Date;\n hmac: string;\n referencenumber: string;\n}\n\nexport interface GetRegistrationCancelRequest {\n cancellationdate: Date;\n expiry: Date;\n id: number;\n reqid: string;\n hmac: string;\n}\n\nexport interface GetRegistrationFormRequest {\n ids?: Array;\n}\n\nexport interface GetRegistrationPriceSchemaRequest {\n ids?: Array;\n}\n\nexport interface PostMyRegistrationsLoginLinkRequest {\n requestBody: { [key: string]: object; };\n}\n\nexport interface PostRegistrationRequest {\n requestBody: { [key: string]: object; };\n}\n\nexport interface PostRegistrationPriceRequest {\n ids?: Array;\n requestBody?: { [key: string]: object; };\n}\n\n/**\n * RegistrationApi - interface\n * \n * @export\n * @interface RegistrationApiInterface\n */\nexport interface RegistrationApiInterface {\n /**\n * Get my registration informations This endpoint is similar with GetRegistration, but here you can get multiple registrations via email, where as GetRegistration returns a registration via reference number (with possibitity with payment)\n * @param {string} reqid \n * @param {Date} expiry \n * @param {string} hmac \n * @param {string} email E-mail address of a client or payer\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof RegistrationApiInterface\n */\n getMyRegistrationsRaw(requestParameters: GetMyRegistrationsRequest): Promise>;\n\n /**\n * Get my registration informations This endpoint is similar with GetRegistration, but here you can get multiple registrations via email, where as GetRegistration returns a registration via reference number (with possibitity with payment)\n */\n getMyRegistrations(requestParameters: GetMyRegistrationsRequest): Promise;\n\n /**\n * Get registration informations This endpoint is similar with GetMyRegistrations\n * @param {string} reqid \n * @param {Date} expiry \n * @param {string} hmac \n * @param {string} referencenumber Reference number for invoice(s)\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof RegistrationApiInterface\n */\n getRegistrationRaw(requestParameters: GetRegistrationRequest): Promise>;\n\n /**\n * Get registration informations This endpoint is similar with GetMyRegistrations\n */\n getRegistration(requestParameters: GetRegistrationRequest): Promise;\n\n /**\n * Cancel registration\n * @param {Date} cancellationdate \n * @param {Date} expiry \n * @param {number} id \n * @param {string} reqid \n * @param {string} hmac \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof RegistrationApiInterface\n */\n getRegistrationCancelRaw(requestParameters: GetRegistrationCancelRequest): Promise>;\n\n /**\n * Cancel registration\n */\n getRegistrationCancel(requestParameters: GetRegistrationCancelRequest): Promise;\n\n /**\n * Get registration form Response is a [JSON Schema](https://json-schema.org/) object. ## Overview The general structure for JSON schema is shown below. `properties.registrations` is the most important field and it defines client and course for a registration. The array will have one item per given course id. `properties.payer` has fields for payer\\'s information. Fields can reference definitions under `$defs`, where - `client` and `clientMinimum` specify client fields - `course-` specifies course fields - `language` and lots of others specify selectable fields - `pinHetu` and `pinBirthday` contain additional specifications that are used along with client fields ```json { \\\"$id\\\": \\\"https://api.opistopalvelut.fi/v1/demo/fi/registration-form?ids=2&ids=2&ids=3&ids=15\\\", \\\"$schema\\\": \\\"http://json-schema.org/draft-07/schema#\\\", \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"registrations\\\": { }, \\\"payer\\\": { } }, \\\"required\\\": [\\\"registrations\\\", \\\"payer\\\"], \\\"$defs\\\": { \\\"client\\\": { }, \\\"clientMinimum\\\": { }, \\\"course-2\\\": { }, \\\"course-3\\\": { }, \\\"language\\\": { }, \\\"pinHetu\\\": { }, \\\"pinBirthday\\\": { } } } ``` ## Registrations `registrations`-array will have client and course fields for each registration. The array items are always references to respective client and course field specifications under `$defs`. Some courses require only minimum amount of client information, which is noted with `clientMinimum` here. Client\\'s `pin` field requirement depends also on course: pin might not be required at all, or it should contain a finnish social security number (`pinHetu`), or just birthday (`pinBirthday`). Even if pin was not required at all, client definition will have the extra `allOf` so that the reference is always found from the same path `properties.client.allOf[0].$ref`. Spare means that the registration is going to queue for a place in the course, i.e. a spare place. This is decided by the backend, so it always has a const value, and is only for showing to the user. Sparecounter is the position in spare place queue. ```json \\\"registrations\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": [ { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/clientMinimum\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-2\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/clientMinimum\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-2\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/client\\\" }, { \\\"$ref\\\": \\\"#/$defs/pinHetu\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-3\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/client\\\" }, { \\\"$ref\\\": \\\"#/$defs/pinBirthday\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-15\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] } ] } ``` ## Payer `payer` has payer information for billing. This is a straightforward object with field definitions depending on configuration. See explanations for different field types under Client. ```json \\\"payer\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"billingid\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^([0-9]{7}-[0-9])|([0-9]{6}[+-A][0-9]{3}[0123456789ABCDEFHJKLMNPRSTUVWXY])$\\\" }, \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" } }, \\\"required\\\": [\\\"billingid\\\", \\\"firstname\\\", \\\"lastname\\\"] } ``` ## Client `client` and `clientMinimum` definitions contain the fields for clients. Types of fields: - `object` object with key-value pairs defined in `properties`. - `array` array of items, defined in `items`. - `string` text input or textarea. - `boolean` checkbox. - `number` text input with a number. Attributes for validation, these should\\'t require any action as json-schema validation libraries will handle the validation: - `pattern` validation for string as a regular expression. - `const` only accepted value for the field. - `oneOf` array of possible valid options for this field. - `required` objects can be valid even if all properties are not included, required tells which must be present. - `not` negates the value, used only as `{ not: { required: [\\'installments\\'] } }` - `minItems`, `maxItems` Attributes for user interface: - `title` label for the field (e.g. a custom question) or for select inputs - `default` default value - `amount` price - `begins` begins at this time - `ends` ends at this time - `location` name of location - `payableOnRegistration` (only on installments) whether this installment is payable on registration - `expiry` ? ```json \\\"client\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"pin\\\": { \\\"type\\\": \\\"string\\\" }, \\\"email\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[a-z0-9!#$%&\\'*+\\\\/=?^_`{|}~-]+(?:\\\\\\\\.[a-z0-9!#$%&\\'*+\\\\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$\\\" }, \\\"language\\\": { \\\"$ref\\\": \\\"#/$defs/language\\\" }, \\\"permissiontopublishphoto\\\": { \\\"type\\\": \\\"boolean\\\" } }, \\\"required\\\": [\\\"firstname\\\", \\\"lastname\\\", \\\"pin\\\"] }, \\\"clientMinimum\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" } }, \\\"required\\\": [\\\"firstname\\\", \\\"lastname\\\"] }, ``` ## Course Basic structure for a course. Price can have multiple options. ```json \\\"course-1\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1 }, \\\"price\\\": { \\\"type\\\": \\\"object\\\", \\\"default\\\": { \\\"id\\\": 1 }, \\\"oneOf\\\": [ { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Kurssimaksu\\\", \\\"amount\\\": 5.5, \\\"const\\\": 1, } }, \\\"required\\\": [\\\"id\\\"], \\\"not\\\": { \\\"required\\\": [\\\"installments\\\"] } } ] } }, \\\"required\\\": [\\\"id\\\", \\\"price\\\"] } ``` ## Course with two prices and installments Price with two options, the other also having two sets of installments. ```json \\\"course-2\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2 }, \\\"price\\\": { \\\"type\\\": \\\"object\\\", \\\"default\\\": { \\\"id\\\": 3, \\\"installments\\\": [1, 2] }, \\\"oneOf\\\": [ { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Kurssimaksu\\\", \\\"amount\\\": 45.7, \\\"const\\\": 2 } }, \\\"required\\\": [\\\"id\\\"], \\\"not\\\": { \\\"required\\\": [\\\"installments\\\"] } }, { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Opiskelijahinta\\\", \\\"amount\\\": 30, \\\"const\\\": 3 }, \\\"installments\\\": { \\\"type\\\": \\\"array\\\", \\\"oneOf\\\": [ { \\\"title\\\": \\\"installment1\\\", \\\"minItems\\\": 2, \\\"maxItems\\\": 2, \\\"items\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1, \\\"title\\\": \\\"Erä 1\\\", \\\"amount\\\": 17, \\\"payableOnRegistration\\\": true, \\\"expiry\\\": null }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2, \\\"title\\\": \\\"Erä 2\\\", \\\"amount\\\": 13, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null } ] }, { \\\"title\\\": \\\"installment2\\\", \\\"minItems\\\": 2, \\\"maxItems\\\": 2, \\\"items\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 3, \\\"title\\\": \\\"Erä 11\\\", \\\"amount\\\": 20, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 4, \\\"title\\\": \\\"Erä 12\\\", \\\"amount\\\": 15, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null } ] } ] } }, \\\"required\\\": [\\\"id\\\", \\\"installments\\\"] } ] }, \\\"info\\\": { \\\"type\\\": \\\"string\\\", \\\"title\\\": \\\"kysymys\\\" } }, \\\"required\\\": [\\\"id\\\", \\\"price\\\"] } ``` ## Course with registration to lessons ## Select with options There are lots of multiple choice fields, where the post value should be a number (id). The ids have meanings in title attribute. ```json \\\"language\\\": { \\\"type\\\": \\\"number\\\", \\\"oneOf\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1, \\\"title\\\": \\\"Suomi\\\" }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2, \\\"title\\\": \\\"Ruotsi\\\" }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 3, \\\"title\\\": \\\"Saame\\\" } ] }, ``` ## Miscellaneous definitions `pinHetu` and `pinBirthday` contain additional validation rules for client\\'s pin field. ```json \\\"pinHetu\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"pin\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[0-9]{6}[+-A][0-9]{3}[0123456789ABCDEFHJKLMNPRSTUVWXY]$\\\" } } }, \\\"pinBirthday\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"pin\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[0-9]{6}$\\\" } } } ```\n * @param {Array} [ids] Course ids for registrations, can contain duplicates if there are multiple registrations for the same course. This parameter is optional. Course ids are fetched from current cart courses if ids are not given. Normal operation should use this endpoint without `ids` query parameter so that registration form is always built from courses in the cart.\n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof RegistrationApiInterface\n */\n getRegistrationFormRaw(requestParameters: GetRegistrationFormRequest): Promise>;\n\n /**\n * Get registration form Response is a [JSON Schema](https://json-schema.org/) object. ## Overview The general structure for JSON schema is shown below. `properties.registrations` is the most important field and it defines client and course for a registration. The array will have one item per given course id. `properties.payer` has fields for payer\\'s information. Fields can reference definitions under `$defs`, where - `client` and `clientMinimum` specify client fields - `course-` specifies course fields - `language` and lots of others specify selectable fields - `pinHetu` and `pinBirthday` contain additional specifications that are used along with client fields ```json { \\\"$id\\\": \\\"https://api.opistopalvelut.fi/v1/demo/fi/registration-form?ids=2&ids=2&ids=3&ids=15\\\", \\\"$schema\\\": \\\"http://json-schema.org/draft-07/schema#\\\", \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"registrations\\\": { }, \\\"payer\\\": { } }, \\\"required\\\": [\\\"registrations\\\", \\\"payer\\\"], \\\"$defs\\\": { \\\"client\\\": { }, \\\"clientMinimum\\\": { }, \\\"course-2\\\": { }, \\\"course-3\\\": { }, \\\"language\\\": { }, \\\"pinHetu\\\": { }, \\\"pinBirthday\\\": { } } } ``` ## Registrations `registrations`-array will have client and course fields for each registration. The array items are always references to respective client and course field specifications under `$defs`. Some courses require only minimum amount of client information, which is noted with `clientMinimum` here. Client\\'s `pin` field requirement depends also on course: pin might not be required at all, or it should contain a finnish social security number (`pinHetu`), or just birthday (`pinBirthday`). Even if pin was not required at all, client definition will have the extra `allOf` so that the reference is always found from the same path `properties.client.allOf[0].$ref`. Spare means that the registration is going to queue for a place in the course, i.e. a spare place. This is decided by the backend, so it always has a const value, and is only for showing to the user. Sparecounter is the position in spare place queue. ```json \\\"registrations\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": [ { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/clientMinimum\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-2\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/clientMinimum\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-2\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/client\\\" }, { \\\"$ref\\\": \\\"#/$defs/pinHetu\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-3\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/client\\\" }, { \\\"$ref\\\": \\\"#/$defs/pinBirthday\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-15\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] } ] } ``` ## Payer `payer` has payer information for billing. This is a straightforward object with field definitions depending on configuration. See explanations for different field types under Client. ```json \\\"payer\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"billingid\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^([0-9]{7}-[0-9])|([0-9]{6}[+-A][0-9]{3}[0123456789ABCDEFHJKLMNPRSTUVWXY])$\\\" }, \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" } }, \\\"required\\\": [\\\"billingid\\\", \\\"firstname\\\", \\\"lastname\\\"] } ``` ## Client `client` and `clientMinimum` definitions contain the fields for clients. Types of fields: - `object` object with key-value pairs defined in `properties`. - `array` array of items, defined in `items`. - `string` text input or textarea. - `boolean` checkbox. - `number` text input with a number. Attributes for validation, these should\\'t require any action as json-schema validation libraries will handle the validation: - `pattern` validation for string as a regular expression. - `const` only accepted value for the field. - `oneOf` array of possible valid options for this field. - `required` objects can be valid even if all properties are not included, required tells which must be present. - `not` negates the value, used only as `{ not: { required: [\\'installments\\'] } }` - `minItems`, `maxItems` Attributes for user interface: - `title` label for the field (e.g. a custom question) or for select inputs - `default` default value - `amount` price - `begins` begins at this time - `ends` ends at this time - `location` name of location - `payableOnRegistration` (only on installments) whether this installment is payable on registration - `expiry` ? ```json \\\"client\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"pin\\\": { \\\"type\\\": \\\"string\\\" }, \\\"email\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[a-z0-9!#$%&\\'*+\\\\/=?^_`{|}~-]+(?:\\\\\\\\.[a-z0-9!#$%&\\'*+\\\\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$\\\" }, \\\"language\\\": { \\\"$ref\\\": \\\"#/$defs/language\\\" }, \\\"permissiontopublishphoto\\\": { \\\"type\\\": \\\"boolean\\\" } }, \\\"required\\\": [\\\"firstname\\\", \\\"lastname\\\", \\\"pin\\\"] }, \\\"clientMinimum\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" } }, \\\"required\\\": [\\\"firstname\\\", \\\"lastname\\\"] }, ``` ## Course Basic structure for a course. Price can have multiple options. ```json \\\"course-1\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1 }, \\\"price\\\": { \\\"type\\\": \\\"object\\\", \\\"default\\\": { \\\"id\\\": 1 }, \\\"oneOf\\\": [ { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Kurssimaksu\\\", \\\"amount\\\": 5.5, \\\"const\\\": 1, } }, \\\"required\\\": [\\\"id\\\"], \\\"not\\\": { \\\"required\\\": [\\\"installments\\\"] } } ] } }, \\\"required\\\": [\\\"id\\\", \\\"price\\\"] } ``` ## Course with two prices and installments Price with two options, the other also having two sets of installments. ```json \\\"course-2\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2 }, \\\"price\\\": { \\\"type\\\": \\\"object\\\", \\\"default\\\": { \\\"id\\\": 3, \\\"installments\\\": [1, 2] }, \\\"oneOf\\\": [ { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Kurssimaksu\\\", \\\"amount\\\": 45.7, \\\"const\\\": 2 } }, \\\"required\\\": [\\\"id\\\"], \\\"not\\\": { \\\"required\\\": [\\\"installments\\\"] } }, { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Opiskelijahinta\\\", \\\"amount\\\": 30, \\\"const\\\": 3 }, \\\"installments\\\": { \\\"type\\\": \\\"array\\\", \\\"oneOf\\\": [ { \\\"title\\\": \\\"installment1\\\", \\\"minItems\\\": 2, \\\"maxItems\\\": 2, \\\"items\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1, \\\"title\\\": \\\"Erä 1\\\", \\\"amount\\\": 17, \\\"payableOnRegistration\\\": true, \\\"expiry\\\": null }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2, \\\"title\\\": \\\"Erä 2\\\", \\\"amount\\\": 13, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null } ] }, { \\\"title\\\": \\\"installment2\\\", \\\"minItems\\\": 2, \\\"maxItems\\\": 2, \\\"items\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 3, \\\"title\\\": \\\"Erä 11\\\", \\\"amount\\\": 20, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 4, \\\"title\\\": \\\"Erä 12\\\", \\\"amount\\\": 15, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null } ] } ] } }, \\\"required\\\": [\\\"id\\\", \\\"installments\\\"] } ] }, \\\"info\\\": { \\\"type\\\": \\\"string\\\", \\\"title\\\": \\\"kysymys\\\" } }, \\\"required\\\": [\\\"id\\\", \\\"price\\\"] } ``` ## Course with registration to lessons ## Select with options There are lots of multiple choice fields, where the post value should be a number (id). The ids have meanings in title attribute. ```json \\\"language\\\": { \\\"type\\\": \\\"number\\\", \\\"oneOf\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1, \\\"title\\\": \\\"Suomi\\\" }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2, \\\"title\\\": \\\"Ruotsi\\\" }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 3, \\\"title\\\": \\\"Saame\\\" } ] }, ``` ## Miscellaneous definitions `pinHetu` and `pinBirthday` contain additional validation rules for client\\'s pin field. ```json \\\"pinHetu\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"pin\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[0-9]{6}[+-A][0-9]{3}[0123456789ABCDEFHJKLMNPRSTUVWXY]$\\\" } } }, \\\"pinBirthday\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"pin\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[0-9]{6}$\\\" } } } ```\n */\n getRegistrationForm(requestParameters: GetRegistrationFormRequest): Promise;\n\n /**\n * Get more precise JSON schema for `/registration-price` payload\n * @param {Array} [ids] \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof RegistrationApiInterface\n */\n getRegistrationPriceSchemaRaw(requestParameters: GetRegistrationPriceSchemaRequest): Promise>;\n\n /**\n * Get more precise JSON schema for `/registration-price` payload\n */\n getRegistrationPriceSchema(requestParameters: GetRegistrationPriceSchemaRequest): Promise;\n\n /**\n * Request a login link for my registrations to be mailed to given e-mail address. E-Mail must be of a payer or a client.\n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof RegistrationApiInterface\n */\n postMyRegistrationsLoginLinkRaw(requestParameters: PostMyRegistrationsLoginLinkRequest): Promise>;\n\n /**\n * Request a login link for my registrations to be mailed to given e-mail address. E-Mail must be of a payer or a client.\n */\n postMyRegistrationsLoginLink(requestParameters: PostMyRegistrationsLoginLinkRequest): Promise;\n\n /**\n * \n * @param {{ [key: string]: object; }} requestBody \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof RegistrationApiInterface\n */\n postRegistrationRaw(requestParameters: PostRegistrationRequest): Promise>;\n\n /**\n */\n postRegistration(requestParameters: PostRegistrationRequest): Promise;\n\n /**\n * \n * @param {Array} [ids] \n * @param {{ [key: string]: object; }} [requestBody] \n * @param {*} [options] Override http request option.\n * @throws {RequiredError}\n * @memberof RegistrationApiInterface\n */\n postRegistrationPriceRaw(requestParameters: PostRegistrationPriceRequest): Promise>;\n\n /**\n */\n postRegistrationPrice(requestParameters: PostRegistrationPriceRequest): Promise;\n\n}\n\n/**\n * \n */\nexport class RegistrationApi extends runtime.BaseAPI implements RegistrationApiInterface {\n\n /**\n * Get my registration informations This endpoint is similar with GetRegistration, but here you can get multiple registrations via email, where as GetRegistration returns a registration via reference number (with possibitity with payment)\n */\n async getMyRegistrationsRaw(requestParameters: GetMyRegistrationsRequest): Promise> {\n if (requestParameters.reqid === null || requestParameters.reqid === undefined) {\n throw new runtime.RequiredError('reqid','Required parameter requestParameters.reqid was null or undefined when calling getMyRegistrations.');\n }\n\n if (requestParameters.expiry === null || requestParameters.expiry === undefined) {\n throw new runtime.RequiredError('expiry','Required parameter requestParameters.expiry was null or undefined when calling getMyRegistrations.');\n }\n\n if (requestParameters.hmac === null || requestParameters.hmac === undefined) {\n throw new runtime.RequiredError('hmac','Required parameter requestParameters.hmac was null or undefined when calling getMyRegistrations.');\n }\n\n if (requestParameters.email === null || requestParameters.email === undefined) {\n throw new runtime.RequiredError('email','Required parameter requestParameters.email was null or undefined when calling getMyRegistrations.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n if (requestParameters.email !== undefined) {\n queryParameters['email'] = requestParameters.email;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/my-registrations`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiMyRegistrationsResponseFromJSON(jsonValue));\n }\n\n /**\n * Get my registration informations This endpoint is similar with GetRegistration, but here you can get multiple registrations via email, where as GetRegistration returns a registration via reference number (with possibitity with payment)\n */\n async getMyRegistrations(requestParameters: GetMyRegistrationsRequest): Promise {\n const response = await this.getMyRegistrationsRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Get registration informations This endpoint is similar with GetMyRegistrations\n */\n async getRegistrationRaw(requestParameters: GetRegistrationRequest): Promise> {\n if (requestParameters.reqid === null || requestParameters.reqid === undefined) {\n throw new runtime.RequiredError('reqid','Required parameter requestParameters.reqid was null or undefined when calling getRegistration.');\n }\n\n if (requestParameters.expiry === null || requestParameters.expiry === undefined) {\n throw new runtime.RequiredError('expiry','Required parameter requestParameters.expiry was null or undefined when calling getRegistration.');\n }\n\n if (requestParameters.hmac === null || requestParameters.hmac === undefined) {\n throw new runtime.RequiredError('hmac','Required parameter requestParameters.hmac was null or undefined when calling getRegistration.');\n }\n\n if (requestParameters.referencenumber === null || requestParameters.referencenumber === undefined) {\n throw new runtime.RequiredError('referencenumber','Required parameter requestParameters.referencenumber was null or undefined when calling getRegistration.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n if (requestParameters.referencenumber !== undefined) {\n queryParameters['referencenumber'] = requestParameters.referencenumber;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/registration`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiGetRegistrationResponseFromJSON(jsonValue));\n }\n\n /**\n * Get registration informations This endpoint is similar with GetMyRegistrations\n */\n async getRegistration(requestParameters: GetRegistrationRequest): Promise {\n const response = await this.getRegistrationRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Cancel registration\n */\n async getRegistrationCancelRaw(requestParameters: GetRegistrationCancelRequest): Promise> {\n if (requestParameters.cancellationdate === null || requestParameters.cancellationdate === undefined) {\n throw new runtime.RequiredError('cancellationdate','Required parameter requestParameters.cancellationdate was null or undefined when calling getRegistrationCancel.');\n }\n\n if (requestParameters.expiry === null || requestParameters.expiry === undefined) {\n throw new runtime.RequiredError('expiry','Required parameter requestParameters.expiry was null or undefined when calling getRegistrationCancel.');\n }\n\n if (requestParameters.id === null || requestParameters.id === undefined) {\n throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling getRegistrationCancel.');\n }\n\n if (requestParameters.reqid === null || requestParameters.reqid === undefined) {\n throw new runtime.RequiredError('reqid','Required parameter requestParameters.reqid was null or undefined when calling getRegistrationCancel.');\n }\n\n if (requestParameters.hmac === null || requestParameters.hmac === undefined) {\n throw new runtime.RequiredError('hmac','Required parameter requestParameters.hmac was null or undefined when calling getRegistrationCancel.');\n }\n\n const queryParameters: any = {};\n\n if (requestParameters.cancellationdate !== undefined) {\n queryParameters['cancellationdate'] = (requestParameters.cancellationdate as any).toISOString();\n }\n\n if (requestParameters.expiry !== undefined) {\n queryParameters['expiry'] = (requestParameters.expiry as any).toISOString();\n }\n\n if (requestParameters.id !== undefined) {\n queryParameters['id'] = requestParameters.id;\n }\n\n if (requestParameters.reqid !== undefined) {\n queryParameters['reqid'] = requestParameters.reqid;\n }\n\n if (requestParameters.hmac !== undefined) {\n queryParameters['hmac'] = requestParameters.hmac;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/registration/cancel`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.VoidApiResponse(response);\n }\n\n /**\n * Cancel registration\n */\n async getRegistrationCancel(requestParameters: GetRegistrationCancelRequest): Promise {\n await this.getRegistrationCancelRaw(requestParameters);\n }\n\n /**\n * Get registration form Response is a [JSON Schema](https://json-schema.org/) object. ## Overview The general structure for JSON schema is shown below. `properties.registrations` is the most important field and it defines client and course for a registration. The array will have one item per given course id. `properties.payer` has fields for payer\\'s information. Fields can reference definitions under `$defs`, where - `client` and `clientMinimum` specify client fields - `course-` specifies course fields - `language` and lots of others specify selectable fields - `pinHetu` and `pinBirthday` contain additional specifications that are used along with client fields ```json { \\\"$id\\\": \\\"https://api.opistopalvelut.fi/v1/demo/fi/registration-form?ids=2&ids=2&ids=3&ids=15\\\", \\\"$schema\\\": \\\"http://json-schema.org/draft-07/schema#\\\", \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"registrations\\\": { }, \\\"payer\\\": { } }, \\\"required\\\": [\\\"registrations\\\", \\\"payer\\\"], \\\"$defs\\\": { \\\"client\\\": { }, \\\"clientMinimum\\\": { }, \\\"course-2\\\": { }, \\\"course-3\\\": { }, \\\"language\\\": { }, \\\"pinHetu\\\": { }, \\\"pinBirthday\\\": { } } } ``` ## Registrations `registrations`-array will have client and course fields for each registration. The array items are always references to respective client and course field specifications under `$defs`. Some courses require only minimum amount of client information, which is noted with `clientMinimum` here. Client\\'s `pin` field requirement depends also on course: pin might not be required at all, or it should contain a finnish social security number (`pinHetu`), or just birthday (`pinBirthday`). Even if pin was not required at all, client definition will have the extra `allOf` so that the reference is always found from the same path `properties.client.allOf[0].$ref`. Spare means that the registration is going to queue for a place in the course, i.e. a spare place. This is decided by the backend, so it always has a const value, and is only for showing to the user. Sparecounter is the position in spare place queue. ```json \\\"registrations\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": [ { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/clientMinimum\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-2\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/clientMinimum\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-2\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/client\\\" }, { \\\"$ref\\\": \\\"#/$defs/pinHetu\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-3\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/client\\\" }, { \\\"$ref\\\": \\\"#/$defs/pinBirthday\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-15\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] } ] } ``` ## Payer `payer` has payer information for billing. This is a straightforward object with field definitions depending on configuration. See explanations for different field types under Client. ```json \\\"payer\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"billingid\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^([0-9]{7}-[0-9])|([0-9]{6}[+-A][0-9]{3}[0123456789ABCDEFHJKLMNPRSTUVWXY])$\\\" }, \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" } }, \\\"required\\\": [\\\"billingid\\\", \\\"firstname\\\", \\\"lastname\\\"] } ``` ## Client `client` and `clientMinimum` definitions contain the fields for clients. Types of fields: - `object` object with key-value pairs defined in `properties`. - `array` array of items, defined in `items`. - `string` text input or textarea. - `boolean` checkbox. - `number` text input with a number. Attributes for validation, these should\\'t require any action as json-schema validation libraries will handle the validation: - `pattern` validation for string as a regular expression. - `const` only accepted value for the field. - `oneOf` array of possible valid options for this field. - `required` objects can be valid even if all properties are not included, required tells which must be present. - `not` negates the value, used only as `{ not: { required: [\\'installments\\'] } }` - `minItems`, `maxItems` Attributes for user interface: - `title` label for the field (e.g. a custom question) or for select inputs - `default` default value - `amount` price - `begins` begins at this time - `ends` ends at this time - `location` name of location - `payableOnRegistration` (only on installments) whether this installment is payable on registration - `expiry` ? ```json \\\"client\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"pin\\\": { \\\"type\\\": \\\"string\\\" }, \\\"email\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[a-z0-9!#$%&\\'*+\\\\/=?^_`{|}~-]+(?:\\\\\\\\.[a-z0-9!#$%&\\'*+\\\\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$\\\" }, \\\"language\\\": { \\\"$ref\\\": \\\"#/$defs/language\\\" }, \\\"permissiontopublishphoto\\\": { \\\"type\\\": \\\"boolean\\\" } }, \\\"required\\\": [\\\"firstname\\\", \\\"lastname\\\", \\\"pin\\\"] }, \\\"clientMinimum\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" } }, \\\"required\\\": [\\\"firstname\\\", \\\"lastname\\\"] }, ``` ## Course Basic structure for a course. Price can have multiple options. ```json \\\"course-1\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1 }, \\\"price\\\": { \\\"type\\\": \\\"object\\\", \\\"default\\\": { \\\"id\\\": 1 }, \\\"oneOf\\\": [ { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Kurssimaksu\\\", \\\"amount\\\": 5.5, \\\"const\\\": 1, } }, \\\"required\\\": [\\\"id\\\"], \\\"not\\\": { \\\"required\\\": [\\\"installments\\\"] } } ] } }, \\\"required\\\": [\\\"id\\\", \\\"price\\\"] } ``` ## Course with two prices and installments Price with two options, the other also having two sets of installments. ```json \\\"course-2\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2 }, \\\"price\\\": { \\\"type\\\": \\\"object\\\", \\\"default\\\": { \\\"id\\\": 3, \\\"installments\\\": [1, 2] }, \\\"oneOf\\\": [ { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Kurssimaksu\\\", \\\"amount\\\": 45.7, \\\"const\\\": 2 } }, \\\"required\\\": [\\\"id\\\"], \\\"not\\\": { \\\"required\\\": [\\\"installments\\\"] } }, { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Opiskelijahinta\\\", \\\"amount\\\": 30, \\\"const\\\": 3 }, \\\"installments\\\": { \\\"type\\\": \\\"array\\\", \\\"oneOf\\\": [ { \\\"title\\\": \\\"installment1\\\", \\\"minItems\\\": 2, \\\"maxItems\\\": 2, \\\"items\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1, \\\"title\\\": \\\"Erä 1\\\", \\\"amount\\\": 17, \\\"payableOnRegistration\\\": true, \\\"expiry\\\": null }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2, \\\"title\\\": \\\"Erä 2\\\", \\\"amount\\\": 13, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null } ] }, { \\\"title\\\": \\\"installment2\\\", \\\"minItems\\\": 2, \\\"maxItems\\\": 2, \\\"items\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 3, \\\"title\\\": \\\"Erä 11\\\", \\\"amount\\\": 20, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 4, \\\"title\\\": \\\"Erä 12\\\", \\\"amount\\\": 15, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null } ] } ] } }, \\\"required\\\": [\\\"id\\\", \\\"installments\\\"] } ] }, \\\"info\\\": { \\\"type\\\": \\\"string\\\", \\\"title\\\": \\\"kysymys\\\" } }, \\\"required\\\": [\\\"id\\\", \\\"price\\\"] } ``` ## Course with registration to lessons ## Select with options There are lots of multiple choice fields, where the post value should be a number (id). The ids have meanings in title attribute. ```json \\\"language\\\": { \\\"type\\\": \\\"number\\\", \\\"oneOf\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1, \\\"title\\\": \\\"Suomi\\\" }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2, \\\"title\\\": \\\"Ruotsi\\\" }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 3, \\\"title\\\": \\\"Saame\\\" } ] }, ``` ## Miscellaneous definitions `pinHetu` and `pinBirthday` contain additional validation rules for client\\'s pin field. ```json \\\"pinHetu\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"pin\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[0-9]{6}[+-A][0-9]{3}[0123456789ABCDEFHJKLMNPRSTUVWXY]$\\\" } } }, \\\"pinBirthday\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"pin\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[0-9]{6}$\\\" } } } ```\n */\n async getRegistrationFormRaw(requestParameters: GetRegistrationFormRequest): Promise> {\n const queryParameters: any = {};\n\n if (requestParameters.ids) {\n queryParameters['ids'] = requestParameters.ids;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/registration-form`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.TextApiResponse(response) as any;\n }\n\n /**\n * Get registration form Response is a [JSON Schema](https://json-schema.org/) object. ## Overview The general structure for JSON schema is shown below. `properties.registrations` is the most important field and it defines client and course for a registration. The array will have one item per given course id. `properties.payer` has fields for payer\\'s information. Fields can reference definitions under `$defs`, where - `client` and `clientMinimum` specify client fields - `course-` specifies course fields - `language` and lots of others specify selectable fields - `pinHetu` and `pinBirthday` contain additional specifications that are used along with client fields ```json { \\\"$id\\\": \\\"https://api.opistopalvelut.fi/v1/demo/fi/registration-form?ids=2&ids=2&ids=3&ids=15\\\", \\\"$schema\\\": \\\"http://json-schema.org/draft-07/schema#\\\", \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"registrations\\\": { }, \\\"payer\\\": { } }, \\\"required\\\": [\\\"registrations\\\", \\\"payer\\\"], \\\"$defs\\\": { \\\"client\\\": { }, \\\"clientMinimum\\\": { }, \\\"course-2\\\": { }, \\\"course-3\\\": { }, \\\"language\\\": { }, \\\"pinHetu\\\": { }, \\\"pinBirthday\\\": { } } } ``` ## Registrations `registrations`-array will have client and course fields for each registration. The array items are always references to respective client and course field specifications under `$defs`. Some courses require only minimum amount of client information, which is noted with `clientMinimum` here. Client\\'s `pin` field requirement depends also on course: pin might not be required at all, or it should contain a finnish social security number (`pinHetu`), or just birthday (`pinBirthday`). Even if pin was not required at all, client definition will have the extra `allOf` so that the reference is always found from the same path `properties.client.allOf[0].$ref`. Spare means that the registration is going to queue for a place in the course, i.e. a spare place. This is decided by the backend, so it always has a const value, and is only for showing to the user. Sparecounter is the position in spare place queue. ```json \\\"registrations\\\": { \\\"type\\\": \\\"array\\\", \\\"items\\\": [ { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/clientMinimum\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-2\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/clientMinimum\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-2\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/client\\\" }, { \\\"$ref\\\": \\\"#/$defs/pinHetu\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-3\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] }, { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"client\\\": { \\\"allOf\\\": [{ \\\"$ref\\\": \\\"#/$defs/client\\\" }, { \\\"$ref\\\": \\\"#/$defs/pinBirthday\\\" }] }, \\\"course\\\": { \\\"$ref\\\": \\\"#/$defs/course-15\\\" } }, \\\"required\\\": [\\\"client\\\", \\\"course\\\"] } ] } ``` ## Payer `payer` has payer information for billing. This is a straightforward object with field definitions depending on configuration. See explanations for different field types under Client. ```json \\\"payer\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"billingid\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^([0-9]{7}-[0-9])|([0-9]{6}[+-A][0-9]{3}[0123456789ABCDEFHJKLMNPRSTUVWXY])$\\\" }, \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" } }, \\\"required\\\": [\\\"billingid\\\", \\\"firstname\\\", \\\"lastname\\\"] } ``` ## Client `client` and `clientMinimum` definitions contain the fields for clients. Types of fields: - `object` object with key-value pairs defined in `properties`. - `array` array of items, defined in `items`. - `string` text input or textarea. - `boolean` checkbox. - `number` text input with a number. Attributes for validation, these should\\'t require any action as json-schema validation libraries will handle the validation: - `pattern` validation for string as a regular expression. - `const` only accepted value for the field. - `oneOf` array of possible valid options for this field. - `required` objects can be valid even if all properties are not included, required tells which must be present. - `not` negates the value, used only as `{ not: { required: [\\'installments\\'] } }` - `minItems`, `maxItems` Attributes for user interface: - `title` label for the field (e.g. a custom question) or for select inputs - `default` default value - `amount` price - `begins` begins at this time - `ends` ends at this time - `location` name of location - `payableOnRegistration` (only on installments) whether this installment is payable on registration - `expiry` ? ```json \\\"client\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"pin\\\": { \\\"type\\\": \\\"string\\\" }, \\\"email\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[a-z0-9!#$%&\\'*+\\\\/=?^_`{|}~-]+(?:\\\\\\\\.[a-z0-9!#$%&\\'*+\\\\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\\\\\\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$\\\" }, \\\"language\\\": { \\\"$ref\\\": \\\"#/$defs/language\\\" }, \\\"permissiontopublishphoto\\\": { \\\"type\\\": \\\"boolean\\\" } }, \\\"required\\\": [\\\"firstname\\\", \\\"lastname\\\", \\\"pin\\\"] }, \\\"clientMinimum\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"firstname\\\": { \\\"type\\\": \\\"string\\\" }, \\\"lastname\\\": { \\\"type\\\": \\\"string\\\" } }, \\\"required\\\": [\\\"firstname\\\", \\\"lastname\\\"] }, ``` ## Course Basic structure for a course. Price can have multiple options. ```json \\\"course-1\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1 }, \\\"price\\\": { \\\"type\\\": \\\"object\\\", \\\"default\\\": { \\\"id\\\": 1 }, \\\"oneOf\\\": [ { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Kurssimaksu\\\", \\\"amount\\\": 5.5, \\\"const\\\": 1, } }, \\\"required\\\": [\\\"id\\\"], \\\"not\\\": { \\\"required\\\": [\\\"installments\\\"] } } ] } }, \\\"required\\\": [\\\"id\\\", \\\"price\\\"] } ``` ## Course with two prices and installments Price with two options, the other also having two sets of installments. ```json \\\"course-2\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2 }, \\\"price\\\": { \\\"type\\\": \\\"object\\\", \\\"default\\\": { \\\"id\\\": 3, \\\"installments\\\": [1, 2] }, \\\"oneOf\\\": [ { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Kurssimaksu\\\", \\\"amount\\\": 45.7, \\\"const\\\": 2 } }, \\\"required\\\": [\\\"id\\\"], \\\"not\\\": { \\\"required\\\": [\\\"installments\\\"] } }, { \\\"properties\\\": { \\\"id\\\": { \\\"type\\\": \\\"number\\\", \\\"title\\\": \\\"Opiskelijahinta\\\", \\\"amount\\\": 30, \\\"const\\\": 3 }, \\\"installments\\\": { \\\"type\\\": \\\"array\\\", \\\"oneOf\\\": [ { \\\"title\\\": \\\"installment1\\\", \\\"minItems\\\": 2, \\\"maxItems\\\": 2, \\\"items\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1, \\\"title\\\": \\\"Erä 1\\\", \\\"amount\\\": 17, \\\"payableOnRegistration\\\": true, \\\"expiry\\\": null }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2, \\\"title\\\": \\\"Erä 2\\\", \\\"amount\\\": 13, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null } ] }, { \\\"title\\\": \\\"installment2\\\", \\\"minItems\\\": 2, \\\"maxItems\\\": 2, \\\"items\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 3, \\\"title\\\": \\\"Erä 11\\\", \\\"amount\\\": 20, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 4, \\\"title\\\": \\\"Erä 12\\\", \\\"amount\\\": 15, \\\"payableOnRegistration\\\": false, \\\"expiry\\\": null } ] } ] } }, \\\"required\\\": [\\\"id\\\", \\\"installments\\\"] } ] }, \\\"info\\\": { \\\"type\\\": \\\"string\\\", \\\"title\\\": \\\"kysymys\\\" } }, \\\"required\\\": [\\\"id\\\", \\\"price\\\"] } ``` ## Course with registration to lessons ## Select with options There are lots of multiple choice fields, where the post value should be a number (id). The ids have meanings in title attribute. ```json \\\"language\\\": { \\\"type\\\": \\\"number\\\", \\\"oneOf\\\": [ { \\\"type\\\": \\\"number\\\", \\\"const\\\": 1, \\\"title\\\": \\\"Suomi\\\" }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 2, \\\"title\\\": \\\"Ruotsi\\\" }, { \\\"type\\\": \\\"number\\\", \\\"const\\\": 3, \\\"title\\\": \\\"Saame\\\" } ] }, ``` ## Miscellaneous definitions `pinHetu` and `pinBirthday` contain additional validation rules for client\\'s pin field. ```json \\\"pinHetu\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"pin\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[0-9]{6}[+-A][0-9]{3}[0123456789ABCDEFHJKLMNPRSTUVWXY]$\\\" } } }, \\\"pinBirthday\\\": { \\\"type\\\": \\\"object\\\", \\\"properties\\\": { \\\"pin\\\": { \\\"type\\\": \\\"string\\\", \\\"pattern\\\": \\\"^[0-9]{6}$\\\" } } } ```\n */\n async getRegistrationForm(requestParameters: GetRegistrationFormRequest): Promise {\n const response = await this.getRegistrationFormRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Get more precise JSON schema for `/registration-price` payload\n */\n async getRegistrationPriceSchemaRaw(requestParameters: GetRegistrationPriceSchemaRequest): Promise> {\n const queryParameters: any = {};\n\n if (requestParameters.ids) {\n queryParameters['ids'] = requestParameters.ids;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/registration-price/schema`,\n method: 'GET',\n headers: headerParameters,\n query: queryParameters,\n });\n\n return new runtime.TextApiResponse(response) as any;\n }\n\n /**\n * Get more precise JSON schema for `/registration-price` payload\n */\n async getRegistrationPriceSchema(requestParameters: GetRegistrationPriceSchemaRequest): Promise {\n const response = await this.getRegistrationPriceSchemaRaw(requestParameters);\n return await response.value();\n }\n\n /**\n * Request a login link for my registrations to be mailed to given e-mail address. E-Mail must be of a payer or a client.\n */\n async postMyRegistrationsLoginLinkRaw(requestParameters: PostMyRegistrationsLoginLinkRequest): Promise> {\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling postMyRegistrationsLoginLink.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/my-registrations/login-link`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.VoidApiResponse(response);\n }\n\n /**\n * Request a login link for my registrations to be mailed to given e-mail address. E-Mail must be of a payer or a client.\n */\n async postMyRegistrationsLoginLink(requestParameters: PostMyRegistrationsLoginLinkRequest): Promise {\n await this.postMyRegistrationsLoginLinkRaw(requestParameters);\n }\n\n /**\n */\n async postRegistrationRaw(requestParameters: PostRegistrationRequest): Promise> {\n if (requestParameters.requestBody === null || requestParameters.requestBody === undefined) {\n throw new runtime.RequiredError('requestBody','Required parameter requestParameters.requestBody was null or undefined when calling postRegistration.');\n }\n\n const queryParameters: any = {};\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/registration`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => HellewiPostRegistrationResponseFromJSON(jsonValue));\n }\n\n /**\n */\n async postRegistration(requestParameters: PostRegistrationRequest): Promise {\n const response = await this.postRegistrationRaw(requestParameters);\n return await response.value();\n }\n\n /**\n */\n async postRegistrationPriceRaw(requestParameters: PostRegistrationPriceRequest): Promise> {\n const queryParameters: any = {};\n\n if (requestParameters.ids) {\n queryParameters['ids'] = requestParameters.ids;\n }\n\n const headerParameters: runtime.HTTPHeaders = {};\n\n headerParameters['Content-Type'] = 'application/json';\n\n if (this.configuration && this.configuration.apiKey) {\n headerParameters[\"Authorization\"] = this.configuration.apiKey(\"Authorization\"); // JWT authentication\n }\n\n const response = await this.request({\n path: `/registration-price`,\n method: 'POST',\n headers: headerParameters,\n query: queryParameters,\n body: requestParameters.requestBody,\n });\n\n return new runtime.JSONApiResponse(response, (jsonValue) => RegistrationPriceNumberFromJSON(jsonValue));\n }\n\n /**\n */\n async postRegistrationPrice(requestParameters: PostRegistrationPriceRequest): Promise {\n const response = await this.postRegistrationPriceRaw(requestParameters);\n return await response.value();\n }\n\n}\n","import Vue, { ComponentOptions } from 'vue';\nimport { NavigationGuard } from 'vue-router';\nimport { SetupContext, getCurrentInstance } from '@vue/composition-api';\n\nexport const filterUndefineds = (xs: Array): T[] =>\n xs.filter((x) => x !== undefined) as T[];\n\nexport const translate = (\n context: SetupContext,\n i18nKey: string,\n params?: { [key: string]: string }\n): string => {\n if (context.parent && context.parent.$t) {\n const translation = context.parent.$t(i18nKey, params);\n\n if (translation) {\n return translation.toString();\n }\n }\n return i18nKey;\n};\n\n// This and onBeforeRouteLeave are workarounds for vue2 composition api to support route guards inside setup\n// https://github.com/vuejs/composition-api/issues/49\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst onHook = (name: keyof ComponentOptions, callback: (...args: any) => void) => {\n const vm = getCurrentInstance();\n const merge = Vue.config.optionMergeStrategies[name];\n\n if (vm && merge) {\n const prototype = Object.getPrototypeOf(vm.proxy.$options);\n\n if (prototype[name]) {\n delete prototype[name];\n }\n\n prototype[name] = merge(vm.proxy.$options[name], callback);\n }\n};\n\nexport const onBeforeRouteLeave = (callback: NavigationGuard): void => {\n return onHook('beforeRouteLeave', callback);\n};\n\nexport const handleWindowUnload = (e: BeforeUnloadEvent): void => {\n e.preventDefault();\n e.returnValue = '';\n};\n","/**\n * Hypertext Transfer Protocol (HTTP) response status codes.\n * https://gist.githubusercontent.com/scokmen/f813c904ef79022e84ab2409574d1b45/\n * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}\n */\nenum HttpStatusCode {\n /**\n * The server has received the request headers and the client should proceed to send the request body\n * (in the case of a request for which a body needs to be sent; for example, a POST request).\n * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient.\n * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request\n * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued.\n */\n CONTINUE = 100,\n\n /**\n * The requester has asked the server to switch protocols and the server has agreed to do so.\n */\n SWITCHING_PROTOCOLS = 101,\n\n /**\n * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request.\n * This code indicates that the server has received and is processing the request, but no response is available yet.\n * This prevents the client from timing out and assuming the request was lost.\n */\n PROCESSING = 102,\n\n /**\n * Standard response for successful HTTP requests.\n * The actual response will depend on the request method used.\n * In a GET request, the response will contain an entity corresponding to the requested resource.\n * In a POST request, the response will contain an entity describing or containing the result of the action.\n */\n OK = 200,\n\n /**\n * The request has been fulfilled, resulting in the creation of a new resource.\n */\n CREATED = 201,\n\n /**\n * The request has been accepted for processing, but the processing has not been completed.\n * The request might or might not be eventually acted upon, and may be disallowed when processing occurs.\n */\n ACCEPTED = 202,\n\n /**\n * SINCE HTTP/1.1\n * The server is a transforming proxy that received a 200 OK from its origin,\n * but is returning a modified version of the origin's response.\n */\n NON_AUTHORITATIVE_INFORMATION = 203,\n\n /**\n * The server successfully processed the request and is not returning any content.\n */\n NO_CONTENT = 204,\n\n /**\n * The server successfully processed the request, but is not returning any content.\n * Unlike a 204 response, this response requires that the requester reset the document view.\n */\n RESET_CONTENT = 205,\n\n /**\n * The server is delivering only part of the resource (byte serving) due to a range header sent by the client.\n * The range header is used by HTTP clients to enable resuming of interrupted downloads,\n * or split a download into multiple simultaneous streams.\n */\n PARTIAL_CONTENT = 206,\n\n /**\n * The message body that follows is an XML message and can contain a number of separate response codes,\n * depending on how many sub-requests were made.\n */\n MULTI_STATUS = 207,\n\n /**\n * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response,\n * and are not being included again.\n */\n ALREADY_REPORTED = 208,\n\n /**\n * The server has fulfilled a request for the resource,\n * and the response is a representation of the result of one or more instance-manipulations applied to the current instance.\n */\n IM_USED = 226,\n\n /**\n * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation).\n * For example, this code could be used to present multiple video format options,\n * to list files with different filename extensions, or to suggest word-sense disambiguation.\n */\n MULTIPLE_CHOICES = 300,\n\n /**\n * This and all future requests should be directed to the given URI.\n */\n MOVED_PERMANENTLY = 301,\n\n /**\n * This is an example of industry practice contradicting the standard.\n * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect\n * (the original describing phrase was \"Moved Temporarily\"), but popular browsers implemented 302\n * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307\n * to distinguish between the two behaviours. However, some Web applications and frameworks\n * use the 302 status code as if it were the 303.\n */\n FOUND = 302,\n\n /**\n * SINCE HTTP/1.1\n * The response to the request can be found under another URI using a GET method.\n * When received in response to a POST (or PUT/DELETE), the client should presume that\n * the server has received the data and should issue a redirect with a separate GET message.\n */\n SEE_OTHER = 303,\n\n /**\n * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match.\n * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy.\n */\n NOT_MODIFIED = 304,\n\n /**\n * SINCE HTTP/1.1\n * The requested resource is available only through a proxy, the address for which is provided in the response.\n * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons.\n */\n USE_PROXY = 305,\n\n /**\n * No longer used. Originally meant \"Subsequent requests should use the specified proxy.\"\n */\n SWITCH_PROXY = 306,\n\n /**\n * SINCE HTTP/1.1\n * In this case, the request should be repeated with another URI; however, future requests should still use the original URI.\n * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request.\n * For example, a POST request should be repeated using another POST request.\n */\n TEMPORARY_REDIRECT = 307,\n\n /**\n * The request and all future requests should be repeated using another URI.\n * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change.\n * So, for example, submitting a form to a permanently redirected resource may continue smoothly.\n */\n PERMANENT_REDIRECT = 308,\n\n /**\n * The server cannot or will not process the request due to an apparent client error\n * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing).\n */\n BAD_REQUEST = 400,\n\n /**\n * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet\n * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the\n * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means\n * \"unauthenticated\",i.e. the user does not have the necessary credentials.\n */\n UNAUTHORIZED = 401,\n\n /**\n * Reserved for future use. The original intention was that this code might be used as part of some form of digital\n * cash or micro payment scheme, but that has not happened, and this code is not usually used.\n * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests.\n */\n PAYMENT_REQUIRED = 402,\n\n /**\n * The request was valid, but the server is refusing action.\n * The user might not have the necessary permissions for a resource.\n */\n FORBIDDEN = 403,\n\n /**\n * The requested resource could not be found but may be available in the future.\n * Subsequent requests by the client are permissible.\n */\n NOT_FOUND = 404,\n\n /**\n * A request method is not supported for the requested resource;\n * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource.\n */\n METHOD_NOT_ALLOWED = 405,\n\n /**\n * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request.\n */\n NOT_ACCEPTABLE = 406,\n\n /**\n * The client must first authenticate itself with the proxy.\n */\n PROXY_AUTHENTICATION_REQUIRED = 407,\n\n /**\n * The server timed out waiting for the request.\n * According to HTTP specifications:\n * \"The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time.\"\n */\n REQUEST_TIMEOUT = 408,\n\n /**\n * Indicates that the request could not be processed because of conflict in the request,\n * such as an edit conflict between multiple simultaneous updates.\n */\n CONFLICT = 409,\n\n /**\n * Indicates that the resource requested is no longer available and will not be available again.\n * This should be used when a resource has been intentionally removed and the resource should be purged.\n * Upon receiving a 410 status code, the client should not request the resource in the future.\n * Clients such as search engines should remove the resource from their indices.\n * Most use cases do not require clients and search engines to purge the resource, and a \"404 Not Found\" may be used instead.\n */\n GONE = 410,\n\n /**\n * The request did not specify the length of its content, which is required by the requested resource.\n */\n LENGTH_REQUIRED = 411,\n\n /**\n * The server does not meet one of the preconditions that the requester put on the request.\n */\n PRECONDITION_FAILED = 412,\n\n /**\n * The request is larger than the server is willing or able to process. Previously called \"Request Entity Too Large\".\n */\n PAYLOAD_TOO_LARGE = 413,\n\n /**\n * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request,\n * in which case it should be converted to a POST request.\n * Called \"Request-URI Too Long\" previously.\n */\n URI_TOO_LONG = 414,\n\n /**\n * The request entity has a media type which the server or resource does not support.\n * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format.\n */\n UNSUPPORTED_MEDIA_TYPE = 415,\n\n /**\n * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion.\n * For example, if the client asked for a part of the file that lies beyond the end of the file.\n * Called \"Requested Range Not Satisfiable\" previously.\n */\n RANGE_NOT_SATISFIABLE = 416,\n\n /**\n * The server cannot meet the requirements of the Expect request-header field.\n */\n EXPECTATION_FAILED = 417,\n\n /**\n * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol,\n * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by\n * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com.\n */\n I_AM_A_TEAPOT = 418,\n\n /**\n * The request was directed at a server that is not able to produce a response (for example because a connection reuse).\n */\n MISDIRECTED_REQUEST = 421,\n\n /**\n * The request was well-formed but was unable to be followed due to semantic errors.\n */\n UNPROCESSABLE_ENTITY = 422,\n\n /**\n * The resource that is being accessed is locked.\n */\n LOCKED = 423,\n\n /**\n * The request failed due to failure of a previous request (e.g., a PROPPATCH).\n */\n FAILED_DEPENDENCY = 424,\n\n /**\n * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field.\n */\n UPGRADE_REQUIRED = 426,\n\n /**\n * The origin server requires the request to be conditional.\n * Intended to prevent \"the 'lost update' problem, where a client\n * GETs a resource's state, modifies it, and PUTs it back to the server,\n * when meanwhile a third party has modified the state on the server, leading to a conflict.\"\n */\n PRECONDITION_REQUIRED = 428,\n\n /**\n * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes.\n */\n TOO_MANY_REQUESTS = 429,\n\n /**\n * The server is unwilling to process the request because either an individual header field,\n * or all the header fields collectively, are too large.\n */\n REQUEST_HEADER_FIELDS_TOO_LARGE = 431,\n\n /**\n * A server operator has received a legal demand to deny access to a resource or to a set of resources\n * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451.\n */\n UNAVAILABLE_FOR_LEGAL_REASONS = 451,\n\n /**\n * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable.\n */\n INTERNAL_SERVER_ERROR = 500,\n\n /**\n * The server either does not recognize the request method, or it lacks the ability to fulfill the request.\n * Usually this implies future availability (e.g., a new feature of a web-service API).\n */\n NOT_IMPLEMENTED = 501,\n\n /**\n * The server was acting as a gateway or proxy and received an invalid response from the upstream server.\n */\n BAD_GATEWAY = 502,\n\n /**\n * The server is currently unavailable (because it is overloaded or down for maintenance).\n * Generally, this is a temporary state.\n */\n SERVICE_UNAVAILABLE = 503,\n\n /**\n * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.\n */\n GATEWAY_TIMEOUT = 504,\n\n /**\n * The server does not support the HTTP protocol version used in the request\n */\n HTTP_VERSION_NOT_SUPPORTED = 505,\n\n /**\n * Transparent content negotiation for the request results in a circular reference.\n */\n VARIANT_ALSO_NEGOTIATES = 506,\n\n /**\n * The server is unable to store the representation needed to complete the request.\n */\n INSUFFICIENT_STORAGE = 507,\n\n /**\n * The server detected an infinite loop while processing the request.\n */\n LOOP_DETECTED = 508,\n\n /**\n * Further extensions to the request are required for the server to fulfill it.\n */\n NOT_EXTENDED = 510,\n\n /**\n * The client needs to authenticate to gain network access.\n * Intended for use by intercepting proxies used to control access to the network (e.g., \"captive portals\" used\n * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot).\n */\n NETWORK_AUTHENTICATION_REQUIRED = 511\n}\n\nexport default HttpStatusCode;\n","import { memoize } from 'lodash/fp';\nimport { ref } from '@vue/composition-api';\nimport {\n BrandApi,\n BrandApiInterface,\n Configuration,\n HellewiBrand,\n HellewiPromotion,\n HellewiText\n} from '../api';\nimport { Api, ApiEndpoint, ApiEndpointInitialization, RequestState } from '../utils/api-utils';\nimport HttpStatusCode from '../utils/http-status-codes';\n\nexport const useBrandApi: Api = memoize(() => {\n const api = ref(undefined);\n\n const changeConfiguration = (configuration: Configuration) => {\n api.value = new BrandApi(configuration);\n };\n\n return {\n api,\n changeConfiguration\n };\n});\n\nexport const useGetBrand: ApiEndpoint = memoize(() => {\n const initial = undefined;\n const { api } = useBrandApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n const status = ref();\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // request already ongoing, don't start a new one\n state.value === RequestState.Loading ||\n // don't load again if this is already successfully loaded\n state.value === RequestState.Success\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.getBrand();\n status.value = HttpStatusCode.OK;\n state.value = RequestState.Success;\n } catch (err) {\n response.value = await err.json();\n status.value = err.status;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute,\n status\n };\n});\n\nexport const useGetPromotions: ApiEndpoint = memoize(() => {\n const initial: HellewiPromotion[] = [];\n const { api } = useBrandApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // request already ongoing, don't start a new one\n state.value === RequestState.Loading ||\n // don't load again if this is already successfully loaded\n state.value === RequestState.Success\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.listPromotions();\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n\nexport const useGetHelp: ApiEndpoint = memoize(() => {\n const initial = undefined;\n const { api } = useBrandApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n state.value === RequestState.Loading ||\n state.value === RequestState.Success\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.getHelp();\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n","import { memoize } from 'lodash/fp';\nimport { ref } from '@vue/composition-api';\nimport { CalloutsApi, CalloutsApiInterface, Configuration, HellewiCallout } from '../api';\nimport { Api, ApiEndpoint, ApiEndpointInitialization, RequestState } from '../utils/api-utils';\n\nexport const useCalloutsApi: Api = memoize(() => {\n const api = ref(undefined);\n\n const changeConfiguration = (configuration: Configuration) => {\n api.value = new CalloutsApi(configuration);\n };\n\n return {\n api,\n changeConfiguration\n };\n});\n\nexport const useGetLocalCallouts: ApiEndpoint = memoize(() => {\n const initial: HellewiCallout[] = [];\n const { api } = useCalloutsApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // request already ongoing, don't start a new one\n state.value === RequestState.Loading ||\n // don't load again if this is already successfully loaded\n state.value === RequestState.Success\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.getLocalCallouts();\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n","import { memoize } from 'lodash/fp';\nimport { ref } from '@vue/composition-api';\nimport {\n HellewiCartItemId,\n CartApi,\n CartApiInterface,\n Configuration,\n DeleteCartItemRequest,\n HellewiCartItem,\n HellewiCartStatus\n} from '../api';\nimport {\n Api,\n ApiEndPointWithSetter,\n ApiEndpoint,\n ApiEndpointInitialization,\n RequestState\n} from '../utils/api-utils';\n\ninterface HellewiCartStatusExtended extends HellewiCartStatus {\n manuallyCleared?: boolean;\n}\n\nexport const useCartApi: Api = memoize(() => {\n const api = ref(undefined);\n\n const changeConfiguration = (configuration: Configuration) => {\n api.value = new CartApi(configuration);\n };\n\n return {\n api,\n changeConfiguration\n };\n});\n\nexport const useCartItems: ApiEndPointWithSetter = memoize(\n () => {\n const initial = undefined;\n const { api } = useCartApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const setResponse = (cartItems: HellewiCartItem[] | undefined) => {\n response.value = cartItems;\n };\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // request already ongoing, don't start a new one\n state.value === RequestState.Loading\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.listCartItems();\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute,\n setResponse\n };\n }\n);\n\nexport const useCartStatus: ApiEndPointWithSetter<\n void,\n HellewiCartStatusExtended | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = useCartApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n state.value === RequestState.Loading\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n const res = await api.value.getCartStatus();\n response.value = { ...res, manuallyCleared: false };\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n const setResponse = (res: HellewiCartStatusExtended | undefined) => {\n response.value = res;\n state.value = RequestState.Success;\n };\n\n return {\n initial,\n state,\n response,\n setResponse,\n execute\n };\n});\n\nexport const useAddToCart: ApiEndpoint<\n HellewiCartItemId[],\n HellewiCartStatusExtended | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = useCartApi();\n const state = ref(RequestState.Uninitialized);\n const { response } = useCartStatus();\n const errorMessage = ref();\n\n ApiEndpointInitialization(api, state, response, response.value);\n\n const execute = async (req: HellewiCartItemId[]) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n state.value === RequestState.Loading\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.addCartItem({ hellewiCartItemId: req });\n errorMessage.value = undefined;\n state.value = RequestState.Success;\n } catch (err) {\n if (typeof err === 'object') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const e = err as any;\n const res = await e.json();\n errorMessage.value = res.message;\n }\n\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n errorMessage,\n execute\n };\n});\n\nexport const useDeleteFromCart: ApiEndpoint<\n DeleteCartItemRequest,\n HellewiCartStatusExtended | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = useCartApi();\n const state = ref(RequestState.Uninitialized);\n const { response } = useCartStatus();\n\n ApiEndpointInitialization(api, state, response, response.value);\n\n const execute = async (req: DeleteCartItemRequest) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n state.value === RequestState.Loading\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n const res = await api.value.deleteCartItem(req);\n response.value = { ...res, manuallyCleared: true };\n state.value = RequestState.Success;\n } catch {\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n","import { isEqual, memoize } from 'lodash/fp';\nimport PCancelable from 'p-cancelable';\nimport { ref } from '@vue/composition-api';\nimport {\n CatalogApi,\n CatalogApiInterface,\n Configuration,\n GetCatalogRequest,\n HellewiCatalog,\n HellewiCatalogSettings\n} from '../api';\nimport { Api, ApiEndpoint, ApiEndpointInitialization, RequestState } from '../utils/api-utils';\n\nexport const useCatalogApi: Api = memoize(() => {\n const api = ref(undefined);\n\n const changeConfiguration = (configuration: Configuration) => {\n api.value = new CatalogApi(configuration);\n };\n\n return {\n api,\n changeConfiguration\n };\n});\n\nexport const useGetCatalogUnfiltered: ApiEndpoint = memoize(\n () => {\n const initial = undefined;\n const { api } = useCatalogApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n state.value === RequestState.Loading ||\n state.value === RequestState.Success\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.getCatalog({});\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n }\n);\n\nexport const useGetCatalog: ApiEndpoint<\n GetCatalogRequest | undefined,\n HellewiCatalog | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = useCatalogApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n const currentQ = ref(undefined);\n const ongoing = ref | undefined>(undefined);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (q: GetCatalogRequest | undefined) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // don't load again if this is already successfully loaded\n (state.value === RequestState.Success && isEqual(currentQ.value, q))\n ) {\n return;\n } else if (ongoing.value) {\n // cancel the previous ongoing load\n ongoing.value.cancel();\n }\n\n ongoing.value = new PCancelable(async (resolve, reject, onCancel) => {\n onCancel(() => reject('cancelled'));\n try {\n if (!api.value) {\n return;\n }\n const catalog = await api.value.getCatalog(q || {});\n resolve(catalog);\n } catch {\n reject();\n }\n });\n\n try {\n state.value = RequestState.Loading;\n response.value = await ongoing.value;\n currentQ.value = q;\n state.value = RequestState.Success;\n } catch (err) {\n if (err !== 'cancelled') {\n state.value = RequestState.Error;\n }\n // if this request was cancelled, don't touch the request state as the cancelling\n // request will handle the situation\n }\n ongoing.value = undefined;\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n\nexport const useCatalogSettings: ApiEndpoint = memoize(\n () => {\n const initial = undefined;\n const { api } = useCatalogApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // request already ongoing, don't start a new one\n state.value === RequestState.Loading ||\n // don't load again if this is already successfully loaded\n state.value === RequestState.Success\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.getCatalogSettings();\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n }\n);\n","import { find, includes, isEmpty, isEqual, map, memoize } from 'lodash/fp';\nimport PCancelable from 'p-cancelable';\nimport { ref } from '@vue/composition-api';\n\nimport {\n Configuration,\n CourseApi,\n CourseApiInterface,\n GetCourseRequest,\n HellewiCourse,\n HellewiCourseCount,\n HellewiCoursePartial,\n HellewiCourseStatus,\n HellewiParticipantCount,\n ListCoursesRequest\n} from '../api';\nimport { Api, ApiEndpoint, ApiEndpointInitialization, RequestState } from '../utils/api-utils';\n\nexport const useCourseApi: Api = memoize(() => {\n const api = ref(undefined);\n\n const changeConfiguration = (configuration: Configuration) => {\n api.value = new CourseApi(configuration);\n };\n\n return {\n api,\n changeConfiguration\n };\n});\n\nexport const useGetCourseCount: ApiEndpoint = memoize(() => {\n const initial = undefined;\n const { api } = useCourseApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // request already ongoing, don't start a new one\n state.value === RequestState.Loading ||\n // don't load again if this is already successfully loaded\n state.value === RequestState.Success\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.getCourseCount({});\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n\nexport const useGetCourse: ApiEndpoint = memoize(\n () => {\n const initial = undefined;\n const { api } = useCourseApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (requestParams: GetCourseRequest) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // course already loaded successfully, don't load again\n // registration to lessons -course data is reloaded always as the\n // lesson participant counts are fetched with this request\n (state.value === RequestState.Success &&\n response.value?.id === requestParams.id &&\n !includes(HellewiCourseStatus.RegistrationToLessons, response.value?.statuses))\n ) {\n return;\n }\n\n try {\n response.value = initial;\n state.value = RequestState.Loading;\n response.value = await api.value.getCourse(requestParams);\n state.value = RequestState.Success;\n } catch {\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n }\n);\n\nexport interface ListCoursesResponseCourse extends HellewiCoursePartial {\n participantcount?: HellewiParticipantCount;\n}\n\nexport interface ListCoursesResponse {\n count: number;\n courses: ListCoursesResponseCourse[];\n}\n\nexport const useListCourses: ApiEndpoint = memoize(() => {\n const initial: ListCoursesResponse = { count: 0, courses: [] };\n const { api } = useCourseApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n const currentParams = ref(undefined);\n const ongoing = ref | undefined>(undefined);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (params: ListCoursesRequest) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // don't load again if this is already successfully loaded\n (state.value === RequestState.Success && isEqual(currentParams.value, params))\n ) {\n return;\n } else if (ongoing.value) {\n // cancel the previous ongoing load\n ongoing.value.cancel();\n }\n\n ongoing.value = new PCancelable(async (resolve, reject, onCancel) => {\n onCancel(() => reject('cancelled'));\n try {\n if (!api.value) {\n return;\n }\n const responseRaw = await api.value.listCoursesRaw(params);\n const partialCourses = await responseRaw.value();\n const ids = partialCourses.map((course) => course.id);\n const participantCounts = isEmpty(ids)\n ? []\n : await api.value.listCourseParticipantCounts({\n ids\n });\n\n const count = Math.min(\n 9996, // Upper limit of the API is currently 10k, limit to full pages\n parseInt(responseRaw.raw.headers.get('x-total-count') as string, 10)\n );\n\n const courses = map(\n (course) => ({\n ...course,\n participantcount: find((pc) => pc.id === course.id, participantCounts)\n }),\n partialCourses\n );\n\n resolve({ count, courses });\n } catch {\n reject();\n }\n });\n\n try {\n state.value = RequestState.Loading;\n response.value = initial;\n response.value = await ongoing.value;\n currentParams.value = params;\n state.value = RequestState.Success;\n } catch (err) {\n if (err !== 'cancelled') {\n state.value = RequestState.Error;\n }\n // if this request was cancelled, don't touch the request state as the cancelling\n // request will handle the situation\n }\n ongoing.value = undefined;\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n","import { memoize } from 'lodash/fp';\nimport { ref } from '@vue/composition-api';\n\nimport {\n Configuration,\n GetPaymentTokenRequest,\n HellewiMyRegistrationsResponse,\n HellewiMyRegistrationsResponseFromJSON,\n HTTPQuery,\n JSONApiResponse,\n MethodOfPayment,\n PaymentApi,\n PaymentApiInterface\n} from '../api';\nimport { Api, ApiEndpoint, ApiEndpointInitialization, RequestState } from '../utils/api-utils';\n\nexport interface HellewiPaymentApiInterface extends PaymentApiInterface {\n getPaymentRequest(req: GetPaymentRequest): Promise;\n}\n\nexport class HellewiPaymentApi extends PaymentApi implements HellewiPaymentApiInterface {\n public async getPaymentRequest(req: GetPaymentRequest): Promise {\n const response = await this.request({\n headers: {},\n method: 'GET',\n path: `/payment/${req.paymentmethod}/${req.status}`,\n query: req.query\n });\n\n return new JSONApiResponse(response, (jsonValue) =>\n HellewiMyRegistrationsResponseFromJSON(jsonValue)\n ).value();\n }\n}\n\nexport const usePaymentApi: Api = memoize(() => {\n const api = ref(undefined);\n\n const changeConfiguration = (configuration: Configuration) => {\n api.value = new HellewiPaymentApi(configuration);\n };\n\n return {\n api,\n changeConfiguration\n };\n});\n\nexport interface GetPaymentRequest {\n paymentmethod: string;\n status: string;\n query: HTTPQuery;\n}\n\nexport const useGetPaymentRequest: ApiEndpoint<\n GetPaymentRequest,\n HellewiMyRegistrationsResponse | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = usePaymentApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (req: GetPaymentRequest) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized\n // here it doesn't matter if there already is an ongoing request,\n // just make a new one if function is called (don't even cancel the old one)\n // The only way to make these requests is with Payment-component's\n // onBeforeMount, so there should only ever be one of these\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.getPaymentRequest(req);\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n\nexport const useGetPaymentToken: ApiEndpoint<\n GetPaymentTokenRequest,\n MethodOfPayment | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = usePaymentApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n const errorMessage = ref();\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (req: GetPaymentTokenRequest) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n // request already ongoing, don't start a new one\n state.value === RequestState.Loading\n ) {\n return;\n }\n try {\n state.value = RequestState.Loading;\n response.value = await api.value.getPaymentToken(req);\n state.value = RequestState.Success;\n } catch (err) {\n if (typeof err === 'object') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const e = err as any;\n const res = await e.json();\n errorMessage.value = res.message;\n }\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n errorMessage,\n state,\n response,\n execute\n };\n});\n","import { isEqual, memoize } from 'lodash/fp';\nimport PCancelable from 'p-cancelable';\nimport { ref } from '@vue/composition-api';\nimport {\n Configuration,\n GetRegistrationFormRequest,\n GetRegistrationRequest,\n GetMyRegistrationsRequest,\n HellewiGetRegistrationResponse,\n HellewiPostRegistrationResponse,\n HellewiMyRegistrationsResponse,\n PostMyRegistrationsLoginLinkRequest,\n RegistrationApi,\n RegistrationApiInterface,\n RegistrationPriceNumber,\n PostHellewiRegistrationRequest,\n HellewiCourseProduct\n} from '../api';\nimport { Api, ApiEndpoint, ApiEndpointInitialization, RequestState } from '../utils/api-utils';\nimport HttpStatusCode from '../utils/http-status-codes';\n\nexport const useRegistrationApi: Api = memoize(() => {\n const api = ref(undefined);\n\n const changeConfiguration = (configuration: Configuration) => {\n api.value = new RegistrationApi(configuration);\n };\n\n return {\n api,\n changeConfiguration\n };\n});\n\nexport const useGetRegistrationForm: ApiEndpoint<\n GetRegistrationFormRequest,\n Record | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = useRegistrationApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref | undefined>(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async () => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n state.value === RequestState.Loading\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n response.value = JSON.parse(await api.value.getRegistrationForm({}));\n state.value = RequestState.Success;\n } catch {\n response.value = initial;\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n\nexport const useGetRegistration: ApiEndpoint<\n GetRegistrationRequest,\n HellewiGetRegistrationResponse | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = useRegistrationApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n const ongoing = ref | undefined>(undefined);\n const currentParams = ref(undefined);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (params: GetRegistrationRequest) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n (state.value === RequestState.Loading && isEqual(currentParams.value, params)) ||\n (state.value === RequestState.Success && isEqual(currentParams.value, params))\n ) {\n return;\n } else if (ongoing.value) {\n ongoing.value.cancel();\n }\n\n ongoing.value = new PCancelable(async (resolve, reject, onCancel) => {\n onCancel(() => reject('cancelled'));\n try {\n if (!api.value) {\n return;\n }\n\n const res = await api.value.getRegistration(params);\n\n resolve(res);\n } catch {\n reject();\n }\n });\n\n try {\n state.value = RequestState.Loading;\n currentParams.value = params;\n response.value = await ongoing.value;\n state.value = RequestState.Success;\n } catch (err) {\n if (err !== 'cancelled') {\n state.value = RequestState.Error;\n }\n }\n ongoing.value = undefined;\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n\nexport const usePostRegistration: ApiEndpoint<\n PostHellewiRegistrationRequest,\n HellewiPostRegistrationResponse | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = useRegistrationApi();\n const state = ref(RequestState.Uninitialized);\n const status = ref();\n const response = ref(initial);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (params: PostHellewiRegistrationRequest) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n state.value === RequestState.Loading\n ) {\n return;\n }\n\n try {\n state.value = RequestState.Loading;\n const responseRaw = await api.value.postRegistrationRaw({\n requestBody: (params as unknown) as { [key: string]: Record }\n });\n response.value = await responseRaw.value();\n state.value = RequestState.Success;\n status.value = HttpStatusCode.OK;\n } catch (err) {\n if (typeof err === 'object') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const e = err as any;\n status.value = e?.status;\n response.value = await e?.json();\n }\n\n state.value = RequestState.Error;\n }\n };\n\n return {\n initial,\n state,\n response,\n execute,\n status\n };\n});\n\nexport interface Registration {\n course: {\n id: number;\n price?: {\n id: number;\n installments?: number[];\n };\n lessonid?: number;\n spare?: boolean;\n courseProducts?: HellewiCourseProduct[];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [fieldname: string]: any;\n };\n client?: Record;\n id: number;\n clientid: string;\n}\n\nexport interface PostRegistrationPriceRequest {\n registrations: Registration[];\n discountcode?: string;\n}\n\nexport const usePostRegistrationPrice: ApiEndpoint<\n PostRegistrationPriceRequest,\n RegistrationPriceNumber | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = useRegistrationApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n const ongoing = ref | undefined>(undefined);\n const currentParams = ref(undefined);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (params: PostRegistrationPriceRequest) => {\n const paramsChanged = !isEqual(currentParams.value, params);\n\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n ((state.value === RequestState.Loading || state.value === RequestState.Success) &&\n !paramsChanged)\n ) {\n return;\n } else if (ongoing.value && paramsChanged) {\n ongoing.value.cancel();\n }\n\n ongoing.value = new PCancelable(async (resolve, reject, onCancel) => {\n onCancel(() => reject('cancelled'));\n try {\n if (!api.value) {\n return;\n }\n\n const res = await api.value.postRegistrationPrice({\n requestBody: (params as unknown) as { [key: string]: Record }\n });\n\n resolve(res);\n } catch {\n reject();\n }\n });\n\n try {\n state.value = RequestState.Loading;\n currentParams.value = params;\n response.value = await ongoing.value;\n state.value = RequestState.Success;\n } catch (err) {\n if (err !== 'cancelled') {\n state.value = RequestState.Error;\n }\n }\n ongoing.value = undefined;\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n\nexport const usePostMyRegistrationsLoginLink: ApiEndpoint<\n PostMyRegistrationsLoginLinkRequest,\n void\n> = memoize(() => {\n const initial = undefined;\n const { api } = useRegistrationApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n const ongoing = ref | undefined>(undefined);\n const currentParams = ref(undefined);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (params: PostMyRegistrationsLoginLinkRequest) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n (state.value === RequestState.Loading && isEqual(currentParams.value, params))\n ) {\n return;\n } else if (ongoing.value) {\n ongoing.value.cancel();\n }\n\n ongoing.value = new PCancelable(async (resolve, reject, onCancel) => {\n onCancel(() => reject('cancelled'));\n try {\n if (!api.value) {\n return;\n }\n\n const res = await api.value.postMyRegistrationsLoginLink(params);\n\n resolve(res);\n } catch {\n reject();\n }\n });\n\n try {\n state.value = RequestState.Loading;\n await ongoing.value;\n currentParams.value = params;\n state.value = RequestState.Success;\n } catch (err) {\n if (err !== 'cancelled') {\n state.value = RequestState.Error;\n }\n }\n ongoing.value = undefined;\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n\nexport const useGetMyRegistrations: ApiEndpoint<\n GetMyRegistrationsRequest,\n HellewiMyRegistrationsResponse | undefined\n> = memoize(() => {\n const initial = undefined;\n const { api } = useRegistrationApi();\n const state = ref(RequestState.Uninitialized);\n const response = ref(initial);\n const ongoing = ref | undefined>(undefined);\n const currentParams = ref(undefined);\n\n ApiEndpointInitialization(api, state, response, initial);\n\n const execute = async (params: GetMyRegistrationsRequest) => {\n if (\n !api.value ||\n state.value === RequestState.Uninitialized ||\n (state.value === RequestState.Loading && isEqual(currentParams.value, params)) ||\n (state.value === RequestState.Success && isEqual(currentParams.value, params))\n ) {\n return;\n } else if (ongoing.value) {\n ongoing.value.cancel();\n }\n\n ongoing.value = new PCancelable(async (resolve, reject, onCancel) => {\n onCancel(() => reject('cancelled'));\n try {\n if (!api.value) {\n return;\n }\n\n const res = await api.value.getMyRegistrations(params);\n\n resolve(res);\n } catch {\n reject();\n }\n });\n\n try {\n state.value = RequestState.Loading;\n currentParams.value = params;\n response.value = await ongoing.value;\n state.value = RequestState.Success;\n } catch (err) {\n if (err !== 'cancelled') {\n state.value = RequestState.Error;\n }\n }\n ongoing.value = undefined;\n };\n\n return {\n initial,\n state,\n response,\n execute\n };\n});\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.brand)?_c('div',{staticClass:\"header-container\"},[_c('b-navbar',{staticClass:\"header header-desktop\",attrs:{\"wrapper-class\":\"container\"}},[_c('template',{slot:\"brand\"},[_c('b-navbar-item',{attrs:{\"tag\":\"router-link\",\"to\":{ name: 'home' }}},[_c('h1',{staticClass:\"nav-title\"},[_vm._v(\" \"+_vm._s(_vm.brand.name)+\" \")])])],1),_c('template',{slot:\"end\"},[_c('b-navbar-item',{attrs:{\"tag\":\"router-link\",\"to\":{ name: 'home' }}},[_vm._v(\" \"+_vm._s(_vm.$t('nav.courses'))+\" \")]),_c('b-navbar-item',{attrs:{\"tag\":\"router-link\",\"to\":{ name: 'my-registrations-login-link' }}},[_vm._v(\" \"+_vm._s(_vm.$t('nav.myregistrations'))+\" \")]),_c('b-navbar-item',{attrs:{\"tag\":\"router-link\",\"to\":{ name: 'help' }}},[_vm._v(\" \"+_vm._s(_vm.$t('nav.help'))+\" \")]),_c('b-navbar-dropdown',{scopedSlots:_vm._u([{key:\"label\",fn:function(){return [_vm._v(\" \"+_vm._s(_vm.$t('nav.language'))),_c('b-icon',{staticStyle:{\"margin-left\":\"0.3rem\"},attrs:{\"icon\":\"earth\"}})]},proxy:true}],null,false,384051627)},[_c('b-navbar-item',{attrs:{\"aria-role\":\"listitem\",\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.setLanguage('fi')}}},[_vm._v(\" Suomeksi \")]),_c('b-navbar-item',{attrs:{\"aria-role\":\"listitem\",\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.setLanguage('sv')}}},[_vm._v(\" På Svenska \")]),_c('b-navbar-item',{attrs:{\"aria-role\":\"listitem\",\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.setLanguage('en')}}},[_vm._v(\" In English \")])],1),_c('b-navbar-item',{attrs:{\"tag\":\"router-link\",\"to\":{ name: 'cart' }}},[_c('span',[_vm._v(_vm._s(_vm.$t('nav.cart')))]),_c('div',{staticClass:\"cartLabel\"},[_c('b-tooltip',{attrs:{\"label\":_vm.$tc('nav.tooltip', _vm.minutesLeft),\"position\":\"is-bottom\",\"size\":\"is-small\",\"multilined\":\"\",\"active\":_vm.minutesLeft >= 0}},[_c('div',{staticClass:\"cart-icon\"},[_c('b-icon',{attrs:{\"icon\":\"cart\"}}),(_vm.cartStatus && _vm.cartStatus.count > 0)?_c('span',{staticClass:\"cartCount\"},[_vm._v(\" \"+_vm._s(_vm.cartStatus.count)+\" \")]):_vm._e(),(_vm.minutesLeft === 0)?_c('span',{staticClass:\"timeWarning\"},[_vm._v(\"<1 min\")]):_vm._e(),(_vm.minutesLeft > 0)?_c('span',{staticClass:\"timeWarning\"},[_vm._v(_vm._s(_vm.minutesLeft)+\" min\")]):_vm._e()],1)])],1)])],1)],2),_c('nav',{staticClass:\"header header-mobile\"},[_c('div',{attrs:{\"aria-controls\":\"nav-items\"},on:{\"click\":function($event){_vm.isOpen = !_vm.isOpen}}},[(_vm.isOpen)?_c('b-icon',{staticClass:\"nav-item\",attrs:{\"icon\":\"close\"}}):_c('b-icon',{staticClass:\"nav-item\",attrs:{\"icon\":\"menu\"}})],1),_c('b-navbar-item',{attrs:{\"tag\":\"router-link\",\"to\":{ name: 'home' }}},[_c('h1',{staticClass:\"nav-title\"},[_vm._v(\" \"+_vm._s(_vm.brand.name)+\" \")])]),_c('b-navbar-item',{staticClass:\"nav-item cart\",attrs:{\"tag\":\"router-link\",\"to\":{ name: 'cart' }}},[_c('div',{staticClass:\"cart-icon\"},[_c('b-icon',{attrs:{\"icon\":\"cart\"}}),(_vm.cartStatus && _vm.cartStatus.count > 0)?_c('span',{staticClass:\"cartCount\"},[_vm._v(\" \"+_vm._s(_vm.cartStatus.count)+\" \")]):_vm._e()],1),_c('div',{staticClass:\"cartLabel\"},[_c('span',{staticClass:\"cartText\"},[_vm._v(_vm._s(_vm.$t('nav.cart')))]),(_vm.minutesLeft === 0)?_c('span',{staticClass:\"timeWarning\"},[_vm._v(\"<1 min\")]):_vm._e(),(_vm.minutesLeft > 0)?_c('span',{staticClass:\"timeWarning\"},[_vm._v(_vm._s(_vm.minutesLeft)+\" min\")]):_vm._e()])]),_c('b-collapse',{attrs:{\"aria-id\":\"nav-items\"},model:{value:(_vm.isOpen),callback:function ($$v) {_vm.isOpen=$$v},expression:\"isOpen\"}},[_c('div',{staticClass:\"content collapse-content\"},[_c('b-navbar-item',{staticClass:\"nav-item\",attrs:{\"tag\":\"router-link\",\"to\":{ name: 'home' }},nativeOn:{\"click\":function($event){_vm.isOpen = !_vm.isOpen}}},[_vm._v(\" \"+_vm._s(_vm.$t('nav.courses'))+\" \")]),_c('b-navbar-item',{staticClass:\"nav-item\",attrs:{\"tag\":\"router-link\",\"to\":{ name: 'my-registrations-login-link' }},nativeOn:{\"click\":function($event){_vm.isOpen = !_vm.isOpen}}},[_vm._v(\" \"+_vm._s(_vm.$t('nav.myregistrations'))+\" \")]),_c('b-navbar-item',{staticClass:\"nav-item\",attrs:{\"tag\":\"router-link\",\"to\":{ name: 'help' }},nativeOn:{\"click\":function($event){_vm.isOpen = !_vm.isOpen}}},[_vm._v(\" \"+_vm._s(_vm.$t('nav.help'))+\" \")]),_c('b-navbar-item',{staticClass:\"nav-item\"},[_c('b-dropdown',{attrs:{\"aria-role\":\"list\"},scopedSlots:_vm._u([{key:\"trigger\",fn:function(){return [_c('div',{staticClass:\"dropdown-btn\"},[_vm._v(\" \"+_vm._s(_vm.$t('nav.language'))+\" \"),_c('b-icon',{staticStyle:{\"margin-left\":\"0.3rem\"},attrs:{\"icon\":\"earth\"}})],1)]},proxy:true}],null,false,614839335)},[_c('b-dropdown-item',{attrs:{\"aria-role\":\"listitem\",\"href\":\"#\"},on:{\"click\":function($event){return _vm.setLanguage('fi')}}},[_vm._v(\" Suomeksi \")]),_c('b-dropdown-item',{attrs:{\"aria-role\":\"listitem\",\"href\":\"#\"},on:{\"click\":function($event){return _vm.setLanguage('sv')}}},[_vm._v(\" På Svenska \")]),_c('b-dropdown-item',{attrs:{\"aria-role\":\"listitem\",\"href\":\"#\"},on:{\"click\":function($event){return _vm.setLanguage('en')}}},[_vm._v(\" In English \")])],1)],1)],1)])],1)],1):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Header.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Header.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Header.vue?vue&type=template&id=53200013&scoped=true\"\nimport script from \"./Header.vue?vue&type=script&lang=ts\"\nexport * from \"./Header.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Header.vue?vue&type=style&index=0&id=53200013&prod&scoped=true&lang=scss\"\nimport style1 from \"./Header.vue?vue&type=style&index=1&id=53200013&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"53200013\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.callouts.length)?_c('div',{staticClass:\"container callouts\"},_vm._l((_vm.callouts),function(callout){return _c('div',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(callout.text),expression:\"callout.text\"}],key:callout.text,staticClass:\"notification is-primary is-light callout\"},[_vm._v(\" \"+_vm._s(callout.text)+\" \")])}),0):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Callouts.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Callouts.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Callouts.vue?vue&type=template&id=4e52dc6f&scoped=true\"\nimport script from \"./Callouts.vue?vue&type=script&lang=ts\"\nexport * from \"./Callouts.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Callouts.vue?vue&type=style&index=0&id=4e52dc6f&prod&scoped=true&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"4e52dc6f\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.brand)?_c('footer',{staticClass:\"footer-container\"},[_c('div',{staticClass:\"container flex-container\"},[_c('div',{staticClass:\"info-container\"},[_c('p',{staticClass:\"block-small\"},[_c('strong',[_vm._v(_vm._s(_vm.brand.name))])]),(_vm.brand.location)?_c('p',{staticClass:\"block-small\"},[_vm._v(\" \"+_vm._s(_vm.brand.location.address)),_c('br'),_vm._v(_vm._s(_vm.brand.location.postalcode)+\" \"+_vm._s(_vm.brand.location.city)+\" \")]):_vm._e(),(_vm.phoneLink)?_c('p',{staticClass:\"block-small\"},[_c('a',{attrs:{\"href\":_vm.phoneLink}},[_vm._v(_vm._s(_vm.brand.phone))])]):(_vm.brand.phone)?_c('p',{staticClass:\"block-small\"},[_vm._v(_vm._s(_vm.brand.phone))]):_vm._e(),(_vm.brand.email)?_c('p',{staticClass:\"block-small\"},[_c('a',{attrs:{\"href\":_vm.emailLink}},[_vm._v(_vm._s(_vm.brand.email))])]):_vm._e(),(_vm.brand.homepage)?_c('p',{staticClass:\"block-small\"},[_c('a',{attrs:{\"href\":_vm.addHttpsToUrl(_vm.brand.homepage)}},[_vm._v(_vm._s(_vm.brand.homepage))])]):_vm._e(),(_vm.brand.accessibilitystatementurl)?_c('p',{staticClass:\"block-small\"},[_c('a',{attrs:{\"href\":_vm.addHttpsToUrl(_vm.brand.accessibilitystatementurl)}},[_vm._v(_vm._s(_vm.$t('nav.accessibilitystatement')))])]):_vm._e(),(_vm.brand.privacystatementurl)?_c('p',{staticClass:\"block-small\"},[_c('a',{attrs:{\"href\":_vm.addHttpsToUrl(_vm.brand.privacystatementurl)}},[_vm._v(_vm._s(_vm.$t('nav.privacystatement')))])]):_vm._e(),(_vm.brand.privacystatement)?_c('router-link',{attrs:{\"to\":{ name: 'help', hash: '#privacystatement' }}},[_vm._v(_vm._s(_vm.$t('privacystatement')))]):_vm._e(),_c('div',{staticClass:\"links block-small\"},[(_vm.brand.facebook)?_c('a',{attrs:{\"href\":_vm.addHttpsToUrl(_vm.brand.facebook)}},[_c('b-icon',{attrs:{\"icon\":\"facebook\"}}),_c('div',{staticClass:\"sr-only\"},[_vm._v(\"Facebook\")])],1):_vm._e(),(_vm.brand.instagram)?_c('a',{attrs:{\"href\":_vm.addHttpsToUrl(_vm.brand.instagram)}},[_c('b-icon',{attrs:{\"icon\":\"instagram\"}}),_c('div',{staticClass:\"sr-only\"},[_vm._v(\"Instagram\")])],1):_vm._e(),(_vm.brand.linkedin)?_c('a',{attrs:{\"href\":_vm.addHttpsToUrl(_vm.brand.linkedin)}},[_c('b-icon',{attrs:{\"icon\":\"linkedin\"}}),_c('div',{staticClass:\"sr-only\"},[_vm._v(\"LinkedIn\")])],1):_vm._e(),(_vm.brand.twitter)?_c('a',{attrs:{\"href\":_vm.addHttpsToUrl(_vm.brand.twitter)}},[_c('b-icon',{attrs:{\"icon\":\"twitter\"}}),_c('div',{staticClass:\"sr-only\"},[_vm._v(\"X\")])],1):_vm._e()])],1),(_vm.brand.logo)?_c('span',{staticClass:\"logo\",style:({ backgroundImage: (\"url(\" + (_vm.brand.logo) + \")\") })}):_vm._e()])]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Footer.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Footer.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Footer.vue?vue&type=template&id=314b1b62&scoped=true\"\nimport script from \"./Footer.vue?vue&type=script&lang=ts\"\nexport * from \"./Footer.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Footer.vue?vue&type=style&index=0&id=314b1b62&prod&scoped=true&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"314b1b62\",\n null\n \n)\n\nexport default component.exports","/* eslint-disable @typescript-eslint/ban-ts-comment */\n/* tslint:disable */\n// @ts-nocheck\n/* eslint-disable */\nconst metaPixel = (f: any, b: any, e: any, v: any, n: any, t: any, s: any) => {\n if (f.fbq) return;\n n = f.fbq = function() {\n n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);\n };\n if (!f._fbq) f._fbq = n;\n n.push = n;\n n.loaded = !0;\n n.version = '2.0';\n n.queue = [];\n t = b.createElement(e);\n t.async = !0;\n t.src = v;\n s = b.getElementsByTagName(e)[0];\n s.parentNode.insertBefore(t, s);\n};\n\nexport const setupMetaPixel = (metaPixelId: string) => {\n metaPixel(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');\n if (metaPixelId) {\n fbq('init', metaPixelId);\n }\n};\n","\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./TenantLanguage.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./TenantLanguage.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./TenantLanguage.vue?vue&type=template&id=28c617ae\"\nimport script from \"./TenantLanguage.vue?vue&type=script&lang=ts\"\nexport * from \"./TenantLanguage.vue?vue&type=script&lang=ts\"\nimport style0 from \"./TenantLanguage.vue?vue&type=style&index=0&id=28c617ae&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[(_vm.cartItems && _vm.cartItems.length > 0 && _vm.schema)?_c('div',[_c('div',{staticClass:\"content\"},[_c('section',{staticClass:\"formSection\"},[_vm._l((_vm.clients),function(client,i){return _c('ClientCard',{key:client.id,attrs:{\"cartItems\":_vm.cartItems,\"schema\":_vm.schema,\"client\":client,\"clientCount\":_vm.clients.length,\"availableCartItems\":_vm.availableCartItems,\"ajvErrors\":_vm.ajvErrors,\"isPayer\":_vm.payerFromClient && i === 0,\"validationTrigger\":_vm.validationTrigger},on:{\"update-prices\":_vm.updateClientPrices,\"update-client\":_vm.updateClient,\"remove-client\":_vm.removeClient,\"validate\":_vm.checkValidation,\"remove-cart-item\":_vm.removeCartItem}})}),_c('section',{staticClass:\"buttonSection desktop-buttons\"},[(_vm.clients.length < _vm.cartItems.length && _vm.availableCartItems.length > 0)?_c('b-button',{attrs:{\"type\":\"is-primary\",\"icon-left\":\"account-plus\"},on:{\"click\":_vm.addClient}},[_vm._v(\" \"+_vm._s(_vm.$t('add.client'))+\" \")]):_vm._e()],1),(_vm.showPayer)?_c('PayerCard',{attrs:{\"client\":_vm.clients[0],\"schema\":_vm.schema,\"ajvErrors\":_vm.ajvErrors,\"validationTrigger\":_vm.validationTrigger,\"payerFromClientInitial\":_vm.payerFromClient},on:{\"update-payer\":_vm.updatePayer,\"update-payer-from-client\":_vm.updatePayerFromClient,\"validate\":_vm.checkValidation}}):_vm._e()],2),_c('section',{staticClass:\"buttonSection mobile-buttons\"},[(_vm.clients.length < _vm.cartItems.length && _vm.availableCartItems.length > 0)?_c('b-button',{attrs:{\"type\":\"is-primary\",\"icon-left\":\"account-plus\"},on:{\"click\":_vm.addClient}},[_vm._v(\" \"+_vm._s(_vm.$t('add.client'))+\" \")]):_vm._e()],1),_c('section',{staticClass:\"summary-section\"},[(_vm.purchase)?_c('div',{staticClass:\"summary\"},[_c('CartSummary',{attrs:{\"cartItems\":_vm.cartItems,\"clients\":_vm.clients,\"purchase\":_vm.purchase,\"postRegIsLoading\":_vm.postRegIsLoading},on:{\"check-discount-code\":_vm.checkDiscountCode,\"send\":_vm.startValidation,\"remove-cart-item\":_vm.removeCartItem}})],1):_vm._e()])])]):_vm._e(),(_vm.cartItems && _vm.cartItems.length === 0 && !_vm.isLoading)?_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h2',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('nav.cart')))]),_c('p',[_vm._v(_vm._s(_vm.$t('cart.empty')))])])]):_vm._e()])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.shownCartItems.length > 0)?_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('header',[_c('h2',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('client.info')))]),(_vm.clientCount > 1 && _vm.selectedItems.length === 0 && _vm.client.id !== '1')?_c('b-icon',{attrs:{\"icon\":\"close\"},nativeOn:{\"click\":function($event){return _vm.removeClient(_vm.client.id)}}}):_vm._e()],1),_c('p',[_vm._v(_vm._s(_vm.$t('client.signupforcourses')))]),_vm._l((_vm.shownCartItems),function(item,itemIndex){return _c('CartItem',{key:item.id,attrs:{\"item\":item,\"item-index\":itemIndex,\"schema\":_vm.schema,\"client\":_vm.client,\"coursedetails\":_vm.client.courses[((item.itemid.id) + \"-\" + (item.itemid.lessonid))],\"selected\":_vm.selectedItems.includes(item.id),\"showCheckbox\":_vm.clientCount > 1 || _vm.cartItems.length > 1,\"ajvErrors\":_vm.ajvErrors},on:{\"toggle-item\":_vm.toggleCartItem,\"change-details\":_vm.updateCourseDetails,\"change-prices\":_vm.updateCoursePrices,\"remove-cart-item\":_vm.removeCartItem}})}),(_vm.schema)?_c('RegistrationForm',{attrs:{\"schema\":_vm.schema,\"client\":_vm.client,\"selectedCourseIds\":_vm.selectedCourseIds,\"isPayer\":_vm.isPayer,\"ajvErrors\":_vm.ajvErrors,\"validationTrigger\":_vm.validationTrigger,\"cartItemAgeLimits\":_vm.cartItemAgeLimits},on:{\"update-client\":_vm.updateClientFields,\"validate\":_vm.passValidationResults}}):_vm._e(),_c('p',[_vm._v(_vm._s(_vm.$t('client.requiredfields')))])],2)]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('article',{staticClass:\"course\",class:{ selectedCourse: _vm.isSelected }},[(_vm.showCheckbox)?_c('div',{staticClass:\"desktop-checkbox\",on:{\"click\":function($event){if($event.target !== $event.currentTarget){ return null; }return _vm.toggleItem($event)}}},[_c('b-checkbox',{attrs:{\"value\":_vm.isSelected,\"aria-label\":_vm.getCourseSelectorLabel(_vm.isSelected, _vm.item.course.name || '')},on:{\"input\":_vm.toggleItem}})],1):_vm._e(),_c('div',{staticClass:\"course-content\"},[_c('div',{staticClass:\"course-mobile-wrapper\"},[(_vm.showCheckbox)?_c('div',{staticClass:\"mobile-checkbox\",on:{\"click\":function($event){if($event.target !== $event.currentTarget){ return null; }return _vm.toggleItem($event)}}},[_c('b-checkbox',{attrs:{\"value\":_vm.isSelected,\"aria-label\":_vm.getCourseSelectorLabel(_vm.isSelected, _vm.item.course.name || '')},on:{\"input\":_vm.toggleItem}})],1):_vm._e(),_c('div',{staticClass:\"courseDetails\"},[_c('div',[_c('router-link',{attrs:{\"to\":{ name: 'course', params: { id: _vm.item.course.id } }}},[_c('h3',{staticClass:\"course-title\"},[_vm._v(\" \"+_vm._s(_vm.item.course.name)+\" \")])]),(_vm.ageLimitString)?_c('span',{staticClass:\"agelimit\"},[_vm._v(\"(\"+_vm._s(_vm.ageLimitString)+\")\")]):_vm._e()],1),_c('div',[_c('span',{staticClass:\"courseCode\"},[_vm._v(_vm._s(_vm.item.course.code))]),(_vm.item.lesson)?_c('div',[_vm._v(\" \"+_vm._s(_vm.$d(new Date(_vm.item.lesson.begins), 'weekdayLong'))+\" \"+_vm._s(_vm._f(\"dateTimeRange\")(_vm.item.lesson.begins,_vm.item.lesson.ends))+\" \")]):_vm._e(),_c('RegistrationStatus',{attrs:{\"registration\":_vm.item,\"fromCart\":true}})],1)]),_c('div',{staticClass:\"trash\"},[_c('b-icon',{attrs:{\"icon\":\"trash-can-outline\"},nativeOn:{\"click\":function($event){return _vm.removeCartItem(_vm.item.id)}}})],1)]),_c('div',{staticClass:\"course-selectors\"},[(_vm.item.course.prices && _vm.item.course.prices.length > 1)?_c('div',{staticClass:\"course-selector\"},[_c('span',{staticClass:\"user-selection-question\"},[_vm._v(_vm._s(_vm.$t('cart.selectPrice'))+\":\")]),_c('b-field',{staticClass:\"user-selection-container\"},_vm._l((_vm.item.course.prices),function(price){return _c('b-radio',{key:price.id,staticClass:\"user-selection-choice\",attrs:{\"native-value\":price},model:{value:(_vm.selectedPrice),callback:function ($$v) {_vm.selectedPrice=$$v},expression:\"selectedPrice\"}},[_c('span',{staticClass:\"user-selection-title\"},[_vm._v(\" \"+_vm._s(price.name ? _vm.formatLabel(price.name) : _vm.formatAmount(price.amount))+\" \")]),(price.name)?_c('span',{staticClass:\"user-selection-content\"},[_vm._v(\" \"+_vm._s(_vm.formatAmount(price.amount))+\" \")]):_vm._e()])}),1)],1):_vm._e(),(_vm.selectedPrice && _vm.installmentGroups && _vm.installmentGroups.length > 1)?_c('div',{staticClass:\"course-selector\"},[_c('span',{staticClass:\"user-selection-question\"},[_vm._v(_vm._s(_vm.$t('cart.selectInstallment'))+\":\")]),_c('b-field',{staticClass:\"user-selection-container\"},_vm._l((_vm.installmentGroups),function(installmentgroup,i){return _c('b-radio',{key:i,staticClass:\"user-selection-choice\",attrs:{\"native-value\":installmentgroup},model:{value:(_vm.selectedInstallmentgroup),callback:function ($$v) {_vm.selectedInstallmentgroup=$$v},expression:\"selectedInstallmentgroup\"}},[_c('span',{staticClass:\"user-selection-title\"},[_vm._v(\" \"+_vm._s(_vm.formatLabel(installmentgroup.name) || _vm.$t('cart.installments.default'))+\" \")]),_c('span',{staticClass:\"user-selection-content\"},[_vm._l((installmentgroup.installments),function(installment){return [_c('span',{key:((installment.id) + \"-1\"),staticClass:\"installment-name\"},[_vm._v(\" \"+_vm._s(_vm.formatLabel(installment.name) || _vm.$t('cart.installments.default'))+\" \")]),_c('span',{key:((installment.id) + \"-2\"),staticClass:\"installment-amount\"},[_vm._v(\" \"+_vm._s(_vm.formatAmount(installment.amount))+\" \")])]})],2)])}),1)],1):_vm._e(),(_vm.item.course.courseProducts && _vm.item.course.courseProducts.length > 0)?_c('div',{staticClass:\"course-selector\"},[_c('span',{staticClass:\"user-selection-question\"},[_vm._v(_vm._s(_vm.$t('cart.selectCourseProducts'))+\":\")]),_c('b-field',{staticClass:\"user-selection-container\"},_vm._l((_vm.item.course.courseProducts),function(product,i){return _c('b-checkbox',{key:i,staticClass:\"user-selection-choice\",attrs:{\"native-value\":product},model:{value:(_vm.selectedCourseProducts),callback:function ($$v) {_vm.selectedCourseProducts=$$v},expression:\"selectedCourseProducts\"}},[_c('span',{staticClass:\"user-selection-title\"},[_vm._v(\" \"+_vm._s(_vm.formatLabel(product.name) || _vm.$t('cart.installments.default'))+\" \")]),_c('span',{staticClass:\"user-selection-content\"},[_vm._v(\" \"+_vm._s(_vm.formatAmount(product.price * 100))+\" \")])])}),1)],1):_vm._e()]),(_vm.questionFields && _vm.isSelected)?_c('section',{staticClass:\"questions\",class:{ infoNoPrice: !_vm.item.course.prices || _vm.item.course.prices.length === 0 }},[_c('ValidationObserver',{ref:\"observer\",attrs:{\"tag\":\"form\"}},_vm._l((_vm.questionFields),function(field,i){return _c('div',{key:i.toString()},[_c('ValidationProvider',{attrs:{\"vid\":i.toString(),\"rules\":{ required: field.required },\"slim\":\"\"},scopedSlots:_vm._u([{key:\"default\",fn:function(ref){\nvar errors = ref.errors;\nreturn _c('div',{},[_c('b-field',{attrs:{\"type\":{ 'is-danger': errors[0] },\"message\":_vm.$t(errors[0]),\"label\":field.title,\"label-for\":field.title + _vm.item.course.id,\"custom-class\":field.required ? 'required' : ''}},[(_vm.questionFieldTypes[field.id].questionType === 'freetext')?_c('b-input',{attrs:{\"type\":\"textarea\",\"maxlength\":\"2000\",\"has-counter\":false,\"lazy\":\"\",\"id\":field.title + _vm.item.course.id},model:{value:(_vm.questionAnswers[field.id]),callback:function ($$v) {_vm.$set(_vm.questionAnswers, field.id, $$v)},expression:\"questionAnswers[field.id]\"}}):_vm._e(),(_vm.questionFieldTypes[field.id].questionType === 'dropdown')?_c('b-select',{staticClass:\"course-question-dropdown\",attrs:{\"placeholder\":_vm.$t('select'),\"id\":field.title + _vm.item.course.id},model:{value:(_vm.questionAnswers[field.id]),callback:function ($$v) {_vm.$set(_vm.questionAnswers, field.id, $$v)},expression:\"questionAnswers[field.id]\"}},_vm._l((field.oneOf),function(option){return _c('option',{key:option.const,domProps:{\"value\":option.const}},[_vm._v(\" \"+_vm._s(option.title)+\" \")])}),0):_vm._e(),(_vm.questionFieldTypes[field.id].questionType === 'radioselect')?_c('div',_vm._l((field.oneOf),function(option){return _c('b-field',{key:option.const},[_c('b-radio',{attrs:{\"name\":\"name\",\"native-value\":option.const},model:{value:(_vm.questionAnswers[field.id]),callback:function ($$v) {_vm.$set(_vm.questionAnswers, field.id, $$v)},expression:\"questionAnswers[field.id]\"}},[_vm._v(\" \"+_vm._s(option.title)+\" \")])],1)}),1):_vm._e(),(errors.length === 0 && _vm.clientAjvErrors.length > 0)?_c('p',{staticClass:\"help is-danger\"},[_vm._v(\" \"+_vm._s(_vm.getFieldError(field.instancePathId))+\" \")]):_vm._e()],1)],1)}}],null,true)})],1)}),0)],1):_vm._e()]),_c('div',{staticClass:\"trash-desktop\"},[_c('b-icon',{attrs:{\"icon\":\"trash-can-outline\"},nativeOn:{\"click\":function($event){return _vm.removeCartItem(_vm.item.id)}}})],1)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { HellewiAgeLimits } from '../api';\n\nexport type AgeLimitTranslation = [string, { age?: string; min?: string; max?: string }];\n\nexport const getAgeLimitTranslation = (ageLimits: HellewiAgeLimits): AgeLimitTranslation => {\n const min = ageLimits.minAge?.toString();\n const maxInt = ageLimits.maxAge;\n const max = maxInt?.toString();\n const maxPlusOne = maxInt ? (maxInt + 1).toString() : undefined;\n\n if (min && max) {\n return min === max ? ['agelimit.absoluteAge', { age: min }] : ['agelimit.minmax', { min, max }];\n }\n\n return maxPlusOne ? ['agelimit.max', { max: maxPlusOne }] : ['agelimit.min', { min }];\n};\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"status\"},[_c('b-icon',{class:(\"status--\" + _vm.status),attrs:{\"icon\":\"circle\",\"size\":\"is-small\"}}),_c('span',[_vm._v(_vm._s(_vm.$t((\"registrationStatus.\" + _vm.status))))])],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./RegistrationStatus.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./RegistrationStatus.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./RegistrationStatus.vue?vue&type=template&id=caaf608a&scoped=true\"\nimport script from \"./RegistrationStatus.vue?vue&type=script&lang=ts\"\nexport * from \"./RegistrationStatus.vue?vue&type=script&lang=ts\"\nimport style0 from \"./RegistrationStatus.vue?vue&type=style&index=0&id=caaf608a&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"caaf608a\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CartItem.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CartItem.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CartItem.vue?vue&type=template&id=36636e56&scoped=true\"\nimport script from \"./CartItem.vue?vue&type=script&lang=ts\"\nexport * from \"./CartItem.vue?vue&type=script&lang=ts\"\nimport style0 from \"./CartItem.vue?vue&type=style&index=0&id=36636e56&prod&lang=scss&scoped=true\"\nimport style1 from \"./CartItem.vue?vue&type=style&index=1&id=36636e56&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"36636e56\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('ValidationObserver',{ref:\"observer\",staticClass:\"reg-form\",attrs:{\"tag\":\"form\"}},[_vm._l((_vm.otherFields),function(field,i){return _c('div',{key:field.name + i.toString()},[_c('ValidationProvider',{attrs:{\"vid\":field.name + i.toString(),\"rules\":{\n required: _vm.clientRequired.includes(field.name),\n email: field.inputType === 'email',\n pin: (field.label === 'pin' && field.pattern) || '',\n birthday: (field.label === 'birthday' && field.pattern) || '',\n participantTooYoung:\n (field.label === 'pin' || field.label === 'birthday') &&\n _vm.ageLimitDates &&\n _vm.ageLimitDates.minBirthDate &&\n _vm.ageLimitDates.minBirthDate.toString(),\n participantTooOld:\n (field.label === 'pin' || field.label === 'birthday') &&\n _vm.ageLimitDates &&\n _vm.ageLimitDates.maxBirthDate &&\n _vm.ageLimitDates.maxBirthDate.toString()\n },\"slim\":\"\"},scopedSlots:_vm._u([{key:\"default\",fn:function(ref){\n var errors = ref.errors;\nreturn _c('div',{staticClass:\"koera\"},[_c('b-field',{attrs:{\"type\":{ 'is-danger': errors[0] },\"message\":_vm.$t(errors[0]),\"label\":_vm.$t('fieldlabel.' + field.label),\"label-for\":field.name + '-' + field.inputType,\"custom-class\":_vm.clientRequired.includes(field.name) ? 'required' : ''}},[(field.type === 'string')?_c('b-input',{attrs:{\"lazy\":\"\",\"id\":field.name + '-' + field.inputType},model:{value:(_vm.model[field.name]),callback:function ($$v) {_vm.$set(_vm.model, field.name, (typeof $$v === 'string'? $$v.trim(): $$v))},expression:\"model[field.name]\"}}):_vm._e(),(field.type === 'number')?_c('b-select',{attrs:{\"placeholder\":_vm.$t('select'),\"id\":field.name + '-' + field.inputType},model:{value:(_vm.model[field.name]),callback:function ($$v) {_vm.$set(_vm.model, field.name, (typeof $$v === 'string'? $$v.trim(): $$v))},expression:\"model[field.name]\"}},_vm._l((field.oneOf),function(option){return _c('option',{key:option.const,domProps:{\"value\":option.const}},[_vm._v(\" \"+_vm._s(option.title)+\" \")])}),0):_vm._e(),(errors.length === 0 && _vm.clientAjvErrors.length > 0)?_c('p',{staticClass:\"help is-danger\"},[_vm._v(\" \"+_vm._s(_vm.getFieldError(field.name))+\" \")]):_vm._e()],1)],1)}}],null,true)})],1)}),(_vm.permissionRadioSelectFields.length > 0)?_c('div',{staticClass:\"checkboxSeparator\"}):_vm._e(),_vm._l((_vm.permissionRadioSelectFields),function(field,i){return _c('fieldset',{key:field.name + i.toString()},[_c('legend',{staticClass:\"label\"},[_vm._v(\" \"+_vm._s(_vm.$t('fieldlabel.' + field.label))+\" \"),_c('span',{staticStyle:{\"color\":\"#ba1427\"}},[_vm._v(\" * \")])]),_c('ValidationProvider',{attrs:{\"vid\":field.name + i.toString(),\"slim\":\"\"}},[_c('div',[_c('b-field',{attrs:{\"grouped\":\"\"}},[_c('b-radio',{attrs:{\"name\":field.name,\"native-value\":true},model:{value:(_vm.model[field.name]),callback:function ($$v) {_vm.$set(_vm.model, field.name, $$v)},expression:\"model[field.name]\"}},[_vm._v(\" \"+_vm._s(_vm.$t('fieldlabel.permissiontopublishYes'))+\" \")])],1),_c('b-field',{attrs:{\"grouped\":\"\"}},[_c('b-radio',{attrs:{\"name\":field.name,\"native-value\":false},model:{value:(_vm.model[field.name]),callback:function ($$v) {_vm.$set(_vm.model, field.name, $$v)},expression:\"model[field.name]\"}},[_vm._v(\" \"+_vm._s(_vm.$t('fieldlabel.permissiontopublishNo'))+\" \")])],1)],1)]),(\n _vm.clientAjvErrors.length > 0 &&\n _vm.clientAjvErrors[0] &&\n _vm.clientAjvErrors[0].params &&\n _vm.clientAjvErrors[0].params.missingProperty &&\n _vm.clientAjvErrors[0].params.missingProperty.includes(field.name)\n )?_c('p',{staticClass:\"help is-danger\"},[_vm._v(\" \"+_vm._s(_vm.$t('fieldlabel.permissiontopublishEmptyError'))+\" \")]):_vm._e()],1)})],2)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","// Duplicate file from API/src/json-schema/agelimit.ts\n\nimport { subYears, endOfDay, startOfDay, isBefore, isAfter, isSameDay } from 'date-fns';\n\nexport const calculateMinAndMaxBirthDates = (\n courseStartDate: string,\n courseEndDate: string,\n minAge?: number,\n maxAge?: number,\n minimumAgeOnFirstDay = false // Toggle for minimum age on the first day\n // maybe in the future should also add minimumAgeOnYear or minimumAgeOnSemester to allow enrolling based of age on the calendar year or school semester...\n): { minBirthDate?: Date; maxBirthDate?: Date } => {\n // If minimum age is 5 then the person must be 5 years old on the last day of the course (so 4 year olds are allowed to enroll if they become 5 during the course)\n // If minimum age is 5 and minimumAgeOnFirstDay is true then the person must be 5 years old on the first day of the course (so 4 year olds are not allowed to enroll even if they become 5 during the course)\n // If maximum age is 10 then the person must be at most 10 years old on the first day of the course (11th birthday may be on the second day of the course)\n\n const startDate = new Date(courseStartDate);\n const endDate = new Date(courseEndDate);\n const maxBirthDate = maxAge ? subYears(startDate, maxAge + 1) : undefined;\n const referenceDate = minimumAgeOnFirstDay ? startOfDay(startDate) : endOfDay(endDate);\n const minBirthDate = minAge ? subYears(referenceDate, minAge) : undefined;\n\n return { minBirthDate, maxBirthDate };\n};\n\nexport const parseBirthDateFromPin = (pin: string): Date => {\n const day = parseInt(pin.slice(0, 2), 10);\n const month = parseInt(pin.slice(2, 4), 10);\n const birthYearLastTwoDigits = parseInt(pin.slice(4), 10);\n return new Date(birthYearLastTwoDigits + 1900, month - 1, day);\n};\n\nexport const adjustBirthDateForLastHundredYears = (birthDate: Date): Date => {\n const currentYear = new Date().getFullYear();\n const birthYear = birthDate.getFullYear();\n return currentYear - birthYear > 100\n ? new Date(birthYear + 100, birthDate.getMonth(), birthDate.getDate())\n : birthDate;\n};\n\nexport const isParticipantTooYoung = (minBirthDate: Date, birthDate: Date): boolean => {\n return isBefore(minBirthDate, birthDate);\n};\n\nexport const isParticipantTooOld = (maxBirthDate: Date, birthDate: Date): boolean => {\n return isSameDay(maxBirthDate, birthDate) || isAfter(maxBirthDate, birthDate);\n};\n","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./RegistrationForm.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./RegistrationForm.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./RegistrationForm.vue?vue&type=template&id=5986ba58\"\nimport script from \"./RegistrationForm.vue?vue&type=script&lang=ts\"\nexport * from \"./RegistrationForm.vue?vue&type=script&lang=ts\"\nimport style0 from \"./RegistrationForm.vue?vue&type=style&index=0&id=5986ba58&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./ClientCard.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./ClientCard.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./ClientCard.vue?vue&type=template&id=11b8061a&scoped=true\"\nimport script from \"./ClientCard.vue?vue&type=script&lang=ts\"\nexport * from \"./ClientCard.vue?vue&type=script&lang=ts\"\nimport style0 from \"./ClientCard.vue?vue&type=style&index=0&id=11b8061a&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"11b8061a\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"card payer-card\"},[_c('div',{staticClass:\"card-content\"},[_c('h2',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('cart.payer.info')))]),(\n (_vm.required.includes('billingid') || _vm.clientpinrequired) &&\n _vm.brand &&\n _vm.brand.onlycompanybilling === false\n )?_c('div',{staticClass:\"block\"},[_c('b-field',[_c('b-checkbox',{model:{value:(_vm.payerFromClient),callback:function ($$v) {_vm.payerFromClient=$$v},expression:\"payerFromClient\"}},[_vm._v(_vm._s(_vm.$t('cart.client.payer')))])],1)],1):_vm._e(),(_vm.brand && _vm.brand.onlycompanybilling === true)?_c('div',{staticClass:\"block\"},[_c('b-field',[_c('b-checkbox',{model:{value:(_vm.companyBilling),callback:function ($$v) {_vm.companyBilling=$$v},expression:\"companyBilling\"}},[_vm._v(_vm._s(_vm.$t('cart.payer.companybilling'))+\" \")])],1)],1):_vm._e(),(!(_vm.brand && _vm.brand.onlycompanybilling === true))?_c('div',{staticClass:\"block\"},[_c('b-field',[_c('b-radio',{attrs:{\"native-value\":\"private\",\"disabled\":_vm.payerFromClient},model:{value:(_vm.payerType),callback:function ($$v) {_vm.payerType=$$v},expression:\"payerType\"}},[_vm._v(\" \"+_vm._s(_vm.$t('cart.payer.private'))+\" \")]),_c('b-radio',{attrs:{\"native-value\":\"business\",\"disabled\":_vm.payerFromClient},model:{value:(_vm.payerType),callback:function ($$v) {_vm.payerType=$$v},expression:\"payerType\"}},[_vm._v(\" \"+_vm._s(_vm.$t('cart.payer.business'))+\" \")])],1)],1):_vm._e(),_c('ValidationObserver',{directives:[{name:\"show\",rawName:\"v-show\",value:(!(_vm.brand && _vm.brand.onlycompanybilling === true && _vm.companyBilling === false)),expression:\"!(brand && brand.onlycompanybilling === true && companyBilling === false)\"}],ref:\"observer\",staticClass:\"reg-form\",attrs:{\"tag\":\"form\"}},_vm._l((_vm.fields),function(field,i){return _c('ValidationProvider',{key:i,attrs:{\"rules\":{\n required: _vm.required.includes(field.name),\n email: field.inputType === 'email',\n pin: (field.label === 'pin' && field.pattern) || '',\n birthday: (field.label === 'birthday' && field.pattern) || ''\n }},scopedSlots:_vm._u([{key:\"default\",fn:function(ref){\n var errors = ref.errors;\nreturn _c('div',{},[_c('b-field',{attrs:{\"type\":{ 'is-danger': errors[0] },\"horizontal\":field.type === 'boolean',\"message\":_vm.$t(errors[0]),\"label\":_vm.$t('fieldlabel.' + field.label),\"label-for\":field.name + '-' + field.inputType,\"custom-class\":_vm.required.includes(field.name) ? 'required' : ''}},[(field.type === 'string')?_c('b-input',{attrs:{\"lazy\":\"\",\"type\":field.inputType ? field.inputType : 'text',\"disabled\":_vm.payerFromClient,\"id\":field.name + '-' + field.inputType},model:{value:(_vm.model[field.name]),callback:function ($$v) {_vm.$set(_vm.model, field.name, (typeof $$v === 'string'? $$v.trim(): $$v))},expression:\"model[field.name]\"}}):_vm._e(),(field.type === 'number')?_c('b-select',{attrs:{\"placeholder\":_vm.$t('select'),\"disabled\":_vm.payerFromClient,\"id\":field.name + '-' + field.inputType},model:{value:(_vm.model[field.name]),callback:function ($$v) {_vm.$set(_vm.model, field.name, (typeof $$v === 'string'? $$v.trim(): $$v))},expression:\"model[field.name]\"}},_vm._l((field.oneOf),function(option){return _c('option',{key:option.const,domProps:{\"value\":option.const}},[_vm._v(\" \"+_vm._s(option.title)+\" \")])}),0):_vm._e(),(field.type === 'boolean')?_c('b-checkbox',{model:{value:(_vm.model[field.name]),callback:function ($$v) {_vm.$set(_vm.model, field.name, $$v)},expression:\"model[field.name]\"}}):_vm._e(),(errors.length === 0 && _vm.payerAjvErrors.length > 0)?_c('p',{staticClass:\"help is-danger\"},[_vm._v(\" \"+_vm._s(_vm.getFieldError(field.name))+\" \")]):_vm._e()],1)],1)}}],null,true)})}),1)],1)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./PayerCard.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./PayerCard.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./PayerCard.vue?vue&type=template&id=7f204192&scoped=true\"\nimport script from \"./PayerCard.vue?vue&type=script&lang=ts\"\nexport * from \"./PayerCard.vue?vue&type=script&lang=ts\"\nimport style0 from \"./PayerCard.vue?vue&type=style&index=0&id=7f204192&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"7f204192\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h2',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('summary')))]),_vm._l((_vm.items),function(item){return _c('article',{key:item.cartItemId,staticClass:\"course\"},[(item.type !== 'discount')?_c('div',[_c('h4',{staticClass:\"course-title\"},[_vm._v(_vm._s(item.name))]),(!item.registrationToLessons)?_c('div',{staticClass:\"trash\"},[_c('b-icon',{attrs:{\"icon\":\"trash-can-outline\"},nativeOn:{\"click\":function($event){return _vm.removeCartItem(item.cartItemId)}}})],1):_vm._e(),_c('div',{staticClass:\"line-status\"},[_c('div',{staticClass:\"lightGray id\"},[_vm._v(_vm._s(item.courseCode))]),_c('RegistrationStatus',{attrs:{\"registration\":item,\"fromCart\":true}})],1),_c('div',{staticClass:\"client\"},[_vm._v(\" \"+_vm._s(_vm.$t('client.client'))+\": \"),_c('span',{class:!item.clientName && 'lightGray'},[_vm._v(\" \"+_vm._s(item.clientName || _vm.$t('cart.notselected'))+\" \")])]),_vm._l((item.items),function(subitem){return _c('div',{key:subitem.id,staticClass:\"row\"},[_c('div',[_vm._v(_vm._s(subitem.name))]),(!item.spare)?_c('div',{staticClass:\"price\"},[_vm._v(_vm._s(_vm.$n(subitem.amount / 100, 'currency')))]):_vm._e(),(item.spare)?_c('div',[_vm._v(_vm._s(_vm.$n(subitem.spareamount / 100, 'currency'))+\" *\")]):_vm._e()])})],2):_vm._e(),(item.type === 'discount')?_c('div',[_c('h4',{staticClass:\"course-title\"},[_vm._v(_vm._s(item.name))]),_vm._l((item.items),function(subitem){return _c('div',{key:subitem.id,staticClass:\"row\"},[_c('div',[_vm._v(_vm._s(subitem.name))]),(!item.spare)?_c('div',{staticClass:\"price\"},[_vm._v(_vm._s(_vm.$n(subitem.amount / 100, 'currency')))]):_vm._e()])})],2):_vm._e()])}),(\n _vm.purchase &&\n _vm.purchase.productstotal &&\n (_vm.purchase.productstotal.amount > 0 || _vm.purchase.productstotal.spareamount > 0)\n )?_c('section',{staticClass:\"prices\"},[(_vm.purchase.productstotal.amount > 0)?_c('div',{staticClass:\"row total\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.paymenttotal')))]),_c('div',{staticClass:\"price\"},[_vm._v(_vm._s(_vm.$n(_vm.purchase.productstotal.amount / 100, 'currency')))])]):_vm._e(),(_vm.hasAnySpare)?_c('div',{staticClass:\"row othertotal\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.sparetotal')))]),_c('div',[_vm._v(_vm._s(_vm.$n(_vm.purchase.productstotal.spareamount / 100, 'currency'))+\" *\")])]):_vm._e(),(_vm.purchase.productstotal.culturevoucher)?_c('div',{staticClass:\"row othertotal\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.summary.culturevouchertotal')))]),_c('div',[_vm._v(_vm._s(_vm.$n(_vm.purchase.productstotal.culturevoucher / 100, 'currency')))])]):_vm._e(),(_vm.purchase.productstotal.sportsvoucher)?_c('div',{staticClass:\"row othertotal\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.summary.sportsvouchertotal')))]),_c('div',[_vm._v(_vm._s(_vm.$n(_vm.purchase.productstotal.sportsvoucher / 100, 'currency')))])]):_vm._e(),(_vm.purchase.productstotal.canpaynow)?_c('div',{staticClass:\"row\"},[_c('div',[_vm._v(\" \"+_vm._s(_vm.$t('cart.canpaynow'))+\" \")]),_c('div',{staticClass:\"priceBig\"},[_vm._v(\" \"+_vm._s(_vm.$n(_vm.purchase.productstotal.canpaynow / 100, 'currency'))+\" \")])]):_vm._e(),(_vm.purchase.productstotal.mustpaynow)?_c('div',{staticClass:\"row\"},[_c('div',[_vm._v(\" \"+_vm._s(_vm.$t('cart.mustpaynow'))+\" \")]),_c('div',{staticClass:\"priceBig\"},[_vm._v(\" \"+_vm._s(_vm.$n(_vm.purchase.productstotal.mustpaynow / 100, 'currency'))+\" \")])]):_vm._e(),(_vm.hasAnySpare)?_c('div',{staticClass:\"spare-explanation\",class:_vm.purchase.productstotal.amount <= 0 && 'only-spares'},[_vm._v(\" \"+_vm._s(_vm.$t('cart.spareExplanation'))+\" \")]):_vm._e(),_c('b-collapse',{attrs:{\"open\":false,\"position\":\"is-top\",\"aria-id\":\"contentIdForA11y4\"},scopedSlots:_vm._u([{key:\"trigger\",fn:function(props){return [_c('a',{attrs:{\"aria-controls\":\"contentIdForA11y4\",\"aria-expanded\":props.open}},[_c('b-icon',{attrs:{\"icon\":!props.open ? 'menu-down' : 'menu-up'}}),_vm._v(\" \"+_vm._s(_vm.$t('cart.discountcode'))+\" \")],1)]}}],null,false,1406187313)},[_c('label',[_c('span',{staticClass:\"dicountlabel\"},[_vm._v(_vm._s(_vm.$t('cart.code')))]),_c('b-input',{attrs:{\"lazy\":\"\",\"id\":_vm.discountcode},model:{value:(_vm.discountcode),callback:function ($$v) {_vm.discountcode=(typeof $$v === 'string'? $$v.trim(): $$v)},expression:\"discountcode\"}})],1),_c('div',{staticClass:\"buttons is-right\"},[_c('b-button',{attrs:{\"size\":\"is-small\",\"type\":\"is-primary \"},on:{\"click\":_vm.checkDiscount}},[_vm._v(\" \"+_vm._s(_vm.$t('cart.discountcodecheck'))+\" \")])],1)])],1):_vm._e(),_c('b-field',[_c('b-checkbox',{model:{value:(_vm.termsAccepted),callback:function ($$v) {_vm.termsAccepted=$$v},expression:\"termsAccepted\"}},[_c('i18n',{attrs:{\"path\":\"cart.terms\"}},[_c('router-link',{attrs:{\"to\":{ name: 'help' },\"target\":\"_blank\"}},[_vm._v(\" \"+_vm._s(_vm.$t('cart.instructions'))+\" \")])],1)],1)],1),_c('b-field',[_c('b-button',{attrs:{\"type\":\"is-primary\",\"loading\":_vm.postRegIsLoading},on:{\"click\":_vm.sendRegistration}},[_vm._v(\" \"+_vm._s(_vm.$t('cart.send'))+\" \")])],1)],2)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CartSummary.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CartSummary.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CartSummary.vue?vue&type=template&id=4a6c092a&scoped=true\"\nimport script from \"./CartSummary.vue?vue&type=script&lang=ts\"\nexport * from \"./CartSummary.vue?vue&type=script&lang=ts\"\nimport style0 from \"./CartSummary.vue?vue&type=style&index=0&id=4a6c092a&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"4a6c092a\",\n null\n \n)\n\nexport default component.exports","import { Ref, computed } from '@vue/composition-api';\nimport { useGetBrand } from './useBrandApi';\n\nconst useTitle = (): {\n setTitle: (handle?: string) => string | undefined;\n brandIsLoaded: Ref;\n} => {\n const { response: brand } = useGetBrand();\n const brandIsLoaded = computed(() => brand.value !== undefined);\n\n const setTitle = (handle?: string) => {\n if (!brand.value) {\n return handle;\n }\n\n document.title = handle ? `${handle} - ${brand.value.name}` : brand.value.name;\n };\n\n return { setTitle, brandIsLoaded };\n};\n\nexport default useTitle;\n","\n\n\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Cart.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Cart.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Cart.vue?vue&type=template&id=20dff0d0&scoped=true\"\nimport script from \"./Cart.vue?vue&type=script&lang=ts\"\nexport * from \"./Cart.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Cart.vue?vue&type=style&index=0&id=20dff0d0&prod&lang=scss&scoped=true\"\nimport style1 from \"./Cart.vue?vue&type=style&index=1&id=20dff0d0&prod&lang=css\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"20dff0d0\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[(_vm.isLoading)?_c('div',[_c('section',{staticClass:\"skeleton-wrapper\"},[_c('div',{staticClass:\"skeleton-content-wrapper\"},[_c('b-skeleton',{attrs:{\"width\":\"100%\",\"height\":\"3rem\",\"animated\":true}}),_c('b-skeleton',{attrs:{\"width\":\"100%\",\"height\":\"20rem\",\"animated\":true}})],1),_c('div',{staticClass:\"registration-box-skeleton card\"},[_c('b-skeleton',{attrs:{\"width\":\"100%\",\"height\":\"3rem\",\"animated\":true}}),_c('b-skeleton',{attrs:{\"width\":\"100%\",\"height\":\"10rem\",\"animated\":true}})],1)])]):_vm._e(),(!_vm.isLoading && _vm.course)?_c('div',{staticClass:\"wrapper\"},[_c('section',{staticClass:\"main-content content\"},[(_vm.course)?_c('BreadCrumbs',{attrs:{\"course\":_vm.course}}):_vm._e(),_c('h1',{staticClass:\"title course-title\"},[_vm._v(\" \"+_vm._s(_vm.course.name)+\" \"),(_vm.course.code)?_c('small',[_vm._v(_vm._s(_vm.course.code))]):_vm._e()]),_c('SocialShare'),(_vm.course.images && _vm.course.images.length > 0)?_c('CourseImage',{attrs:{\"image\":_vm.course.images[0],\"main-image\":\"\",\"fallback-alt-text\":_vm.course.name}}):_vm._e(),_c('div',_vm._l((_vm.course.notifications),function(notification,i){return _c('b-message',{key:i,attrs:{\"type\":\"is-primary is-light\",\"has-icon\":\"\",\"icon\":\"information\",\"icon-size\":\"is-medium\",\"closable\":false}},[_vm._v(\" \"+_vm._s(notification.text)+\" \")])}),1),_c('div',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.course.description),expression:\"course.description\"}],staticClass:\"description\"}),(_vm.course.additionalinfo)?_c('div',[_c('p',[_c('strong',[_vm._v(_vm._s(_vm.$t('course.additionalinfo')))])])]):_vm._e(),_c('div',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.course.additionalinfo),expression:\"course.additionalinfo\"}],staticClass:\"additionalinfo\"}),(_vm.ageLimitString)?_c('div',{staticClass:\"age-limits\"},[_c('p',[_c('strong',[_vm._v(_vm._s(_vm.$t('agelimit.agelimit')))])]),_c('p',[_vm._v(\" \"+_vm._s(_vm.ageLimitString)+\" \")])]):_vm._e(),(_vm.course.learningobjectives)?_c('div',[_c('p',[_c('strong',[_vm._v(_vm._s(_vm.$t('course.learningobjectives')))])])]):_vm._e(),_c('div',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.course.learningobjectives),expression:\"course.learningobjectives\"}],staticClass:\"learningobjectives\"}),(_vm.course.evaluationcriteria)?_c('div',[_c('p',[_c('strong',[_vm._v(_vm._s(_vm.$t('course.evaluationcriteria')))])])]):_vm._e(),_c('div',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.course.evaluationcriteria),expression:\"course.evaluationcriteria\"}],staticClass:\"evaluationcriteria\"}),(_vm.course.askabout)?_c('div',[_c('a',{staticClass:\"button is-primary is-small\",attrs:{\"href\":'mailto:' + _vm.course.askabout}},[_vm._v(\" \"+_vm._s(_vm.$t('course.askabout'))+\" \")])]):_vm._e(),(_vm.course.files && _vm.course.files.length > 0)?_c('div',[_c('p',[_c('strong',[_vm._v(_vm._s(_vm.$t('course.files')))])])]):_vm._e(),_c('div',{staticClass:\"coursefiles\"},_vm._l((_vm.course.files),function(file){return _c('CourseFile',{key:file.url,attrs:{\"file\":file}})}),1),(_vm.additionalImages && _vm.additionalImages.length > 0)?_c('div',{staticClass:\"additional-images\"},_vm._l((_vm.additionalImages),function(image){return _c('CourseImage',{key:image.url,attrs:{\"image\":image,\"fallback-alt-text\":_vm.course.name}})}),1):_vm._e(),_c('hr'),(_vm.registrationToLessons)?_c('LessonsRegistration',{attrs:{\"course\":_vm.course,\"unlistedid\":_vm.unlistedid}}):_c('div',[_vm._l((_vm.sortBy(['begins', 'ends'], _vm.course.periods)),function(period){return _c('div',{key:period.name + period.begins},[(period.keywords.some(function (keyword) { return keyword.includes('period'); }))?_c('LessonsCollapse',{attrs:{\"period\":period}}):_vm._e()],1)}),(_vm.course.periods.length === 0 && _vm.course.lessons.length > 0)?_c('div',[_c('LessonsCollapse',{attrs:{\"course\":_vm.course}})],1):_vm._e()],2),(_vm.course.periods.length !== 0 || _vm.course.lessons.length !== 0)?_c('hr'):_vm._e(),_c('RegistrationBox',{staticClass:\"registration-box-mobile card\",attrs:{\"course\":_vm.course},on:{\"cart-add\":_vm.addToCart}}),_c('CourseInfoDl',{attrs:{\"course\":_vm.course,\"spacing\":true,\"fields\":[\n _vm.CourseInfoDlField.Teacher,\n _vm.CourseInfoDlField.Location,\n _vm.CourseInfoDlField.Ectscredits\n ],\"locationSingleLine\":true,\"locationMapLink\":false,\"horizontal\":true}}),(_vm.course.location && _vm.course.location.latlon)?_c('CourseMap',{staticClass:\"course-map\",attrs:{\"coordinates\":_vm.course.location.latlon,\"text\":_vm.course.location.name}}):_vm._e()],1),_c('RegistrationBox',{staticClass:\"registration-box-desktop card\",attrs:{\"course\":_vm.course},on:{\"cart-add\":_vm.addToCart}}),_c('script',{attrs:{\"type\":\"application/ld+json\"},domProps:{\"innerHTML\":_vm._s(_vm.jsonld)}})],1):_vm._e()])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('dl',{class:{ horizontal: _vm.horizontal }},[(_vm.show(_vm.CourseInfoDlField.Period))?_c('div',{class:{ spacing: _vm.spacing }},[_c('dt',[_vm._v(_vm._s(_vm.$t('courseInfoDl.period')))]),(_vm.beginsAndEndsFromLesson)?_c('dd',[_vm._v(\" \"+_vm._s(_vm._f(\"dateTimeRange\")(_vm.begins,_vm.ends))+\" \")]):_c('dd',[_vm._v(\" \"+_vm._s(_vm._f(\"dateRange\")(_vm.begins,_vm.ends))+\" \")])]):_vm._e(),(_vm.show(_vm.CourseInfoDlField.Periods))?_c('div',{class:{ spacing: _vm.spacing }},_vm._l((_vm.periods),function(period){return _c('div',{key:period.keywords[0]},[_c('dt',[_vm._v(_vm._s(period.name))]),_c('dd',[_vm._v(\" \"+_vm._s(_vm._f(\"dateRange\")(period.begins,period.ends))+\" \"),(period.lessoncount)?_c('span',[_vm._v(\" (\"+_vm._s(_vm.$tc('courseInfoDl.lessons', period.lessoncount))+\") \")]):_vm._e()])])}),0):_vm._e(),(_vm.show(_vm.CourseInfoDlField.Weekdays))?_c('div',{class:{ spacing: _vm.spacing }},[_c('dt',[_vm._v(_vm._s(_vm.$t('courseInfoDl.weekdays')))]),_vm._l((_vm.formattedWeekdays),function(day){return _c('dd',{key:String(day.weekday),staticClass:\"weekday\"},[(day.weekday)?_c('span',{staticClass:\"weekday-short\"},[_vm._v(\" \"+_vm._s(_vm.$d(day.weekday, 'weekdayShort'))+\" \")]):_vm._e(),_c('span',_vm._l((day.times),function(ref){\nvar begins = ref.begins;\nvar ends = ref.ends;\nreturn _c('p',{key:(begins + \"-\" + ends)},[_vm._v(\" \"+_vm._s(begins)+\"–\"+_vm._s(ends)+\" \")])}),0)])})],2):_vm._e(),(_vm.show(_vm.CourseInfoDlField.MobileMinimal))?_c('div',{class:{ spacing: _vm.spacing }},[(_vm.isMobileOneLinerPossible)?_c('div',[_vm._l((_vm.formattedWeekdays),function(day){return [(day.weekday)?_c('span',{key:String(day.weekday),staticClass:\"weekday-short-minimal\"},[_vm._v(_vm._s(_vm.$d(day.weekday, 'weekdayShort')))]):_vm._e()]}),(_vm.formattedWeekdays[0].times[0])?_c('div',{staticClass:\"time\"},[_vm._v(\" \"+_vm._s(_vm.formattedWeekdays[0].times[0].begins)+\"–\"+_vm._s(_vm.formattedWeekdays[0].times[0].ends)+\" \")]):_vm._e()],2):_vm._l((_vm.formattedWeekdays),function(day){return _c('dd',{key:String(day.weekday),staticClass:\"weekday\"},[(day.weekday)?_c('span',{staticClass:\"weekday-short\"},[_vm._v(\" \"+_vm._s(_vm.$d(day.weekday, 'weekdayShort'))+\" \")]):_vm._e(),_c('span',_vm._l((day.times),function(ref){\nvar begins = ref.begins;\nvar ends = ref.ends;\nreturn _c('p',{key:(begins + \"-\" + ends)},[_vm._v(\" \"+_vm._s(begins)+\"–\"+_vm._s(ends)+\" \")])}),0)])}),(_vm.periods[0].begins)?_c('span',[_vm._v(\" \"+_vm._s(_vm.$t('courseInfoDl.starts'))+\" \"+_vm._s(_vm._f(\"dateRange\")(_vm.periods[0].begins,_vm.periods[0].begins))+\" \"),(_vm.periods[0].lessoncount)?_c('span',[_vm._v(\" (\"+_vm._s(_vm.$tc('courseInfoDl.lessons', _vm.periods[0].lessoncount))+\") \")]):_vm._e()]):_vm._e()],2):_vm._e(),(_vm.show(_vm.CourseInfoDlField.Ectscredits))?_c('div',{class:{ spacing: _vm.spacing }},[_c('dt',[_vm._v(_vm._s(_vm.$t('courseInfoDl.ectscredits')))]),_c('dd',[_vm._v(_vm._s(_vm.course.ectscredits))])]):_vm._e(),(_vm.show(_vm.CourseInfoDlField.Teacher) && _vm.$t('courseInfoDl.teacher'))?_c('div',{class:{ spacing: _vm.spacing }},[_c('dt',[_vm._v(_vm._s(_vm.$t('courseInfoDl.teacher')))]),_c('dd',[_vm._v(_vm._s(_vm.course.teacher))])]):_vm._e(),(_vm.show(_vm.CourseInfoDlField.Location))?_c('div',{class:{ spacing: _vm.spacing }},[_c('dt',[_vm._v(_vm._s(_vm.$t('courseInfoDl.location')))]),_c('dd',[_c('address',{class:{ 'single-line': _vm.locationSingleLine }},[(_vm.location.name)?_c('span',{staticClass:\"name\"},[_vm._v(_vm._s(_vm.location.name))]):_vm._e(),(_vm.location.address)?_c('span',[_vm._v(_vm._s(_vm.location.address))]):_vm._e(),(_vm.location.city)?_c('span',[_vm._v(_vm._s(_vm.location.city))]):_vm._e()])]),(_vm.location.accessibility)?_c('dd',[(_vm.location.accessibility)?_c('div',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.location.accessibility),expression:\"location.accessibility\"}],staticClass:\"accessibility\"}):_vm._e()]):_vm._e(),(_vm.locationMapLink && _vm.location.latlon)?_c('dd',{staticClass:\"location-map-link\"},[_c('a',{on:{\"click\":function($event){return _vm.$emit('map-modal-open')}}},[_vm._v(_vm._s(_vm.$t('courseInfoDl.showOnMap')))])]):_vm._e()]):_vm._e(),(\n _vm.show(_vm.CourseInfoDlField.RegistrationTime) &&\n _vm.registrationbegins &&\n _vm.registrationendssoft &&\n !_vm.participantcount.registrationopen\n )?_c('div',{class:{ spacing: _vm.spacing }},[_c('dt',[_vm._v(_vm._s(_vm.$t('courseInfoDl.registrationRange')))]),_c('dd',[_vm._v(_vm._s(_vm._f(\"dateTime\")(_vm.registrationbegins))+\"–\"+_vm._s(_vm._f(\"midnight\")(_vm.registrationendssoft)))])]):_vm._e(),(\n _vm.show(_vm.CourseInfoDlField.RegistrationTime) &&\n _vm.registrationbegins &&\n !_vm.registrationendssoft &&\n _vm.registrationbegins > new Date()\n )?_c('div',{class:{ spacing: _vm.spacing }},[_c('dt',[_vm._v(_vm._s(_vm.$t('courseInfoDl.registrationBegins')))]),_c('dd',[_vm._v(\" \"+_vm._s(_vm._f(\"dateTime\")(_vm.registrationbegins))+\" \")])]):_vm._e(),(_vm.show(_vm.CourseInfoDlField.RegistrationTime) && _vm.participantcount.registrationopen)?_c('div',{class:{ spacing: _vm.spacing, registrationopen: _vm.registrationopen }},[_vm._v(\" \"+_vm._s(_vm.$t('courseInfoDl.registrationOpen'))+\" \")]):_vm._e(),(\n _vm.show(_vm.CourseInfoDlField.RegistrationTime) &&\n _vm.registrationbegins &&\n _vm.registrationendssoft &&\n _vm.registrationbegins < new Date() &&\n _vm.registrationendssoft > new Date()\n )?_c('div',{class:{ spacing: _vm.spacing }},[_c('dt',[_vm._v(_vm._s(_vm.$t('courseInfoDl.registrationEnds')))]),_c('dd',[_vm._v(\" \"+_vm._s(_vm._f(\"dateTime\")(_vm.registrationendssoft))+\" \")])]):_vm._e(),(_vm.show(_vm.CourseInfoDlField.PriceClasses))?_c('div',{class:{ spacing: _vm.spacing }},[_c('dt',[_vm._v(_vm._s(_vm.$t('courseInfoDl.priceClasses')))]),_vm._l((_vm.prices),function(price){return _c('dd',{key:price.id},[_vm._v(\" \"+_vm._s(price.name)+\" \"+_vm._s(_vm.$n(price.amount / 100, 'currency'))+\" \"),(price.installmentgroups)?_c('dl',_vm._l((price.installmentgroups),function(installmentgroup,i){return _c('div',{key:i},[_c('dt',[_vm._v(_vm._s(installmentgroup.name))]),_vm._l((installmentgroup.installments),function(installment){return _c('dd',{key:installment.id,staticClass:\"installment\"},[_vm._v(\" \"+_vm._s(installment.name)+\" \"+_vm._s(_vm.$n(installment.amount / 100, 'currency'))+\" \")])})],2)}),0):_vm._e()])})],2):_vm._e()])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { setISODay } from 'date-fns';\nimport { groupBy, map, sortBy, values } from 'lodash/fp';\n\nimport { HellewiCourseDay } from '../api';\n\nexport interface FormatedWeekday {\n weekday: Date | undefined;\n times: WeekdayTime[];\n}\n\nexport interface WeekdayTime {\n begins: string | undefined;\n ends: string | undefined;\n}\n\n/**\n * Combine an array of HellewiCourseDays with possible multiple times on a same day\n * to an array that has only one entry for each day, and that day's times in a\n * separate array.\n *\n * Weekday is also converted to a javascript date so that it can be formatted\n * with i18n. (date and time are faked with setISODay, so use only the weekday from that)\n *\n * Grouped days are not sorted as it would be difficult to get sunday last.\n * Rely on backed giving the days sorted correctly and groupBy retaining that order.\n *\n * @param days array of HellewiCourseDays to be combined\n * @returns combined weekdays with possibly lots of times on a single weekday\n */\nexport const formatWeekdays = (days: HellewiCourseDay[] | undefined): FormatedWeekday[] =>\n map(\n (groupedDays) => ({\n weekday: groupedDays[0].weekday ? setISODay(new Date(), groupedDays[0].weekday) : undefined,\n times: sortBy(\n ({ begins }) => begins,\n map(({ begins, ends }) => ({ begins, ends }), groupedDays)\n )\n }),\n values(groupBy((day) => day.weekday, days))\n );\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseInfoDl.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseInfoDl.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CourseInfoDl.vue?vue&type=template&id=51af39e5&scoped=true\"\nimport script from \"./CourseInfoDl.vue?vue&type=script&lang=ts\"\nexport * from \"./CourseInfoDl.vue?vue&type=script&lang=ts\"\nimport style0 from \"./CourseInfoDl.vue?vue&type=style&index=0&id=51af39e5&prod&lang=scss&scoped=true\"\nimport style1 from \"./CourseInfoDl.vue?vue&type=style&index=1&id=51af39e5&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"51af39e5\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[(_vm.center)?_c('l-map',{staticStyle:{\"z-index\":\"0\"},attrs:{\"zoom\":14,\"center\":_vm.center,\"options\":_vm.MAP_OPTIONS}},[_c('l-control-zoom'),_c('l-tile-layer',{attrs:{\"url\":_vm.URL}}),_c('l-marker',{attrs:{\"lat-lng\":_vm.center,\"icon\":_vm.markerIcon}},[_c('l-tooltip',[_vm._v(_vm._s(_vm.text))])],1)],1):_vm._e()],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseMap.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseMap.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CourseMap.vue?vue&type=template&id=6a549bb2\"\nimport script from \"./CourseMap.vue?vue&type=script&lang=ts\"\nexport * from \"./CourseMap.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-collapse',{staticClass:\"collapse-card\",attrs:{\"animation\":\"slide\",\"aria-id\":_vm.lessons.name + _vm.lessons.begins,\"open\":false},scopedSlots:_vm._u([{key:\"trigger\",fn:function(props){return _c('div',{class:{ 'card-header': true, 'collapse-card-header': _vm.lessons.lessons },attrs:{\"role\":\"button\",\"aria-controls\":_vm.lessons.name + _vm.lessons.begins}},[_c('span',{staticClass:\"card-header-title collapse-title-container\"},[(_vm.lessons.name)?_c('span',{staticClass:\"period-name\"},[_vm._v(_vm._s(_vm.lessons.name + ' '))]):_vm._e(),_c('div',{staticClass:\"collapse-title-details\"},[(_vm.lessons.begins && _vm.lessons.ends)?_c('span',{staticClass:\"collapse-title-detail\"},[_vm._v(\" \"+_vm._s(_vm._f(\"dateRange\")(_vm.lessons.begins,_vm.lessons.ends))+\" \")]):_vm._e(),(_vm.lessons.lessoncount)?_c('span',{staticClass:\"collapse-title-detail\"},[_vm._v(\" (\"+_vm._s(_vm.$t('lessons'))+\": \"+_vm._s(_vm.lessons.lessoncount)+\") \")]):_vm._e()])]),(_vm.lessons.lessons)?_c('div',{staticClass:\"card-header-toggle\"},[_c('div',[_vm._v(_vm._s(props.open ? _vm.$t('close') : _vm.$t('open')))]),_c('a',{staticClass:\"card-header-icon\"},[_c('b-icon',{attrs:{\"icon\":props.open ? 'chevron-up' : 'chevron-down'}})],1)]):_vm._e()])}}])},[(_vm.lessons.lessons)?_c('div',{staticClass:\"card-content\"},[_c('div',{staticClass:\"content\"},[(_vm.lessons.lessons)?_c('b-table',{staticClass:\"lessons-table\",attrs:{\"data\":_vm.lessons.lessons,\"mobile-cards\":true}},[_c('b-table-column',{attrs:{\"custom-key\":\"day\"},scopedSlots:_vm._u([{key:\"header\",fn:function(ref){return [_vm._v(\" \"+_vm._s(_vm.$t('registration.date'))+\" \")]}},{key:\"default\",fn:function(props){return [_c('span',{staticClass:\"weekdayLong\"},[_vm._v(\" \"+_vm._s(_vm.$d(new Date(props.row.begins), 'weekdayLong'))+\" \"+_vm._s(_vm._f(\"dateRange\")(new Date(props.row.begins),new Date(props.row.ends)))+\" \")])]}}],null,false,258276477)}),_c('b-table-column',{attrs:{\"custom-key\":\"time\"},scopedSlots:_vm._u([{key:\"header\",fn:function(ref){return [_vm._v(\" \"+_vm._s(_vm.$t('registration.time'))+\" \")]}},{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm._f(\"timeRange\")(new Date(props.row.begins),new Date(props.row.ends)))+\" \")]}}],null,false,3785659801)}),_c('b-table-column',{attrs:{\"custom-key\":\"place\"},scopedSlots:_vm._u([{key:\"header\",fn:function(ref){return [_vm._v(\" \"+_vm._s(_vm.$t('locationgroup'))+\" \")]}},{key:\"default\",fn:function(props){return [(props.row.location)?_c('span',[_vm._v(\" \"+_vm._s([props.row.location.name, props.row.location.address].filter(Boolean).join(', '))+\" \")]):_c('span',[_vm._v(\" \"+_vm._s(_vm.$t('lessonsCollapse.placeNotProvided'))+\" \")])]}}],null,false,2598098201)})],1):_vm._e()],1)]):_vm._e()])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./LessonsCollapse.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./LessonsCollapse.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./LessonsCollapse.vue?vue&type=template&id=637fd7fb&scoped=true\"\nimport script from \"./LessonsCollapse.vue?vue&type=script&lang=ts\"\nexport * from \"./LessonsCollapse.vue?vue&type=script&lang=ts\"\nimport style0 from \"./LessonsCollapse.vue?vue&type=style&index=0&id=637fd7fb&prod&lang=scss&scoped=true\"\nimport style1 from \"./LessonsCollapse.vue?vue&type=style&index=1&id=637fd7fb&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"637fd7fb\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.lessons.length > 0)?_c('b-table',{staticClass:\"lessons-table\",attrs:{\"data\":_vm.lessons,\"mobile-cards\":true}},[_c('b-table-column',{attrs:{\"custom-key\":\"day\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_c('span',{staticClass:\"weekdayLong\"},[_vm._v(\" \"+_vm._s(_vm.$d(new Date(props.row.begins), 'weekdayLong'))+\" \"+_vm._s(_vm._f(\"dateRange\")(new Date(props.row.begins),new Date(props.row.ends)))+\" \")])]}}],null,false,461554443)}),_c('b-table-column',{attrs:{\"custom-key\":\"time\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm._f(\"timeRange\")(new Date(props.row.begins),new Date(props.row.ends)))+\" \")]}}],null,false,1019148718)}),_c('b-table-column',{attrs:{\"custom-key\":\"availability\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_c('CourseAvailability',{staticClass:\"availability-container\",attrs:{\"participantcount\":props.row.participantcount,\"statuses\":_vm.course.statuses}})]}}],null,false,1594966331)}),_c('b-table-column',{attrs:{\"custom-key\":\"buttons\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_c('b-button',{attrs:{\"type\":\"is-primary is-small\",\"icon-left\":\"cart\",\"disabled\":!props.row.participantcount.registrationopen || props.row.participantcount.full},on:{\"click\":function($event){return _vm.addToCart(props.row.id)}}},[_vm._v(\" \"+_vm._s(_vm.$t('cart.addtocart'))+\" \")])]}}],null,false,4026627583)})],1):_c('b-message',{attrs:{\"type\":\"is-primary is-light\",\"has-icon\":\"\",\"icon\":\"information\",\"icon-size\":\"is-medium\",\"closable\":false}},[_vm._v(\" \"+_vm._s(_vm.$t('lessonsCollapse.lessonsNotAvailable'))+\" \")])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"status\"},[_c('b-icon',{class:(\"status--\" + (_vm.availability.availability)),attrs:{\"icon\":\"circle\",\"size\":\"is-small\"}}),_vm._v(\" \"+_vm._s(_vm.$tc((_vm.i18nKey + \".\" + (_vm.availability.availability)), _vm.availability.places))+\" \")],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseAvailability.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseAvailability.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CourseAvailability.vue?vue&type=template&id=a6ddb5fa&scoped=true\"\nimport script from \"./CourseAvailability.vue?vue&type=script&lang=ts\"\nexport * from \"./CourseAvailability.vue?vue&type=script&lang=ts\"\nimport style0 from \"./CourseAvailability.vue?vue&type=style&index=0&id=a6ddb5fa&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"a6ddb5fa\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./LessonsRegistration.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./LessonsRegistration.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./LessonsRegistration.vue?vue&type=template&id=505ced52&scoped=true\"\nimport script from \"./LessonsRegistration.vue?vue&type=script&lang=ts\"\nexport * from \"./LessonsRegistration.vue?vue&type=script&lang=ts\"\nimport style0 from \"./LessonsRegistration.vue?vue&type=style&index=0&id=505ced52&prod&lang=scss&scoped=true\"\nimport style1 from \"./LessonsRegistration.vue?vue&type=style&index=1&id=505ced52&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"505ced52\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.course)?_c('div',{staticClass:\"registration-box\"},[(_vm.price)?_c('div',{staticClass:\"block\"},[_c('strong',{staticClass:\"is-size-3\"},[_vm._v(_vm._s(_vm.$n(_vm.price, 'currency')))])]):_vm._e(),_c('div',{staticClass:\"block\"},[_c('CourseInfoDl',{staticClass:\"courseDetails\",attrs:{\"course\":_vm.course,\"fields\":_vm.courseInfoDlFields,\"spacing\":true}})],1),(\n _vm.course.registrationbegins ||\n _vm.course.registrationendshard ||\n _vm.course.registrationendssoft ||\n _vm.course.statuses.includes(_vm.HellewiCourseStatus.Cancelled) ||\n _vm.course.statuses.includes(_vm.HellewiCourseStatus.Interrupted)\n )?_c('div',{staticClass:\"cart-availability\"},[(!_vm.registrationToLessons)?_c('div',{staticClass:\"block\"},[_c('CourseAvailability',{attrs:{\"participantcount\":_vm.course.participantcount,\"statuses\":_vm.course.statuses}})],1):_vm._e()]):_vm._e(),(\n (_vm.course.registrationbegins || _vm.course.registrationendshard || _vm.course.registrationendssoft) &&\n !_vm.course.statuses.includes(_vm.HellewiCourseStatus.Cancelled) &&\n !_vm.course.statuses.includes(_vm.HellewiCourseStatus.Interrupted)\n )?_c('div',{staticClass:\"add-to-cart-availability\"},[(!_vm.registrationToLessons)?_c('b-button',{staticClass:\"button is-primary is-medium cart-button\",attrs:{\"icon-left\":\"cart\",\"disabled\":!_vm.course.participantcount.registrationopen ||\n (_vm.course.participantcount.full && _vm.course.participantcount.sparefull)},nativeOn:{\"click\":function($event){return _vm.addToCart($event)}}},[_vm._v(\" \"+_vm._s(_vm.$t('cart.addtocart'))+\" \")]):_vm._e()],1):_vm._e()]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { find } from 'lodash/fp';\nimport { HellewiCoursePartial, HellewiCoursePrice } from '../api';\n\nexport const getDefaultPrice = (\n course: Pick | undefined\n): HellewiCoursePrice | undefined => {\n if (!course || !course.prices) {\n return undefined;\n }\n\n return find({ _default: true }, course.prices);\n};\n\nexport const getPriceEuros = (\n price: Pick | undefined\n): number | undefined => {\n if (price != null && price.amount != null) {\n return price.amount / 100;\n } else {\n return undefined;\n }\n};\n","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./RegistrationBox.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./RegistrationBox.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./RegistrationBox.vue?vue&type=template&id=91991ddc&scoped=true\"\nimport script from \"./RegistrationBox.vue?vue&type=script&lang=ts\"\nexport * from \"./RegistrationBox.vue?vue&type=script&lang=ts\"\nimport style0 from \"./RegistrationBox.vue?vue&type=style&index=0&id=91991ddc&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"91991ddc\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"social\"},[_c('div',{staticClass:\"social-child\"},[_c('a',{staticClass:\"twitter-share-button\",attrs:{\"href\":(\"https://twitter.com/intent/tweet?url=\" + _vm.fullpath),\"target\":\"_blank\"}},[_c('b-icon',{attrs:{\"icon\":\"twitter\"}}),_vm._v(_vm._s(_vm.$t('tweet')))],1)]),_c('div',{staticClass:\"social-child\"},[_c('a',{staticClass:\"fb-share-button\",attrs:{\"href\":(\"https://www.facebook.com/share.php?u=\" + _vm.fullpath),\"target\":\"_blank\"}},[_c('b-icon',{attrs:{\"icon\":\"facebook\"}}),_vm._v(_vm._s(_vm.$t('share'))+\" \")],1)])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[1]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./SocialShare.vue?vue&type=script&lang=js\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[1]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./SocialShare.vue?vue&type=script&lang=js\"","import { render, staticRenderFns } from \"./SocialShare.vue?vue&type=template&id=76707c47&scoped=true\"\nimport script from \"./SocialShare.vue?vue&type=script&lang=js\"\nexport * from \"./SocialShare.vue?vue&type=script&lang=js\"\nimport style0 from \"./SocialShare.vue?vue&type=style&index=0&id=76707c47&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"76707c47\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"image-container\",class:{ main: _vm.mainImage }},[_c('div',{ref:\"imageElement\",staticClass:\"image\",class:{\n 'main-image': _vm.mainImage,\n 'show-full-image': _vm.showFullImage,\n clickable: _vm.showToggle\n },style:({ backgroundImage: (\"url(\" + (_vm.image.url) + \")\") }),on:{\"click\":function($event){return _vm.toggleShowImage()}}},[_c('img',{ref:\"invisibleImageElement\",staticClass:\"invisible-placeholder-image\",attrs:{\"src\":_vm.image.url,\"alt\":_vm.image.alt ? _vm.image.alt : _vm.fallbackAltText}})]),(_vm.showToggle)?_c('b-button',{staticClass:\"enlarge-image-button\",attrs:{\"type\":\"is-link is-light\",\"size\":\"is-small\",\"icon-right\":_vm.showFullImage ? 'chevron-up' : 'chevron-down'},on:{\"click\":function($event){return _vm.toggleShowImage()}}},[_vm._v(\" \"+_vm._s(_vm.showFullImage ? _vm.$t('course.shrinkImage') : _vm.$t('course.enlargeImage'))+\" \")]):_vm._e()],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseImage.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseImage.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CourseImage.vue?vue&type=template&id=2a224f0e&scoped=true\"\nimport script from \"./CourseImage.vue?vue&type=script&lang=ts\"\nexport * from \"./CourseImage.vue?vue&type=script&lang=ts\"\nimport style0 from \"./CourseImage.vue?vue&type=style&index=0&id=2a224f0e&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"2a224f0e\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-button',{staticClass:\"download-button\",attrs:{\"tag\":\"a\",\"href\":_vm.file.url,\"type\":\"is-light\",\"icon-left\":\"download\",\"download\":\"\"}},[_vm._v(\" \"+_vm._s(_vm.fileName)+\" \")])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseFile.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseFile.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CourseFile.vue?vue&type=template&id=197842bc&scoped=true\"\nimport script from \"./CourseFile.vue?vue&type=script&lang=ts\"\nexport * from \"./CourseFile.vue?vue&type=script&lang=ts\"\nimport style0 from \"./CourseFile.vue?vue&type=style&index=0&id=197842bc&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"197842bc\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"breadcrumbs\"},[(_vm.department)?_c('span',[_c('router-link',{attrs:{\"to\":{ name: 'home', query: { q: _vm.department.keywords[0] } }}},[_vm._v(\" \"+_vm._s(_vm.department.name)+\" \")]),(_vm.category || _vm.subject)?_c('b-icon',{attrs:{\"icon\":\"chevron-right\",\"size\":\"is-small\"}}):_vm._e()],1):_vm._e(),(_vm.category)?_c('span',[_c('router-link',{attrs:{\"to\":{ name: 'home', query: { q: _vm.category.keywords[0] } }}},[_vm._v(_vm._s(_vm.category.name)+\" \")]),(_vm.subject)?_c('b-icon',{attrs:{\"icon\":\"chevron-right\",\"size\":\"is-small\"}}):_vm._e()],1):_vm._e(),(_vm.subject)?_c('span',[_c('router-link',{attrs:{\"to\":{\n name: 'home',\n query: {\n // Use both keywords for search: e.g. 'subject:8 subject:3/8'\n // It depends on store settings which are properly interpreted as tags\n // so better to use both\n q:\n _vm.subject.keywords.length > 1\n ? ((_vm.subject.keywords[0]) + \" \" + (_vm.subject.keywords[1]))\n : _vm.subject.keywords[0]\n }\n }}},[_vm._v(_vm._s(_vm.subject.name)+\" \")])],1):_vm._e()])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./BreadCrumbs.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./BreadCrumbs.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./BreadCrumbs.vue?vue&type=template&id=24543d30&scoped=true\"\nimport script from \"./BreadCrumbs.vue?vue&type=script&lang=ts\"\nexport * from \"./BreadCrumbs.vue?vue&type=script&lang=ts\"\nimport style0 from \"./BreadCrumbs.vue?vue&type=style&index=0&id=24543d30&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"24543d30\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Course.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Course.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Course.vue?vue&type=template&id=c0765e6e&scoped=true\"\nimport script from \"./Course.vue?vue&type=script&lang=ts\"\nexport * from \"./Course.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Course.vue?vue&type=style&index=0&id=c0765e6e&prod&lang=scss&scoped=true\"\nimport style1 from \"./Course.vue?vue&type=style&index=1&id=c0765e6e&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"c0765e6e\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.help && !_vm.isLoading)?_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('article',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.help.text),expression:\"help.text\"}],staticClass:\"help-content\"})])]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Help.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Help.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Help.vue?vue&type=template&id=65f21e6f&scoped=true\"\nimport script from \"./Help.vue?vue&type=script&lang=ts\"\nexport * from \"./Help.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Help.vue?vue&type=style&index=0&id=65f21e6f&prod&scoped=true&lang=css\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"65f21e6f\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('Hero',{on:{\"search\":_vm.setKeyword}}),_c('PromotionCarousel',{staticClass:\"promotion-carousel\"}),_c('div',{staticClass:\"mainContent\",attrs:{\"id\":\"mainContent\"}},[_c('div',{staticClass:\"filters\"},[_c('div',{staticClass:\"desktop-filters\"},[_c('div',{staticClass:\"search desktop-filters\"},[_c('label',{staticClass:\"is-sr-only\",attrs:{\"for\":\"desktop-aside-search\"}},[_vm._v(_vm._s(_vm.$t('search.label')))]),_c('SearchInput',{attrs:{\"id\":\"desktop-aside-search\",\"placeholder\":_vm.$t('search.term'),\"iconRight\":\"\"},on:{\"search\":_vm.setKeyword}})],1),_c('CatalogFilters',{on:{\"filters-changed\":_vm.setFilters}})],1),_c('MobileFilters',{staticClass:\"mobile-filters\",on:{\"search\":_vm.setKeyword},scopedSlots:_vm._u([{key:\"filters\",fn:function(){return [_c('CatalogFilters',{staticClass:\"mobile-filter-container\",on:{\"filters-changed\":_vm.setFilters}})]},proxy:true},{key:\"sorting\",fn:function(){return [_c('div',{staticClass:\"sorting\"},[(_vm.isLoading)?_c('p',{staticClass:\"result-amount\"},[_vm._v(_vm._s(_vm.$t('coursesLoading')))]):_c('p',{staticClass:\"result-amount\"},[_vm._v(_vm._s(_vm.$tc('results', _vm.courseCount)))]),_c('Sort',{on:{\"sort-changed\":_vm.setSorting}})],1)]},proxy:true}])}),_c('ScrollToFilters')],1),_c('div',{staticClass:\"hide-desktop\"},[_c('div',{staticClass:\"sorting only-mobile\"},[(_vm.isLoading)?_c('p',{staticClass:\"result-amount\"},[_vm._v(_vm._s(_vm.$t('coursesLoading')))]):_c('p',{staticClass:\"result-amount\"},[_vm._v(_vm._s(_vm.$tc('results', _vm.courseCount)))]),_c('Sort',{on:{\"sort-changed\":_vm.setSorting}})],1),_c('FilterTags',{on:{\"filters-changed\":_vm.setFilters,\"search\":_vm.setKeyword}})],1),_c('div',[_c('div',{staticClass:\"sorting desktop-filters\"},[_c('div',{staticClass:\"sorting-top-row\"},[(_vm.isLoading)?_c('p',{staticClass:\"result-amount\"},[_vm._v(_vm._s(_vm.$t('coursesLoading')))]):_c('p',{staticClass:\"result-amount\"},[_vm._v(_vm._s(_vm.$tc('results', _vm.courseCount)))]),_c('Sort',{attrs:{\"keyword\":_vm.keyword,\"sort\":_vm.sort},on:{\"sort-changed\":_vm.setSorting}},[(_vm.isLoading)?_c('p',{staticClass:\"result-amount\"},[_vm._v(_vm._s(_vm.$t('coursesLoading')))]):_c('p',{staticClass:\"result-amount\"},[_vm._v(_vm._s(_vm.$tc('results', _vm.courseCount)))])])],1),_c('FilterTags',{on:{\"filters-changed\":_vm.setFilters,\"search\":_vm.setKeyword}})],1),_c('CourseList',{staticClass:\"course-list\",attrs:{\"sort\":_vm.sort}})],1)])],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { partition } from 'lodash/fp';\nimport { Ref, SetupContext, onBeforeMount, ref, watch } from '@vue/composition-api';\n\ntype useSearchParamsReturnType = {\n query: Ref;\n filters: Ref;\n keyword: Ref;\n};\n\nconst useSearchParams = (ctx: SetupContext): useSearchParamsReturnType => {\n const query = ref(ctx.root.$route.query.q as string);\n const filters = ref([]);\n const keyword = ref();\n\n const updateValues = () => {\n query.value = ctx.root.$route.query.q as string;\n\n if (!query.value) {\n keyword.value = undefined;\n filters.value = [];\n return;\n }\n\n const [filtersInQuery, keywordsInQuery] = partition(\n (x) => x.includes(':'),\n query.value.split(' ')\n );\n\n keyword.value = keywordsInQuery.length > 0 ? keywordsInQuery.join(' ') : undefined;\n filters.value = filtersInQuery;\n };\n\n watch(\n () => ctx.root.$route,\n () => {\n updateValues();\n }\n );\n\n onBeforeMount(() => {\n updateValues();\n });\n\n return { query, filters, keyword };\n};\n\nexport default useSearchParams;\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"courselist\"}},[(_vm.isLoading)?_c('div',[_c('b-skeleton',{attrs:{\"width\":\"100%\",\"height\":\"20rem\",\"animated\":true}}),_c('b-skeleton',{attrs:{\"width\":\"100%\",\"height\":\"20rem\",\"animated\":true}}),_c('b-skeleton',{attrs:{\"width\":\"100%\",\"height\":\"20rem\",\"animated\":true}})],1):(_vm.courses && _vm.courses.length < 1)?_c('div',[_vm._v(_vm._s(_vm.$t('search.noresults')))]):_vm._e(),_vm._l((_vm.courses),function(course){return _c('CourseCard',{key:course.id,attrs:{\"course\":course,\"participantcount\":course.participantcount},on:{\"add-to-cart\":_vm.addToCart}})}),(_vm.courseCount > _vm.COURSES_ON_PAGE)?_c('b-pagination',{attrs:{\"per-page\":_vm.COURSES_ON_PAGE,\"total\":_vm.courseCount,\"range-before\":\"3\",\"range-after\":\"4\",\"order\":\"is-centered\",\"aria-next-label\":\"Next\",\"aria-previous-label\":\"Previous\",\"aria-page-label\":\"Page\",\"aria-current-label\":\"Current page\"},on:{\"change\":_vm.changePage},model:{value:(_vm.currentPage),callback:function ($$v) {_vm.currentPage=$$v},expression:\"currentPage\"}}):_vm._e()],2)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('div',{staticClass:\"wrapper\"},[_c('section',{staticClass:\"mainSection\"},[_c('div',{staticClass:\"block\"},[_c('h2',{staticClass:\"title\"},[_c('router-link',{attrs:{\"to\":{ name: 'course', params: { id: _vm.course.id } }}},[_vm._v(\" \"+_vm._s(_vm.course.name)+\" \")])],1),_c('p',{staticClass:\"subtitle is-hidden-mobile\"},[_vm._v(\" \"+_vm._s(_vm.course.code)+\" \")])]),(_vm.course.notifications && _vm.course.notifications.length > 0)?_c('div',{staticClass:\"notifications block\",class:{ notificationsColumns: _vm.course.notifications.length > 1 }},_vm._l((_vm.course.notifications),function(notification,i){return _c('b-message',{key:i,attrs:{\"type\":\"is-primary is-light\",\"has-icon\":\"\",\"icon\":\"information\",\"icon-size\":\"is-small\",\"closable\":false}},[_vm._v(\" \"+_vm._s(notification.text)+\" \")])}),1):_vm._e(),_c('section',{staticClass:\"block\"},[_c('CourseInfoDl',{staticClass:\"courseDetails\",class:{ 'mobile-hidden': !_vm.mobileCardOpen },attrs:{\"course\":_vm.course,\"spacing\":true,\"fields\":[\n _vm.CourseInfoDlField.Periods,\n _vm.CourseInfoDlField.Weekdays,\n _vm.CourseInfoDlField.Location,\n _vm.CourseInfoDlField.Teacher\n ],\"locationMapLink\":true,\"locationSingleLine\":false},on:{\"map-modal-open\":function($event){_vm.mapModalOpen = true}}}),_c('CourseInfoDl',{staticClass:\"courseDetails hidden\",class:{ 'mobile-visible': !_vm.mobileCardOpen },attrs:{\"course\":_vm.course,\"spacing\":false,\"fields\":[_vm.CourseInfoDlField.MobileMinimal]}})],1),_c('footer',{staticClass:\"detailsFooter block\",class:{ 'mobile-hidden': !_vm.mobileCardOpen }},[_c('router-link',{attrs:{\"to\":{ name: 'course', params: { id: _vm.course.id } }}},[_vm._v(\" \"+_vm._s(_vm.$t('details'))+\" » \")])],1)]),_c('section',{staticClass:\"cartSection\",class:{ mobileMinimalSection: !_vm.mobileCardOpen }},[_c('div',{staticClass:\"cart-section-content\"},[(_vm.price)?_c('h2',{staticClass:\"title\"},[_vm._v(\" \"+_vm._s(_vm.$n(_vm.price, 'currency'))+\" \")]):_vm._e(),(_vm.showDetailedPrice)?_c('p',{staticClass:\"subtitle\"},[_vm._v(\" \"+_vm._s(_vm.$t('cart.viewdetails'))+\" \")]):_vm._e(),(\n (_vm.course.registrationbegins ||\n _vm.course.registrationendshard ||\n _vm.course.registrationendssoft) &&\n !_vm.course.statuses.includes(_vm.HellewiCourseStatus.RegistrationToLessons)\n )?_c('div',{staticClass:\"add-to-cart-availibility\"},[(_vm.addButtonShown)?_c('b-button',{staticClass:\"addToCartButton\",attrs:{\"type\":\"is-primary\",\"disabled\":_vm.addButtonDisabled,\"icon-left\":\"cart\"},on:{\"click\":function($event){return _vm.addToCart(_vm.course.id)}}},[_vm._v(\" \"+_vm._s(_vm.$t('cart.addtocart'))+\" \")]):_vm._e()],1):_c('router-link',{attrs:{\"to\":{ name: 'course', params: { id: _vm.course.id } }}},[_c('b-button',{staticClass:\"addToCartButton\",attrs:{\"type\":\"is-primary\",\"icon-left\":\"calendar\"}},[_vm._v(\" \"+_vm._s(_vm.course.statuses.includes(_vm.HellewiCourseStatus.RegistrationToLessons) ? _vm.$t('cart.checktimes') : _vm.$t('registration.details'))+\" \")])],1),_c('CourseInfoDl',{class:{ 'mobile-hidden': !_vm.mobileCardOpen },attrs:{\"course\":_vm.course,\"spacing\":true,\"fields\":[_vm.CourseInfoDlField.RegistrationTime]}}),(\n _vm.course.registrationbegins ||\n _vm.course.registrationendshard ||\n _vm.course.registrationendssoft ||\n _vm.course.statuses.includes(_vm.HellewiCourseStatus.Cancelled) ||\n _vm.course.statuses.includes(_vm.HellewiCourseStatus.Interrupted)\n )?_c('div',{staticClass:\"cart-availability\"},[(_vm.participantcount)?_c('CourseAvailability',{staticClass:\"availability-container\",class:{ mobileMinimalSection: !_vm.mobileCardOpen },attrs:{\"participantcount\":_vm.participantcount,\"statuses\":_vm.course.statuses}}):_vm._e()],1):_vm._e()],1)]),_c('b-button',{staticClass:\"mobile-toggle-open visible-only-mobile\",attrs:{\"type\":\"is-ghost\"},on:{\"click\":_vm.toggleMobileCardOpen}},[_vm._v(\" \"+_vm._s(_vm.$t(_vm.mobileCardOpen ? 'showLess' : 'showMore'))+\" \"),_c('b-icon',{attrs:{\"icon\":_vm.mobileCardOpen ? 'chevron-up' : 'chevron-down'}})],1)],1)]),(_vm.course.location && _vm.course.location.latlon)?_c('b-modal',{staticClass:\"modal\",attrs:{\"aria-label\":\"Map Modal\",\"aria-modal\":\"\"},model:{value:(_vm.mapModalOpen),callback:function ($$v) {_vm.mapModalOpen=$$v},expression:\"mapModalOpen\"}},[_c('div',{staticClass:\"modal-wrapper\"},[_c('CourseMap',{staticClass:\"course-map\",attrs:{\"coordinates\":_vm.course.location.latlon,\"text\":_vm.course.location.name}}),_c('b-button',{staticClass:\"closeBtn\",attrs:{\"type\":\"is-primary\"},on:{\"click\":function($event){_vm.mapModalOpen = false}}},[_vm._v(\" \"+_vm._s(_vm.$t('close'))+\" \")])],1)]):_vm._e()],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseCard.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseCard.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CourseCard.vue?vue&type=template&id=b8132052&scoped=true\"\nimport script from \"./CourseCard.vue?vue&type=script&lang=ts\"\nexport * from \"./CourseCard.vue?vue&type=script&lang=ts\"\nimport style0 from \"./CourseCard.vue?vue&type=style&index=0&id=b8132052&prod&lang=scss&scoped=true\"\nimport style1 from \"./CourseCard.vue?vue&type=style&index=1&id=b8132052&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"b8132052\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseList.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CourseList.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CourseList.vue?vue&type=template&id=22655846\"\nimport script from \"./CourseList.vue?vue&type=script&lang=ts\"\nexport * from \"./CourseList.vue?vue&type=script&lang=ts\"\nimport style0 from \"./CourseList.vue?vue&type=style&index=0&id=22655846&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"search-filters\",attrs:{\"id\":\"search-filters\"}},[_vm._l((_vm.filterGroups),function(filterGroup){return _c('div',{key:filterGroup.name,staticClass:\"filtergroup\"},[_c('header',{attrs:{\"role\":\"button\",\"aria-controls\":filterGroup.name},on:{\"click\":function($event){return _vm.toggleGroupOpen(filterGroup.name)}}},[_c('div',[_c('h4',{staticClass:\"filtergroup-label\"},[_vm._v(\" \"+_vm._s(_vm.$t(filterGroup.name))+\" \")])]),_c('aside',{staticClass:\"filter-amount\"},[_c('b-button',{staticClass:\"filtergroup-button\",attrs:{\"aria-label\":_vm.getFilterGroupToggleButtonLabel(filterGroup.name)}},[_c('b-icon',{attrs:{\"icon\":_vm.isGroupOpen(filterGroup.name) ? 'chevron-up' : 'chevron-down',\"size\":\"is-medium\"}})],1)],1)]),_c('b-collapse',{attrs:{\"aria-role\":\"list\",\"animation\":\"slide\",\"aria-id\":filterGroup.name,\"open\":_vm.isGroupOpen(filterGroup.name)}},[_c('div',{staticClass:\"filters\"},_vm._l((filterGroup.filters),function(filter){return _c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(!filter.disabled),expression:\"!filter.disabled\"}],key:filter.inputId,class:[\n 'filter',\n filterGroup.type,\n {\n 'has-children': filter.subFilters.length > 0\n }\n ],attrs:{\"aria-role\":\"listitem\"}},[(filter.type === _vm.HellewiCatalogItemType.Dateinput)?_c('div',{key:\"coursesbeginningdate\",class:['filter', 'date'],attrs:{\"aria-role\":\"listitem\"}},[_c('div',{staticClass:\"filter-container\"},[_c('div',{staticClass:\"input-container\"},[_c('b-field',{attrs:{\"label\":_vm.$t((\"\" + (filter.name)))}},[_c('b-datepicker',{attrs:{\"placeholder\":_vm.$t((\"\" + (filter.name))),\"icon\":\"calendar-today\",\"icon-right-clickable\":\"\",\"trap-focus\":\"\",\"icon-right\":'close-circle',\"value\":_vm.getDatefilterValue(filter.name),\"first-day-of-week\":1,\"years-range\":[-1, 1]},on:{\"icon-right-click\":function($event){return _vm.clearDate(filter)},\"input\":function($event){return _vm.dateChanged(filter, $event)}}})],1)],1)])]):_vm._e(),(filter.type !== _vm.HellewiCatalogItemType.Dateinput)?_c('div',{staticClass:\"filter-container\",on:{\"change\":function($event){return _vm.filterClick(filter)}}},[_c('div',{staticClass:\"input-container\"},[_c('b-checkbox',{class:[{ 'checkbox-with-subfilters': filter.subFilters.length > 0 }],attrs:{\"id\":filter.inputId,\"type\":filterGroup.type,\"value\":_vm.getFilterCheckboxValue(filter),\"native-value\":filter.keyword,\"disabled\":filter.disabled,\"indeterminate\":_vm.isIndeterminateFilter(filter)}},[_vm._v(\" \"+_vm._s(filter.translatelabel ? _vm.$t(filter.name) : filter.name)+\" \")]),(filter.subFilters.length > 0)?_c('b-button',{staticClass:\"subfilter-button\",attrs:{\"aria-label\":_vm.getFilterGroupToggleButtonLabel(filterGroup.name)},on:{\"click\":function($event){$event.stopPropagation();_vm.toggleSubfilterGroup(\n filter.keyword,\n filter.subFilters,\n !_vm.openSubFilters.find(function (key) { return key === filter.keyword; })\n )}}},[_c('b-icon',{attrs:{\"custom-class\":\"subfilter-toggle\",\"icon\":_vm.openSubFilters.find(function (key) { return key === filter.keyword; })\n ? 'chevron-up'\n : 'chevron-down'}})],1):_vm._e()],1),(filter.type !== _vm.HellewiCatalogItemType.Date)?_c('aside',{staticClass:\"filter-amount\"},[_vm._v(\" \"+_vm._s(filter.courseCount)+\" \")]):_vm._e()]):_vm._e(),(_vm.openSubFilters.find(function (key) { return key === filter.keyword; }))?_c('div',{staticClass:\"subfilters\"},_vm._l((filter.subFilters),function(subFilter){return _c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(!subFilter.disabled),expression:\"!subFilter.disabled\"}],key:subFilter.keyword,staticClass:\"filter-container\",class:['filter', filterGroup.type]},[_c('div',{staticClass:\"input-container\",on:{\"change\":function($event){return _vm.filterClick(subFilter)}}},[_c('b-checkbox',{attrs:{\"id\":subFilter.inputId,\"type\":filterGroup.type,\"value\":_vm.getSubFilterCheckboxValue(filter, subFilter),\"native-value\":subFilter.keyword,\"disabled\":subFilter.disabled}},[_vm._v(\" \"+_vm._s(subFilter.name)+\" \")])],1),_c('aside',{staticClass:\"filter-amount\"},[_vm._v(_vm._s(subFilter.courseCount))])])}),0):_vm._e()])}),0)])],1)}),_c('footer',[_c('a',{staticClass:\"reset-filter\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.resetFilterGroups()}}},[_vm._v(\" \"+_vm._s(_vm.$t('deselectFilters'))+\" \")])])],2)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { computed, onMounted, Ref, ref, SetupContext, watch } from '@vue/composition-api';\nimport { isValid, parse } from 'date-fns';\nimport { difference, mapValues, sumBy, uniqBy, values } from 'lodash/fp';\nimport { HellewiCatalog, HellewiCatalogItem, HellewiCatalogItemType } from '../api';\nimport { useCatalogSettings, useGetCatalog, useGetCatalogUnfiltered } from './useCatalogApi';\nimport useSearchParams from './useSearchParams';\n\nimport { translate } from '../utils/misc-utils';\n\nexport interface FilterGroup {\n filters: Filter[];\n name: string;\n type: string;\n}\n\nexport interface Filter {\n keyword: string;\n name: string;\n inputId: string;\n disabled: boolean;\n subFilters: Filter[];\n courseCount: number;\n parent?: string; // Parent keyword\n siblings: string[]; // Sibling keyword,\n type?: HellewiCatalogItemType;\n translatelabel?: boolean;\n}\n\nconst filterNames: Record = {}; // resetFilterGroupsia varten\n\n// empty string as keyword probably breaks something, filter those\n// out later\nconst getKeyword = (catalogItem: HellewiCatalogItem): string =>\n catalogItem.keywords && catalogItem.keywords.length > 0 ? catalogItem.keywords[0] : '';\n\n// Make these refs 'global' by scoping them out of useCatalogFilters scope\nconst filterGroups = ref([]);\nconst selected = ref([]);\nconst selectedParents = ref([]);\n\ntype catalogFiltersReturnType = {\n resetCatalogFilterGroups: () => void;\n filterGroups: Ref;\n selected: Ref;\n isGroupOpen: (groupName: string) => boolean;\n toggleGroupOpen: (groupName: string) => void;\n selectedParents: Ref;\n openSubFilters: Ref;\n toggleSubfilterGroup: (\n keyword: string,\n subFilters: Filter[],\n desiredGroupOpenState?: boolean | null\n ) => void;\n getFilterTags: () => Filter[];\n findFiltersForKeyword: (catalogFilters: Filter[], keyword: string) => Filter[];\n getFilterGroupIndividualFilters: () => Filter[];\n parseDateinputFilter: (\n keyword: string,\n ctx?: SetupContext\n ) => { dateStr: string | false; codeStr: string; comparatorStr: string };\n};\n\nexport const useCatalogFilters = (ctx: SetupContext): catalogFiltersReturnType => {\n const { filters, query: queryString } = useSearchParams(ctx);\n const { response: settings, execute: getCatalogSettings } = useCatalogSettings();\n const { response: catalog, execute: getCatalog } = useGetCatalog();\n const { response: catalogUnfiltered, execute: getCatalogUnfiltered } = useGetCatalogUnfiltered();\n\n const openSubFilters = ref([]);\n\n // by default all groups are closed except the ones listed below\n const closedGroups = ref(\n localStorage.getItem('closed_filtergroups')?.split('|') ||\n difference(\n [...values(HellewiCatalogItemType), HellewiCatalogItemType.Categorysubject],\n [\n HellewiCatalogItemType.Tag,\n HellewiCatalogItemType.Teachingformat,\n HellewiCatalogItemType.Department,\n HellewiCatalogItemType.Category,\n HellewiCatalogItemType.Subject,\n HellewiCatalogItemType.Language,\n HellewiCatalogItemType.Levelofstudy,\n HellewiCatalogItemType.Unit,\n HellewiCatalogItemType.Categorysubject\n ]\n )\n );\n\n const combinedCatalog = computed(() => {\n if (!catalogUnfiltered.value || !catalog.value) {\n return;\n }\n\n return mapValues((catalogItems: HellewiCatalogItem[]) => {\n const filtered: HellewiCatalogItem[] = Object.values(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (catalog.value as any) as HellewiCatalogItem[]\n ).flat();\n\n return catalogItems.map((ci: HellewiCatalogItem) => {\n const found = filtered.find(\n (c: HellewiCatalogItem) => c.keywords?.[0] === ci.keywords?.[0]\n );\n return found ? found : { ...ci, coursecount: 0 };\n });\n })(catalogUnfiltered.value) as HellewiCatalog;\n });\n\n // Toggle subFilterGroup open / closed\n const toggleSubfilterGroup = (\n keyword: string,\n subFilters: Filter[],\n desiredGroupOpenState: boolean | null = null\n ) => {\n if (\n desiredGroupOpenState === false ||\n (desiredGroupOpenState === null &&\n (openSubFilters.value.find((key) => key === keyword) ||\n subFilters.some(({ disabled }) => !disabled)))\n ) {\n // Close group\n openSubFilters.value = openSubFilters.value.filter(\n (subFilterKeyword) => subFilterKeyword !== keyword\n );\n subFilters.forEach((subFilter) => (subFilter.disabled = true));\n return;\n }\n // Open group\n openSubFilters.value.push(keyword);\n subFilters.forEach((subFilter) => (subFilter.disabled = false));\n };\n\n const filterShouldBeHidden = (keyword: string, coursecount?: number) =>\n coursecount === 0 && !selected.value.includes(keyword);\n\n const catalogItemToFilter = (\n catalogItem: HellewiCatalogItem,\n subCatalogItems: HellewiCatalogItem[]\n ): Filter => {\n const keyword = getKeyword(catalogItem);\n\n if (catalogItem) {\n if (!catalogItem.parent && !(catalogItem.type in filterNames)) {\n filterNames[catalogItem.type] = [catalogItem.type];\n } else if (catalogItem.parent) {\n const parent = catalogItem.parent.split(':')[0];\n if (parent in filterNames && !filterNames[parent].includes(catalogItem.type)) {\n filterNames[parent].push(catalogItem.type);\n }\n }\n }\n\n const subFilters = subCatalogItems\n .filter((sci) => sci.parent === keyword)\n .map((x) => catalogItemToFilter(x, []))\n .filter(\n ({ courseCount, keyword: kw }) =>\n !filterShouldBeHidden(kw, courseCount) ||\n (catalogItem.parent && selectedParents.value.includes(catalogItem.parent))\n )\n .map((x: Filter) => {\n return { ...x, disabled: !openSubFilters.value.includes(keyword) };\n });\n\n const subFilterCourseCountSum = sumBy((f) => f.courseCount, subFilters);\n\n // Generate siblings for subFilters\n subFilters.forEach(\n (subFilter) =>\n (subFilter.siblings = subFilters\n .filter(({ keyword: kw }) => subFilter.keyword !== kw)\n .map(({ keyword: kw }) => kw))\n );\n\n // Open subFilterGroup if a subFilter is active\n if (\n subFilters.some(\n ({ keyword: kw, parent }) =>\n parent && selected.value.includes(kw) && !openSubFilters.value.includes(parent)\n )\n ) {\n toggleSubfilterGroup(keyword, subFilters, true);\n }\n\n return {\n inputId: `input-${keyword}`,\n name: catalogItem.name,\n keyword,\n disabled:\n subFilters.length > 0\n ? subFilterCourseCountSum === 0\n : filterShouldBeHidden(keyword, catalogItem.coursecount),\n courseCount: subFilters.length > 0 ? subFilterCourseCountSum : catalogItem.coursecount || 0,\n parent: catalogItem.parent,\n subFilters,\n siblings: [],\n type: catalogItem.type,\n translatelabel: catalogItem.translatelabel\n };\n };\n\n const fetchFilters = (): FilterGroup[] => {\n if (!combinedCatalog.value || !catalogUnfiltered.value) {\n return [];\n }\n const {\n subject,\n department,\n category,\n period,\n weekday,\n location,\n tag,\n teachingformat,\n language,\n levelofstudy,\n educationsector,\n unit,\n educationtype,\n locationgroup,\n coursetype,\n date\n } = combinedCatalog.value;\n\n const updateCourseCounts = (\n catalogItems: HellewiCatalogItem[],\n subCatalogItems: HellewiCatalogItem[]\n ): Filter[] => {\n return catalogItems.map((x) => catalogItemToFilter(x, subCatalogItems));\n };\n\n const filterGroups = [\n {\n name: HellewiCatalogItemType.Date,\n type: 'checkbox',\n filters: updateCourseCounts(\n date.filter((f) => {\n return (\n (f.name === 'begins' && settings.value?.enabledcatalogitemtypes['date']) ||\n settings.value?.enabledcatalogitemtypes[f.name]\n );\n }),\n []\n )\n },\n {\n name: HellewiCatalogItemType.Tag,\n type: 'checkbox',\n filters: updateCourseCounts(tag, [])\n },\n {\n name: HellewiCatalogItemType.Teachingformat,\n type: 'checkbox',\n filters: updateCourseCounts(teachingformat, [])\n },\n {\n name: HellewiCatalogItemType.Unit,\n type: 'checkbox',\n filters: updateCourseCounts(unit, [])\n },\n {\n name: HellewiCatalogItemType.Department,\n type: 'checkbox',\n filters: updateCourseCounts(department, [])\n },\n\n {\n name: HellewiCatalogItemType.Category,\n type: 'checkbox',\n filters: updateCourseCounts(\n category.filter((c) => !c.parent),\n []\n )\n },\n {\n name: 'categorysubject',\n type: 'checkbox',\n filters: updateCourseCounts(\n category.filter((c) => !c.parent),\n subject\n )\n },\n {\n name: HellewiCatalogItemType.Subject,\n type: 'checkbox',\n filters: updateCourseCounts(\n subject.filter((s) => !s.parent),\n []\n )\n },\n {\n name: HellewiCatalogItemType.Language,\n type: 'checkbox',\n filters: updateCourseCounts(language, [])\n },\n {\n name: HellewiCatalogItemType.Levelofstudy,\n type: 'checkbox',\n filters: updateCourseCounts(levelofstudy, [])\n },\n {\n name: HellewiCatalogItemType.Educationsector,\n type: 'checkbox',\n filters: updateCourseCounts(educationsector, [])\n },\n {\n name: HellewiCatalogItemType.Period,\n type: 'checkbox',\n filters: updateCourseCounts(period, [])\n },\n {\n name: HellewiCatalogItemType.Weekday,\n type: 'checkbox',\n filters: updateCourseCounts(weekday, [])\n },\n {\n name: HellewiCatalogItemType.Locationgroup,\n type: 'checkbox',\n filters: updateCourseCounts(locationgroup, [])\n },\n {\n name: HellewiCatalogItemType.Location,\n type: 'checkbox',\n filters: updateCourseCounts(location, [])\n },\n {\n name: HellewiCatalogItemType.Educationtype,\n type: 'checkbox',\n filters: updateCourseCounts(educationtype, [])\n },\n {\n name: HellewiCatalogItemType.Coursetype,\n type: 'checkbox',\n filters: updateCourseCounts(coursetype, [])\n }\n ].filter(\n (filterGroup) =>\n (filterGroup.name === 'date' && filterGroup.filters.length > 0) ||\n settings.value?.enabledcatalogitemtypes[filterGroup.name]\n );\n return filterGroups;\n };\n\n const updateFilterGroups = () => {\n if (settings.value && catalog.value && catalogUnfiltered.value) {\n filterGroups.value = fetchFilters();\n }\n };\n\n const resetCatalogFilterGroups = () => {\n selected.value = [];\n selectedParents.value = [];\n ctx.emit('filters-changed', selected.value);\n };\n\n const isGroupOpen = (groupName: string) => !closedGroups.value.includes(groupName);\n\n const toggleGroupOpen = (groupName: string) => {\n if (closedGroups.value.includes(groupName)) {\n closedGroups.value = closedGroups.value.filter((fg) => fg !== groupName);\n } else {\n closedGroups.value.push(groupName);\n }\n\n localStorage.setItem('closed_filtergroups', closedGroups.value.join('|'));\n };\n\n // Break down filter group filters into single array of filters\n const getFilterGroupIndividualFilters = () =>\n filterGroups.value\n .flatMap(({ filters: filterGroupFilters }) => filterGroupFilters)\n .flatMap((filter) => (filter.siblings ? [filter, ...filter.subFilters] : [filter]));\n\n const findFiltersForKeyword = (catalogFilters: Filter[], keyword: string) => {\n return catalogFilters.filter((filter) => {\n const filteredFilters =\n (filter.type !== HellewiCatalogItemType.Dateinput && filter.keyword === keyword) ||\n (filter.type === HellewiCatalogItemType.Dateinput &&\n filter.keyword.indexOf(keyword.slice(0, keyword.indexOf(':'))) !== -1 &&\n filter.keyword.indexOf('currentdate') === -1);\n\n if (\n filter.type === HellewiCatalogItemType.Dateinput &&\n filter.keyword.indexOf(keyword.slice(0, keyword.indexOf(':'))) !== -1 &&\n filter.keyword !== keyword\n // &&\n // parseDateinputFilter(keyword).dateStr\n ) {\n filter.keyword = keyword;\n }\n return filteredFilters;\n });\n };\n\n const parseDateinputFilter = (\n keyword: string,\n ctx?: SetupContext\n ): { dateStr: string | false; codeStr: string; comparatorStr: string } => {\n const code = keyword.slice(0, keyword.indexOf(':'));\n const rest = keyword.slice(keyword.indexOf(':'));\n\n // eslint-disable-next-line no-useless-escape\n const match = rest.match(/\\:([!<>=]*)(.*)/);\n\n const comparatorMatch = match ? match[1] : '';\n const dateMatch = match ? match[2] : '';\n\n const codeStr = ctx ? translate(ctx, `filterTags.${code}`) : '';\n const comparatorStr = ctx ? translate(ctx, `filterTags.${comparatorMatch}`) : '';\n const dateIsValid = isValid(parse(dateMatch, 'yyyy-MM-dd', new Date()));\n const dateStr = dateIsValid\n ? parse(dateMatch, 'yyyy-MM-dd', new Date()).toLocaleDateString()\n : false;\n\n return { dateStr, codeStr, comparatorStr };\n };\n\n const getFilterTags = () => {\n const currentFilters = getFilterGroupIndividualFilters();\n const tagsWithDuplicates = selectedParents.value\n .concat(selected.value)\n .flatMap((keyword) => findFiltersForKeyword(currentFilters, keyword))\n // Fetch parent tags for subfilters\n .flatMap((filter) =>\n filter.parent?.includes('category:')\n ? findFiltersForKeyword(currentFilters, filter.parent).concat([filter])\n : [filter]\n )\n .flatMap((filter) => (filter ? [filter] : []))\n // If all sibling filters are active displaying only the parent tag will suffice\n .filter(\n ({ siblings }) =>\n !(\n siblings.length > 0 &&\n siblings.every((siblingKeyword) => selected.value.includes(siblingKeyword))\n )\n )\n // Don't show subfilter tag if it's the only child for the parent filter\n .filter(({ parent, siblings }) => !(parent?.includes('category:') && siblings.length === 0))\n .sort((a, b) => (a.subFilters.length > b.subFilters.length ? -1 : 1));\n return uniqBy('name', tagsWithDuplicates);\n };\n\n watch(filters, () => {\n selected.value = filters.value;\n });\n watch(queryString, () => getCatalog({ q: queryString.value }));\n watch([catalog, settings, catalogUnfiltered], () => {\n updateFilterGroups();\n });\n\n onMounted(() => {\n getCatalogSettings();\n getCatalogUnfiltered();\n getCatalog({ q: queryString.value });\n\n selected.value = filters.value;\n\n // This needs to be here for hot-reloading to work\n updateFilterGroups();\n });\n\n return {\n resetCatalogFilterGroups,\n filterGroups,\n selected,\n isGroupOpen,\n toggleGroupOpen,\n selectedParents,\n openSubFilters,\n toggleSubfilterGroup,\n getFilterTags,\n findFiltersForKeyword,\n getFilterGroupIndividualFilters,\n parseDateinputFilter\n };\n};\n","\n\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CatalogFilters.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CatalogFilters.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./CatalogFilters.vue?vue&type=template&id=0cf8edb2&scoped=true\"\nimport script from \"./CatalogFilters.vue?vue&type=script&lang=ts\"\nexport * from \"./CatalogFilters.vue?vue&type=script&lang=ts\"\nimport style0 from \"./CatalogFilters.vue?vue&type=style&index=0&id=0cf8edb2&prod&lang=scss&scoped=true\"\nimport style1 from \"./CatalogFilters.vue?vue&type=style&index=1&id=0cf8edb2&prod&global=true&lang=css\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"0cf8edb2\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"filters\"},[_c('div',{staticClass:\"dropdowns\"},[_c('div',{staticClass:\"filters-container\"},[_c('b-button',{staticClass:\"filtergroup-button\",attrs:{\"type\":\"is-primary\",\"outlined\":\"\",\"role\":\"button\"},on:{\"click\":function($event){_vm.filterModalOpen = true}}},[(_vm.filterCount && _vm.filterCount > 0)?_c('span',[_vm._v(\" \"+_vm._s(_vm.$t('filtersActive'))+\" \")]):_c('span',[_vm._v(\" \"+_vm._s(_vm.$t('filter'))+\" \")]),(_vm.filterCount && _vm.filterCount > 0)?_c('span',[_vm._v(\"(\"+_vm._s(_vm.filterCount)+\")\")]):_vm._e()]),_c('b-modal',{staticClass:\"filter-modal\",attrs:{\"aria-role\":\"dialog\",\"has-modal-card\":\"\",\"full-screen\":\"\",\"destroy-on-hide\":false,\"can-cancel\":false,\"aria-modal\":\"\",\"aria-label\":\"Filters\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_c('div',{staticClass:\"modal-card\",staticStyle:{\"width\":\"auto\"}},[_c('section',{staticClass:\"modal-card-body\"},[_vm._t(\"filters\")],2),_c('footer',{staticClass:\"modal-card-foot\"},[_c('b-button',{attrs:{\"type\":\"is-primary\",\"label\":_vm.$t('seeResults')},on:{\"click\":props.close}})],1)])]}}],null,true),model:{value:(_vm.filterModalOpen),callback:function ($$v) {_vm.filterModalOpen=$$v},expression:\"filterModalOpen\"}})],1),_vm._t(\"sorting\")],2),_vm._t(\"tags\")],2)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./MobileFilters.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./MobileFilters.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./MobileFilters.vue?vue&type=template&id=0b4d5596&scoped=true\"\nimport script from \"./MobileFilters.vue?vue&type=script&lang=ts\"\nexport * from \"./MobileFilters.vue?vue&type=script&lang=ts\"\nimport style0 from \"./MobileFilters.vue?vue&type=style&index=0&id=0b4d5596&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"0b4d5596\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.promotions && _vm.promotions.length > 0)?_c('section',{staticClass:\"wrapper\"},[_c('b-carousel-list',{staticClass:\"carousel\",attrs:{\"data\":_vm.promotions,\"items-to-show\":_vm.cardsShown,\"icon-size\":\"is-medium\"},scopedSlots:_vm._u([{key:\"item\",fn:function(promotion){return [_c('a',{attrs:{\"href\":promotion.url}},[_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-image\"},[_c('figure',{staticClass:\"image\"},[_c('b-image',{attrs:{\"ratio\":\"2by1\",\"src\":promotion.image,\"alt\":promotion.text}})],1)]),_c('div',{staticClass:\"card-content\"},[_c('div',{staticClass:\"content\"},[_c('v-clamp',{staticClass:\"title is-6\",attrs:{\"tag\":\"p\",\"autoresize\":\"\",\"max-lines\":2}},[_vm._v(\" \"+_vm._s(promotion.text)+\" \")]),_c('b-icon',{staticClass:\"arrow\",attrs:{\"icon\":\"chevron-right\"}})],1)])])])]}}],null,false,759233979)})],1):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./PromotionCarousel.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./PromotionCarousel.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./PromotionCarousel.vue?vue&type=template&id=4d4e6ddd&scoped=true\"\nimport script from \"./PromotionCarousel.vue?vue&type=script&lang=ts\"\nexport * from \"./PromotionCarousel.vue?vue&type=script&lang=ts\"\nimport style0 from \"./PromotionCarousel.vue?vue&type=style&index=0&id=4d4e6ddd&prod&scoped=true&lang=scss\"\nimport style1 from \"./PromotionCarousel.vue?vue&type=style&index=1&id=4d4e6ddd&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"4d4e6ddd\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('form',{staticClass:\"search-input\",on:{\"submit\":function($event){$event.preventDefault();return _vm.search($event)}}},[_c('b-field',[_c('b-input',{staticClass:\"search-input\",attrs:{\"id\":_vm.id,\"type\":\"search\",\"placeholder\":_vm.placeholder,\"icon-right\":_vm.iconRight ? 'magnify' : '',\"icon-right-clickable\":true},on:{\"input\":_vm.updateValue,\"icon-right-click\":function($event){_vm.iconRight ? _vm.search() : null},\"keyup\":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }return _vm.search($event)}},model:{value:(_vm.inputValue),callback:function ($$v) {_vm.inputValue=$$v},expression:\"inputValue\"}})],1)],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./SearchInput.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./SearchInput.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./SearchInput.vue?vue&type=template&id=7bd60962\"\nimport script from \"./SearchInput.vue?vue&type=script&lang=ts\"\nexport * from \"./SearchInput.vue?vue&type=script&lang=ts\"\nimport style0 from \"./SearchInput.vue?vue&type=style&index=0&id=7bd60962&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"sort-container\"},[_c('b-dropdown',{attrs:{\"value\":_vm.sortModel,\"aria-role\":\"list\"},on:{\"change\":_vm.setSort},scopedSlots:_vm._u([{key:\"trigger\",fn:function(){return [_c('b-button',{staticClass:\"sort-title\"},[(_vm.sortModel && _vm.sortModel.field)?_c('span',[_vm._v(\" \"+_vm._s(_vm.$t((\"order.\" + (_vm.sortModel.field) + (_vm.sortModel.dir))))+\" \")]):_c('span',[_vm._v(\" \"+_vm._s(_vm.$t('order.mostrelevant'))+\" \")]),_c('b-icon',{attrs:{\"icon\":\"chevron-down\",\"size\":\"is-medium\"}})],1)]},proxy:true}])},[(_vm.keyword)?_c('b-dropdown-item',{attrs:{\"href\":\"#\",\"value\":null,\"aria-role\":\"listitem\"}},[_vm._v(\" \"+_vm._s(_vm.$t('order.mostrelevant'))+\" \")]):_vm._e(),_c('b-dropdown-item',{attrs:{\"href\":\"#\",\"value\":{ field: _vm.CourseSortfield.Name, dir: _vm.Sortdir.Asc },\"aria-role\":\"listitem\"}},[_vm._v(\" \"+_vm._s(_vm.$t('order.nameasc'))+\" \")]),_c('b-dropdown-item',{attrs:{\"href\":\"#\",\"value\":{ field: _vm.CourseSortfield.Name, dir: _vm.Sortdir.Desc },\"aria-role\":\"listitem\"}},[_vm._v(\" \"+_vm._s(_vm.$t('order.namedesc'))+\" \")]),_c('b-dropdown-item',{attrs:{\"href\":\"#\",\"value\":{ field: _vm.CourseSortfield.Begins, dir: _vm.Sortdir.Asc },\"aria-role\":\"listitem\"}},[_vm._v(\" \"+_vm._s(_vm.$t('order.beginsasc'))+\" \")]),_c('b-dropdown-item',{attrs:{\"href\":\"#\",\"value\":{ field: _vm.CourseSortfield.Begins, dir: _vm.Sortdir.Desc },\"aria-role\":\"listitem\"}},[_vm._v(\" \"+_vm._s(_vm.$t('order.beginsdesc'))+\" \")]),_c('b-dropdown-item',{attrs:{\"href\":\"#\",\"value\":{ field: _vm.CourseSortfield.Code, dir: _vm.Sortdir.Asc },\"aria-role\":\"listitem\"}},[_vm._v(\" \"+_vm._s(_vm.$t('order.codeasc'))+\" \")]),_c('b-dropdown-item',{attrs:{\"href\":\"#\",\"value\":{ field: _vm.CourseSortfield.Code, dir: _vm.Sortdir.Desc },\"aria-role\":\"listitem\"}},[_vm._v(\" \"+_vm._s(_vm.$t('order.codedesc'))+\" \")])],1)],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Sort.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Sort.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Sort.vue?vue&type=template&id=bd8bd76e&scoped=true\"\nimport script from \"./Sort.vue?vue&type=script&lang=ts\"\nexport * from \"./Sort.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Sort.vue?vue&type=style&index=0&id=bd8bd76e&prod&lang=scss&scoped=true\"\nimport style1 from \"./Sort.vue?vue&type=style&index=1&id=bd8bd76e&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"bd8bd76e\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.brand)?_c('section',{style:({ backgroundImage: (\"url('\" + (_vm.brand.heroimage) + \"')\") })},[_c('div',{staticClass:\"gradient\"},[_c('div',{staticClass:\"wrapper\"},[_c('h2',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.brand.herotitle))]),_c('p',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.brand.herotext),expression:\"brand.herotext\"}],staticClass:\"subtitle\"}),_c('div',{staticClass:\"content\"},[_c('label',{staticClass:\"is-sr-only\",attrs:{\"for\":\"hero-search\"}},[_vm._v(_vm._s(_vm.$t('search.label')))]),_c('SearchInput',{attrs:{\"id\":\"hero-search\",\"placeholder\":_vm.$t('herosearchplaceholder')},on:{\"updatevalue\":_vm.updateValue,\"search\":_vm.search}}),_c('b-button',{attrs:{\"type\":\"is-primary\"},on:{\"click\":_vm.search}},[_vm._v(_vm._s(_vm.$t('search.search')))])],1)])])]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Hero.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Hero.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Hero.vue?vue&type=template&id=56b1ad34&scoped=true\"\nimport script from \"./Hero.vue?vue&type=script&lang=ts\"\nexport * from \"./Hero.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Hero.vue?vue&type=style&index=0&id=56b1ad34&prod&lang=scss&scoped=true\"\nimport style1 from \"./Hero.vue?vue&type=style&index=1&id=56b1ad34&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"56b1ad34\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"tags-container\"},[(_vm.tags.length > 0)?_c('b-taglist',{staticClass:\"tags\"},[_vm._l((_vm.tags),function(tag){return _c('b-tag',{key:tag.keyword,attrs:{\"type\":\"is-primary\",\"closable\":\"\",\"aria-close-label\":\"Close tag\"},on:{\"close\":function($event){return _vm.removeFilter(tag)}},nativeOn:{\"keyup\":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }return _vm.removeFilter(tag)}}},[_vm._v(\" \"+_vm._s(_vm.getTagText(tag))+\" \")])}),_c('a',{staticClass:\"reset-filter\",attrs:{\"href\":\"#\"},on:{\"click\":function($event){$event.preventDefault();return _vm.resetAllFilters($event)}}},[_vm._v(\" \"+_vm._s(_vm.$t('deselectFilters'))+\" \")])],2):_vm._e()],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./FilterTags.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./FilterTags.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./FilterTags.vue?vue&type=template&id=cb81c894&scoped=true\"\nimport script from \"./FilterTags.vue?vue&type=script&lang=ts\"\nexport * from \"./FilterTags.vue?vue&type=script&lang=ts\"\nimport style0 from \"./FilterTags.vue?vue&type=style&index=0&id=cb81c894&prod&lang=scss&scoped=true\"\nimport style1 from \"./FilterTags.vue?vue&type=style&index=1&id=cb81c894&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"cb81c894\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('transition',{attrs:{\"name\":\"fade\"}},[(_vm.scrollButtonVisible)?_c('b-button',{staticClass:\"scrollButton\",attrs:{\"icon-left\":\"arrow-up\",\"rounded\":\"\"},on:{\"click\":_vm.scrollToFilters}},[_vm._v(\" \"+_vm._s(_vm.$t('Takaisin ylös'))+\" \")]):_vm._e()],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./ScrollToFilters.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./ScrollToFilters.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./ScrollToFilters.vue?vue&type=template&id=36c2b44d\"\nimport script from \"./ScrollToFilters.vue?vue&type=script&lang=ts\"\nexport * from \"./ScrollToFilters.vue?vue&type=script&lang=ts\"\nimport style0 from \"./ScrollToFilters.vue?vue&type=style&index=0&id=36c2b44d&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Home.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Home.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Home.vue?vue&type=template&id=eda744c6&scoped=true\"\nimport script from \"./Home.vue?vue&type=script&lang=ts\"\nexport * from \"./Home.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Home.vue?vue&type=style&index=0&id=eda744c6&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"eda744c6\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.registration)?_c('div',{staticClass:\"registration\"},[_c('Registration',{attrs:{\"registration\":_vm.registration,\"titleTranslationKey\":_vm.titleTranslationKey,\"showRegistrationPrices\":false,\"showInvoices\":true,\"showPaymentColumn\":true}})],1):(_vm.hasError)?_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h1',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('payment.title.error')))]),_c('p',{staticClass:\"mt-4\"},[_vm._v(_vm._s(_vm.$t('registration.getRegistrationError')))])])]):(_vm.isLoading)?_c('section',{staticClass:\"registration card skeleton-wrapper\"},[_c('div',{staticClass:\"skeleton-content-wrapper\"},[_c('b-skeleton',{attrs:{\"width\":\"100%\",\"height\":\"20rem\",\"animated\":true}}),_c('b-skeleton',{attrs:{\"width\":\"100%\",\"height\":\"20rem\",\"animated\":true}})],1)]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.registration)?_c('div',{staticClass:\"registration\"},[_c('RegistrationCard',{attrs:{\"registration\":_vm.registration,\"showRegistrationPrices\":_vm.showRegistrationPrices,\"titleTranslationKey\":_vm.titleTranslationKey,\"showInvoices\":_vm.showInvoices}}),(_vm.showInvoices && _vm.openInvoices.length > 0)?_c('InvoiceCard',{attrs:{\"invoices\":_vm.openInvoices,\"titleTranslationKey\":'registration.openInvoices',\"showTotalAmountColumn\":true,\"showPaymentColumn\":_vm.showPaymentColumn}}):_vm._e(),(_vm.showInvoices && _vm.closedInvoices.length > 0)?_c('InvoiceCard',{attrs:{\"invoices\":_vm.closedInvoices,\"titleTranslationKey\":'registration.closedInvoices',\"showTotalAmountColumn\":false,\"showPaymentColumn\":false}}):_vm._e(),(_vm.registration.methodsofpayment && _vm.registration.methodsofpayment.length > 0)?_c('PaymentCard',{attrs:{\"methodsOfPayment\":_vm.registration.methodsofpayment,\"paymentText\":_vm.registration.paymenttext}}):_vm._e()],1):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.invoices.length > 0)?_c('div',{staticClass:\"card\",attrs:{\"id\":\"invoices\"}},[_c('div',{staticClass:\"card-content\"},[_c('h1',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t(_vm.titleTranslationKey)))]),_c('b-table',{staticClass:\"invoices\",attrs:{\"data\":_vm.invoices,\"detailed\":\"\",\"detail-key\":\"referencenumber\",\"show-detail-icon\":false},scopedSlots:_vm._u([{key:\"detail\",fn:function(props){return [_c('Invoice',{attrs:{\"invoice\":props.row}})]}}],null,false,1014786141)},[_c('b-table-column',{attrs:{\"label\":_vm.$t('registration.date'),\"width\":\"25%\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm._f(\"date\")(props.row.timestamp))+\" \")]}}],null,false,218435006)}),_c('b-table-column',{attrs:{\"label\":_vm.$t('registration.referencenumber')},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(props.row.referencenumber)+\" \")]}}],null,false,490263701)}),(_vm.showTotalAmountColumn)?_c('b-table-column',{attrs:{\"label\":_vm.$t('registration.total'),\"numeric\":\"\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm.$n(props.row.total.amount / 100, 'currency'))+\" \")]}}],null,false,1934853891)}):_vm._e(),(_vm.showPaymentColumn)?_c('b-table-column',{attrs:{\"numeric\":\"\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [(props.row.paymenturl)?_c('a',{attrs:{\"href\":props.row.paymenturl}},[_c('b-button',{staticClass:\"addToCartButton\",attrs:{\"type\":\"is-primary\",\"icon-left\":\"calendar\"}},[_vm._v(\" \"+_vm._s(_vm.$t('registration.paymentButton'))+\" \")])],1):_vm._e()]}}],null,false,1546946730)}):_vm._e(),_c('b-table-column',{attrs:{\"label\":_vm.$t('registration.details'),\"numeric\":\"\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_c('a',{on:{\"click\":function($event){return props.toggleDetails(props.row)}}},[_vm._v(\" \"+_vm._s(_vm.$t('registration.show'))+\" \"),_c('b-icon',{attrs:{\"icon\":_vm.includes(props.row.referencenumber, props.column.$table.openedDetailed)\n ? 'chevron-up'\n : 'chevron-down'}})],1)]}}],null,false,1050343303)})],1)],1)]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('div',{staticClass:\"block\"},[_c('h2',{staticClass:\"title is-5\"},[_vm._v(_vm._s(_vm.$t('registration.invoice'))+\" \"+_vm._s(_vm.invoice.referencenumber))])]),_c('div',{staticClass:\"block\"},[_c('b-table',{attrs:{\"data\":_vm.invoice.items},scopedSlots:_vm._u([(_vm.invoice.items.length > 1)?{key:\"footer\",fn:function(){return [_c('td',{attrs:{\"colspan\":\"3\"}},[_c('strong',[_vm._v(\" \"+_vm._s(_vm.$t('registration.total'))+\" \")])]),_c('td',{staticClass:\"has-text-right\"},[_c('strong',[_vm._v(\" \"+_vm._s(_vm.$n(_vm.invoice.total.amount / 100, 'currency'))+\" \")])])]},proxy:true}:null],null,true)},[_c('b-table-column',{attrs:{\"label\":_vm.$t('registration.date')},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm._f(\"date\")(props.row.timestamp))+\" \")]}}])}),_c('b-table-column',{attrs:{\"label\":_vm.$t('registration.invoiceDetails.typeLabel')},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm.$t((\"registration.invoiceDetails.type.\" + (props.row.type))))+\" \")]}}])}),_c('b-table-column',{attrs:{\"label\":_vm.$t('registration.invoiceDetails.name')},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [(props.row.client && props.row.course && props.row.name)?_c('span',[_vm._v(\" \"+_vm._s(props.row.course.code)+\" \"+_vm._s(props.row.course.name)+\", \"+_vm._s(props.row.name)+\", \"+_vm._s(_vm.$t('registration.client'))+\": \"+_vm._s(props.row.client.firstname)+\" \"+_vm._s(props.row.client.lastname)+\" \")]):(props.row.client && props.row.course)?_c('span',[_vm._v(\" \"+_vm._s(props.row.course.code)+\" \"+_vm._s(props.row.course.name)+\", \"+_vm._s(_vm.$t('registration.client').toLowerCase())+\": \"+_vm._s(props.row.client.firstname)+\" \"+_vm._s(props.row.client.lastname)+\" \")]):_c('span',[_vm._v(\" \"+_vm._s(props.row.name)+\" \")])]}}])}),_c('b-table-column',{attrs:{\"label\":_vm.$t('registration.invoiceDetails.amount'),\"numeric\":\"\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm.$n(props.row.amount / 100, 'currency'))+\" \")]}}])})],1)],1),(_vm.invoice.senttoaccounting)?_c('div',{staticClass:\"block sent-to-accounting\"},[_vm._v(\" \"+_vm._s(_vm.$t('registration.sentToAccounting'))+\" \")]):_vm._e()])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Invoice.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Invoice.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Invoice.vue?vue&type=template&id=17254602\"\nimport script from \"./Invoice.vue?vue&type=script&lang=ts\"\nexport * from \"./Invoice.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./InvoiceCard.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./InvoiceCard.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./InvoiceCard.vue?vue&type=template&id=443be9d9\"\nimport script from \"./InvoiceCard.vue?vue&type=script&lang=ts\"\nexport * from \"./InvoiceCard.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h1',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('registration.paymentTitle')))]),_c('div',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.paymentText),expression:\"paymentText\"}],staticClass:\"paymenttext\"}),_c('section',{staticClass:\"paymentmethods\"},_vm._l((_vm.methodsOfPayment),function(methodOfPayment,i){return _c('div',{key:i},[(methodOfPayment.fields)?_c('form',{attrs:{\"action\":methodOfPayment.action,\"id\":\"paymentform\",\"method\":\"POST\"}},[_vm._l((methodOfPayment.fields),function(field,idx){return _c('input',{key:idx,attrs:{\"name\":field.name,\"type\":\"hidden\"},domProps:{\"value\":field.value}})}),_c('button',{staticClass:\"button\",attrs:{\"type\":\"submit\"}},[_c('span',{staticClass:\"logo\",style:({ backgroundImage: (\"url('\" + (_vm.settings[methodOfPayment.name].logo) + \"')\") })}),_vm._v(\" \"+_vm._s(_vm.$t('payment.methodOfPayment.' + methodOfPayment.infotext))+\" \")])],2):_vm._e(),(methodOfPayment.url && !methodOfPayment.requestdata)?_c('a',{staticClass:\"button\",attrs:{\"href\":methodOfPayment.url}},[_c('span',{staticClass:\"logo\",style:({ backgroundImage: (\"url('\" + (_vm.settings[methodOfPayment.name].logo) + \"')\") })}),_vm._v(\" \"+_vm._s(_vm.$t('payment.methodOfPayment.' + methodOfPayment.infotext))+\" \")]):_vm._e(),(methodOfPayment.requestdata)?_c('div',{staticClass:\"button\",on:{\"click\":function($event){return _vm.handlePaymentRequest(methodOfPayment)}}},[(_vm.getPaymentTokenState === 'LOADING')?_c('div',{staticClass:\"payment-loading\"},[_c('div',{staticClass:\"loader is-loading\"})]):_vm._e(),_c('span',{staticClass:\"logo\",style:({ backgroundImage: (\"url('\" + (_vm.settings[methodOfPayment.name].logo) + \"')\") })}),_vm._v(\" \"+_vm._s(_vm.$t('payment.methodOfPayment.' + methodOfPayment.infotext))+\" \")]):_vm._e()])}),0)])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./PaymentCard.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./PaymentCard.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./PaymentCard.vue?vue&type=template&id=3f805f9e&scoped=true\"\nimport script from \"./PaymentCard.vue?vue&type=script&lang=ts\"\nexport * from \"./PaymentCard.vue?vue&type=script&lang=ts\"\nimport style0 from \"./PaymentCard.vue?vue&type=style&index=0&id=3f805f9e&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"3f805f9e\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\",attrs:{\"id\":\"registration-content\"}},[_c('h1',{staticClass:\"title\"},[_vm._v(\" \"+_vm._s(_vm.$t(_vm.titleTranslationKey))+\" \")]),(_vm.hasInvoices && _vm.showInvoices)?_c('a',{attrs:{\"href\":\"#invoices\"}},[_c('b-button',{staticClass:\"showInvoicesButton\",attrs:{\"type\":\"is-primary\"}},[_vm._v(\" \"+_vm._s(_vm.$t('registration.showInvoices'))+\" \")])],1):_vm._e(),(_vm.registration.confirmationtext)?_c('div',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.registration.confirmationtext),expression:\"registration.confirmationtext\"}],staticClass:\"description\"}):_vm._e(),(_vm.registration.cancellationtext)?_c('div',{directives:[{name:\"dompurify-html\",rawName:\"v-dompurify-html\",value:(_vm.registration.cancellationtext),expression:\"registration.cancellationtext\"}],staticClass:\"description\"}):_vm._e(),(_vm.registration.queuetext)?_c('p',{staticClass:\"queue\",domProps:{\"innerHTML\":_vm._s(_vm.registration.queuetext)}}):_vm._e(),_c('div',{class:{\n twoColumn: _vm.registration.productstotal && _vm.registration.productstotal.amount === 0\n }},[_c('div',{staticClass:\"block\"},[_c('b-table',{staticClass:\"products\",attrs:{\"data\":_vm.registration.products}},[_c('b-table-column',{attrs:{\"field\":\"registration\",\"label\":_vm.$t('registration.product'),\"cell-class\":'product-cell'},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_c('div',[_c('h2',[(props.row.course)?_c('router-link',{attrs:{\"to\":{ name: 'course', params: { id: props.row.course.id } }}},[_vm._v(\" \"+_vm._s(props.row.course.name)+\" \")]):_c('span',[_vm._v(_vm._s(props.row.name))])],1),(props.row.course)?_c('div',{staticClass:\"course-code\"},[_vm._v(_vm._s(props.row.course.code))]):_vm._e(),(props.row.type !== 'discount')?_c('RegistrationStatus',{attrs:{\"registration\":props.row,\"fromCart\":false}}):_vm._e(),(props.row.cancellationurl)?_c('a',{staticClass:\"is-danger\",on:{\"click\":function($event){return _vm.cancelRegistration(props.row.cancellationurl)}}},[_vm._v(_vm._s(_vm.$t('registration.cancel'))+\" » \")]):_vm._e(),(props.row.cancellationurl)?_c('p',[_vm._v(\" \"+_vm._s(_vm.$t('registration.lastcancellationdate'))+\" \"+_vm._s(_vm._f(\"date\")(props.row.lastcancellationdate))+\" \")]):_vm._e()],1)]}}])}),_c('b-table-column',{attrs:{\"field\":\"client\",\"label\":_vm.$t('registration.client'),\"cell-class\":'product-cell'},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [(props.row.client)?_c('div',[_c('h4',[_vm._v(_vm._s(props.row.client.firstname)+\" \"+_vm._s(props.row.client.lastname))]),_c('p',[_vm._v(_vm._s(props.row.client.email))]),_c('p',[_vm._v(_vm._s(props.row.client.phone))])]):_vm._e()]}}])}),_c('b-table-column',{attrs:{\"field\":\"priceclass\",\"visible\":_vm.showPrices,\"label\":_vm.$t('registration.priceclass'),\"cell-class\":'product-cell'},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [(props.row.items)?_c('div',[_vm._l((props.row.items),function(item){return _c('p',{key:item.id},[_vm._v(\" \"+_vm._s(item.name)+\" \"+_vm._s(_vm.$n(item.unitprice / 100, 'currency'))+\" \")])}),(props.row.total.amount)?_c('p',[_vm._v(\" \"+_vm._s(_vm.$t('registration.incvat'))+\" \"+_vm._s(_vm.$n(props.row.total.vatpercentage / 100, 'percent'))+\" \"+_vm._s(_vm.$n(props.row.total.vat / 100, 'currency'))+\" \")]):_vm._e()],2):_vm._e()]}}])}),_c('b-table-column',{attrs:{\"field\":\"price\",\"visible\":_vm.showPrices,\"label\":_vm.$t('registration.price'),\"cell-class\":'product-cell price-cell',\"numeric\":\"\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [(props.row.total)?_c('div',[(props.row.total.amount)?_c('span',{staticClass:\"product-price\"},[_vm._v(\" \"+_vm._s(_vm.$n(props.row.total.amount / 100, 'currency'))+\" \")]):_vm._e(),(props.row.total.spareamount)?_c('span',{staticClass:\"product-price-spare\"},[_vm._v(\" \"+_vm._s(_vm.$n(props.row.total.spareamount / 100, 'currency'))+\" * \")]):_vm._e()]):_vm._e()]}}])}),_c('b-table-column',{attrs:{\"field\":\"course\",\"visible\":!_vm.showPrices,\"label\":_vm.$t('registration.details'),\"cell-class\":'product-cell'},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [(props.row.course && _vm.isEmpty(props.row.lessons))?_c('CourseInfoDl',{attrs:{\"course\":props.row.course,\"fields\":_vm.courseInfoDlFields,\"spacing\":false,\"locationSingleLine\":false,\"locationMapLink\":false}}):(props.row.course && !_vm.isEmpty(props.row.lessons))?_c('div',_vm._l((props.row.lessons),function(lesson){return _c('CourseInfoDl',{key:lesson.id,staticClass:\"block\",attrs:{\"course\":props.row.course,\"lesson\":lesson,\"fields\":_vm.courseInfoDlFields,\"spacing\":false,\"locationSingleLine\":false,\"locationMapLink\":false}})}),1):_vm._e()]}}])})],1)],1),(_vm.showPrices)?_c('div',{staticClass:\"columns block\"},[_c('div',{staticClass:\"column is-half-desktop\"},[_c('b-table',{staticClass:\"groupedvatamounts\",attrs:{\"data\":_vm.groupedvatamounts,\"narrowed\":true,\"mobile-cards\":false}},[_c('b-table-column',{attrs:{\"label\":_vm.$t('registration.vatpercentage'),\"header-class\":'has-text-weight-normal',\"width\":\"25%\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(props.row.label)+\" \")]}}],null,false,2354628123)}),_c('b-table-column',{attrs:{\"label\":((_vm.$t('registration.net')) + \" (€)\"),\"numeric\":\"\",\"header-class\":'has-text-weight-normal',\"width\":\"25%\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm.$n(props.row.net / 100, 'twodecimal'))+\" \")]}}],null,false,925332476)}),_c('b-table-column',{attrs:{\"label\":((_vm.$t('registration.vat')) + \" (€)\"),\"numeric\":\"\",\"header-class\":'has-text-weight-normal',\"width\":\"25%\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm.$n(props.row.vat / 100, 'twodecimal'))+\" \")]}}],null,false,731566304)}),_c('b-table-column',{attrs:{\"label\":((_vm.$t('registration.gross')) + \" (€)\"),\"numeric\":\"\",\"header-class\":'has-text-weight-normal',\"width\":\"25%\"},scopedSlots:_vm._u([{key:\"default\",fn:function(props){return [_vm._v(\" \"+_vm._s(_vm.$n(props.row.amount / 100, 'twodecimal'))+\" \")]}}],null,false,2853534191)})],1)],1),_c('div',{staticClass:\"column is-half-desktop\"},[_c('div',{staticClass:\"block\"},[(_vm.registration.productstotal.amount)?_c('div',{staticClass:\"row\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.paymenttotal')))]),_c('div',{staticClass:\"bold-price\"},[_vm._v(\" \"+_vm._s(_vm.$n(_vm.registration.productstotal.amount / 100, 'currency'))+\" \")])]):_vm._e(),(_vm.registration.productstotal.culturevoucher)?_c('div',{staticClass:\"row\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.summary.culturevouchertotal')))]),_c('div',{staticClass:\"price\"},[_vm._v(\" \"+_vm._s(_vm.$n(_vm.registration.productstotal.culturevoucher / 100, 'currency'))+\" \")])]):_vm._e(),(_vm.registration.productstotal.sportsvoucher)?_c('div',{staticClass:\"row\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.summary.sportsvouchertotal')))]),_c('div',{staticClass:\"price\"},[_vm._v(\" \"+_vm._s(_vm.$n(_vm.registration.productstotal.sportsvoucher / 100, 'currency'))+\" \")])]):_vm._e()]),(_vm.registration.productstotal.canpaynow)?_c('div',{staticClass:\"block row\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.canpaynow')))]),_c('div',{staticClass:\"price-big\"},[_vm._v(\" \"+_vm._s(_vm.$n(_vm.registration.productstotal.canpaynow / 100, 'currency'))+\" \")])]):_vm._e(),(_vm.registration.productstotal.mustpaynow)?_c('div',{staticClass:\"block row\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.mustpaynow')))]),_c('div',{staticClass:\"price-big\"},[_vm._v(\" \"+_vm._s(_vm.$n(_vm.registration.productstotal.mustpaynow / 100, 'currency'))+\" \")])]):_vm._e(),_c('div',{staticClass:\"block\"},[(_vm.someRegistrationsAreSpare)?_c('div',{staticClass:\"row\"},[_c('div',[_vm._v(_vm._s(_vm.$t('cart.sparetotal'))+\" *\")]),_c('div',{staticClass:\"price\"},[_vm._v(\" \"+_vm._s(_vm.$n(_vm.registration.productstotal.spareamount / 100, 'currency'))+\" \")])]):_vm._e(),(_vm.someRegistrationsAreSpare)?_c('div',{staticClass:\"row spare-explanation\"},[_vm._v(\" \"+_vm._s(_vm.$t('cart.spareExplanation'))+\" \")]):_vm._e()])])]):_vm._e()])])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n","import mod from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./RegistrationCard.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./RegistrationCard.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./RegistrationCard.vue?vue&type=template&id=75ee0006&scoped=true\"\nimport script from \"./RegistrationCard.vue?vue&type=script&lang=ts\"\nexport * from \"./RegistrationCard.vue?vue&type=script&lang=ts\"\nimport style0 from \"./RegistrationCard.vue?vue&type=style&index=0&id=75ee0006&prod&lang=scss&scoped=true\"\nimport style1 from \"./RegistrationCard.vue?vue&type=style&index=1&id=75ee0006&prod&lang=css\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"75ee0006\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Registration.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Registration.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Registration.vue?vue&type=template&id=28546162\"\nimport script from \"./Registration.vue?vue&type=script&lang=ts\"\nexport * from \"./Registration.vue?vue&type=script&lang=ts\"\nimport style0 from \"./Registration.vue?vue&type=style&index=0&id=28546162&prod&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./MyRegistrations.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./MyRegistrations.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./MyRegistrations.vue?vue&type=template&id=cc9ad916\"\nimport script from \"./MyRegistrations.vue?vue&type=script&lang=ts\"\nexport * from \"./MyRegistrations.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"card\"},[_c('ValidationObserver',{ref:\"observer\"},[_c('ValidationProvider',{attrs:{\"vid\":_vm.email,\"rules\":{\n required: true,\n email: true\n }},scopedSlots:_vm._u([{key:\"default\",fn:function(ref){\n var errors = ref.errors;\nreturn _c('div',{staticClass:\"card-content\"},[_c('h2',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('nav.myregistrations')))]),_c('p',[_vm._v(_vm._s(_vm.$t('myregistrations.email.help')))]),_c('form',{on:{\"submit\":function($event){$event.preventDefault();return _vm.submit($event)}}},[_c('b-field',{staticClass:\"email-input\",attrs:{\"type\":{ 'is-danger': errors[0] },\"message\":_vm.$t(errors[0])}},[_c('b-input',{attrs:{\"lazy\":\"\",\"placeholder\":_vm.$t('fieldlabel.email'),\"type\":\"email\"},model:{value:(_vm.email),callback:function ($$v) {_vm.email=$$v},expression:\"email\"}})],1),_c('b-button',{staticClass:\"is-primary\",attrs:{\"type\":\"submit\",\"loading\":_vm.isLoading},on:{\"click\":_vm.submit}},[_vm._v(\" \"+_vm._s(_vm.$t('myregistrations.email.send'))+\" \")])],1)])}}])})],1)],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./MyRegistrationsLoginLink.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./MyRegistrationsLoginLink.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./MyRegistrationsLoginLink.vue?vue&type=template&id=7d2cf182&scoped=true\"\nimport script from \"./MyRegistrationsLoginLink.vue?vue&type=script&lang=ts\"\nexport * from \"./MyRegistrationsLoginLink.vue?vue&type=script&lang=ts\"\nimport style0 from \"./MyRegistrationsLoginLink.vue?vue&type=style&index=0&id=7d2cf182&prod&scoped=true&lang=scss\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"7d2cf182\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.registration)?_c('div',{staticClass:\"registration\"},[_c('Registration',{attrs:{\"registration\":_vm.registration,\"titleTranslationKey\":_vm.titleTranslationKey,\"showRegistrationPrices\":true,\"showInvoices\":false,\"showPaymentColumn\":false}})],1):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./NewRegistration.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./NewRegistration.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./NewRegistration.vue?vue&type=template&id=1918460c\"\nimport script from \"./NewRegistration.vue?vue&type=script&lang=ts\"\nexport * from \"./NewRegistration.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.brandIsLoaded)?_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h2',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('404.title')))]),_c('p',[_vm._v(_vm._s(_vm.$t('404.content')))])])]):_c('section',{staticClass:\"noBrand\"},[_vm._m(0)])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h2',[_vm._v(\"404\")]),_c('p',[_vm._v(\"Sivua ei löytynyt / Page not found\")])])])}]\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./NotFound.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./NotFound.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./NotFound.vue?vue&type=template&id=bb2be3ee&scoped=true\"\nimport script from \"./NotFound.vue?vue&type=script&lang=ts\"\nexport * from \"./NotFound.vue?vue&type=script&lang=ts\"\nimport style0 from \"./NotFound.vue?vue&type=style&index=0&id=bb2be3ee&prod&lang=scss&scoped=true\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"bb2be3ee\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[(_vm.isSuccess)?_c('div',[_c('div',{staticClass:\"notification is-success is-light callout\"},[_vm._v(\" \"+_vm._s(_vm.$t('payment.title.success'))+\" \")]),(_vm.registration)?_c('div',{staticClass:\"registration\"},[_c('Registration',{attrs:{\"registration\":_vm.registration,\"titleTranslationKey\":_vm.titleTranslationKey,\"showRegistrationPrices\":false,\"showInvoices\":true,\"showPaymentColumn\":false}})],1):_vm._e()]):(_vm.isOkError)?_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h1',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('payment.title.error')))]),_c('p',{staticClass:\"mt-4\"},[_vm._v(_vm._s(_vm.$t('payment.content.error')))])])]):(_vm.isCancel)?_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h1',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('payment.title.cancel')))]),_c('p',{staticClass:\"mt-4\"},[_vm._v(_vm._s(_vm.$t('payment.content.cancel')))])])]):_vm._e()])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Payment.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Payment.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./Payment.vue?vue&type=template&id=2388722e\"\nimport script from \"./Payment.vue?vue&type=script&lang=ts\"\nexport * from \"./Payment.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.registration)?_c('div',{staticClass:\"registration\"},[_c('Registration',{attrs:{\"registration\":_vm.registration,\"titleTranslationKey\":_vm.titleTranslationKey,\"showRegistrationPrices\":false,\"showInvoices\":true,\"showPaymentColumn\":false}})],1):(_vm.hasError || _vm.invalidParameters)?_c('div',{staticClass:\"card\"},[_c('div',{staticClass:\"card-content\"},[_c('h1',{staticClass:\"title\"},[_vm._v(_vm._s(_vm.$t('payment.title.error')))]),_c('p',{staticClass:\"mt-4\"},[_vm._v(_vm._s(_vm.$t('registration.getRegistrationError')))])])]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./SingleRegistration.vue?vue&type=script&lang=ts\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[2]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./SingleRegistration.vue?vue&type=script&lang=ts\"","import { render, staticRenderFns } from \"./SingleRegistration.vue?vue&type=template&id=44a22818\"\nimport script from \"./SingleRegistration.vue?vue&type=script&lang=ts\"\nexport * from \"./SingleRegistration.vue?vue&type=script&lang=ts\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import Vue from 'vue';\nimport VueRouter, { RouteConfig } from 'vue-router';\nimport TenantLanguage from './components/TenantLanguage.vue';\nimport Cart from './views/Cart.vue';\nimport Course from './views/Course.vue';\nimport Help from './views/Help.vue';\nimport Home from './views/Home.vue';\nimport MyRegistrations from './views/MyRegistrations.vue';\nimport MyRegistrationsLoginLink from './views/MyRegistrationsLoginLink.vue';\nimport NewRegistration from './views/NewRegistration.vue';\nimport NotFound from './views/NotFound.vue';\nimport Payment from './views/Payment.vue';\nimport SingleRegistration from './views/SingleRegistration.vue';\n\nVue.use(VueRouter);\n\nconst routes: RouteConfig[] = [\n {\n path: '/:tenant',\n component: TenantLanguage,\n props: true\n },\n {\n path: '/:tenant/:language',\n component: TenantLanguage,\n props: true,\n children: [\n {\n path: '/',\n name: 'home',\n component: Home\n },\n {\n path: 'cart',\n name: 'cart',\n component: Cart\n },\n {\n path: 'registration',\n name: 'single-registration',\n component: SingleRegistration\n },\n {\n path: 'payment/:paymentmethod/:status',\n name: 'payment',\n props: true,\n component: Payment\n },\n {\n path: 'help',\n name: 'help',\n component: Help\n },\n {\n path: 'course/:id',\n name: 'course',\n component: Course\n },\n {\n path: 'my-registrations/login-link',\n name: 'my-registrations-login-link',\n component: MyRegistrationsLoginLink\n },\n {\n path: 'new-registration',\n name: 'new-registration',\n component: NewRegistration\n },\n {\n path: 'my-registrations',\n name: 'my-registrations',\n component: MyRegistrations\n },\n {\n // https://github.com/vuejs/vue-router/issues/724#issuecomment-301260298\n path: 'not-found',\n alias: '*',\n name: 'NotFound',\n component: NotFound\n }\n ]\n },\n {\n path: '/not-found',\n alias: '*',\n name: 'NotFoundBrandless',\n component: NotFound\n }\n];\n\nconst router = new VueRouter({\n mode: 'history',\n scrollBehavior(to, from) {\n // Don't scroll to top if pagination or search params change in the current path\n if (\n to.path === from.path &&\n (to.query.q !== from.query.q || to.query.page !== from.query.page)\n ) {\n return;\n }\n if (to.hash) {\n return { selector: to.hash };\n }\n\n return { x: 0, y: 0 };\n },\n routes\n});\n\nexport default router;\n","/* eslint-disable */\n\nimport { extend } from 'vee-validate';\nimport { email, regex, required } from 'vee-validate/dist/rules';\nimport {\n adjustBirthDateForLastHundredYears,\n isParticipantTooOld,\n isParticipantTooYoung,\n parseBirthDateFromPin\n} from './utils/agelimit';\n\nexport const initVeeValidate = () => {\n extend('required', {\n ...required,\n message: 'validation.required'\n });\n\n extend('email', {\n ...email,\n message: 'validation.email'\n });\n\n extend('pin', {\n ...regex,\n message: 'validation.patternPin'\n });\n\n extend('birthday', {\n ...regex,\n message: 'validation.patternBirthDay'\n });\n\n extend('participantTooYoung', {\n validate: (value: string, args) => {\n const stringArray = args as string[];\n if (!stringArray[0]) {\n return true;\n }\n const minDate = new Date(stringArray[0]);\n const birthDate = parseBirthDateFromPin(value);\n const parsedBirthDate = adjustBirthDateForLastHundredYears(birthDate);\n return !isParticipantTooYoung(minDate, parsedBirthDate);\n },\n message: 'validation.participantTooYoung'\n });\n extend('participantTooOld', {\n validate: (value: string, args) => {\n const stringArray = args as string[];\n if (!stringArray[0]) {\n return true;\n }\n const maxDate = new Date(stringArray[0]);\n const birthDate = parseBirthDateFromPin(value);\n const parsedBirthDate = adjustBirthDateForLastHundredYears(birthDate);\n return !isParticipantTooOld(maxDate, parsedBirthDate);\n },\n message: 'validation.participantTooOld'\n });\n};\n","import './installCompositionApi.ts';\nimport '../frontend-assets/buefy.scss';\nimport '@mdi/font/css/materialdesignicons.css';\nimport 'leaflet/dist/leaflet.css';\n\nimport Buefy from 'buefy';\nimport Vue from 'vue';\nimport VueI18n from 'vue-i18n';\nimport VueDOMPurifyHTML from 'vue-dompurify-html';\nimport App from './App.vue';\nimport {\n formatDate,\n formatDateRange,\n formatDateTime,\n formatDateTimeRange,\n formatMidnight,\n formatTime,\n formatTimeRange\n} from './filters';\nimport { initializeI18n } from './i18n';\nimport router from './router';\nimport { initVeeValidate } from './vee-validate';\n\n// fix missing GlobalFetch in openapi-generator generated code\ndeclare module './api/runtime' {\n type GlobalFetch = WindowOrWorkerGlobalScope;\n}\n\ninitVeeValidate();\n\nVue.use(Buefy);\nVue.use(VueI18n);\nVue.use(VueDOMPurifyHTML, {\n default: {\n ADD_ATTR: ['target']\n }\n});\n\nVue.config.productionTip = false;\n\nVue.filter('date', formatDate);\nVue.filter('dateRange', formatDateRange);\nVue.filter('dateTime', formatDateTime);\nVue.filter('dateTimeRange', formatDateTimeRange);\nVue.filter('midnight', formatMidnight);\nVue.filter('time', formatTime);\nVue.filter('timeRange', formatTimeRange);\n\nnew Vue({\n i18n: initializeI18n(),\n router,\n render: (h) => h(App)\n}).$mount('#app');\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\tid: moduleId,\n\t\tloaded: false,\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Flag the module as loaded\n\tmodule.loaded = true;\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","var deferred = [];\n__webpack_require__.O = (result, chunkIds, fn, priority) => {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar [chunkIds, fn, priority] = deferred[i];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.hmd = (module) => {\n\tmodule = Object.create(module);\n\tif (!module.children) module.children = [];\n\tObject.defineProperty(module, 'exports', {\n\t\tenumerable: true,\n\t\tset: () => {\n\t\t\tthrow new Error('ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id);\n\t\t}\n\t});\n\treturn module;\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","__webpack_require__.nmd = (module) => {\n\tmodule.paths = [];\n\tif (!module.children) module.children = [];\n\treturn module;\n};","__webpack_require__.p = \"/static/\";","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t143: 0\n};\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n__webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = (parentChunkLoadingFunction, data) => {\n\tvar [chunkIds, moreModules, runtime] = data;\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some((id) => (installedChunks[id] !== 0))) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\treturn __webpack_require__.O(result);\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunkhellewi_ilmoittautuminen\"] = self[\"webpackChunkhellewi_ilmoittautuminen\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [998], () => (__webpack_require__(670)))\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n"],"names":["Vue","use","VueCompositionApi","render","_vm","this","_h","$createElement","_c","_self","attrs","staticRenderFns","defineComponent","setup","onBeforeMount","link","document","querySelector","createElement","type","rel","href","favicon","getElementsByTagName","appendChild","component","DATE_FORMAT_FI","DATETIME_FORMAT_FI","TIME_FORMAT_FI","formatTime","date","isValid","format","formatDate","formatDateTime","formatTimeRange","begins","ends","concat","formatDateRange","isSameDay","beginsFormat","isSameMonth","isSameYear","formatDateTimeRange","formatMidnight","getTime","startOfDay","subMinutes","numberFormats","fi","currency","style","twodecimal","minimumFractionDigits","maximumFractionDigits","percent","sv","en","dateTimeFormats","weekdayShort","weekday","weekdayLong","initializeI18n","VueI18n","locale","fallbackLocale","silentFallbackWarn","brand","color","complementarycolor","font","headerfont","i18nIsLoaded","errorMsg","staticClass","_e","_v","_s","$t","BASE_PATH","replace","isBlob","value","Blob","BaseAPI","_this","configuration","arguments","length","undefined","Configuration","_classCallCheck","_defineProperty","_ref","_asyncToGenerator","_regeneratorRuntime","mark","_callee","url","init","fetchParams","_iterator","_step","middleware","response","_iterator2","_step2","_middleware","wrap","_context","prev","next","_createForOfIteratorHelper","s","n","done","pre","_objectSpread","fetch","fetchApi","t0","sent","t1","e","f","finish","post","clone","t2","t3","abrupt","stop","_x","_x2","apply","_createClass","key","_next$middleware","_len","preMiddlewares","Array","_key","middlewares","map","withMiddleware","_toConsumableArray","_len2","postMiddlewares","_key2","_request","_callee2","context","_this$createFetchPara","_context2","createFetchParams","status","request","_x3","basePath","path","query","Object","keys","queryParamsStringify","body","FormData","URLSearchParams","JSON","stringify","headers","assign","method","credentials","constructor","slice","RequiredError","_Error","_inherits","_super","_createSuper","field","msg","_this2","call","_assertThisInitialized","_wrapNativeSuper","Error","get","window","bind","querystring","username","password","apiKey","accessToken","exists","json","params","prefix","fullKey","multiValue","singleValue","encodeURIComponent","String","join","Date","toISOString","filter","part","BenefitTypeCulture","BenefitTypeExcercise","JSONApiResponse","raw","transformer","jsonValue","_value","_callee3","_context3","VoidApiResponse","_value2","_callee4","_context4","TextApiResponse","_value4","_callee6","_context6","text","ClientLessonAction","CourseSortOrder","CourseSortfield","ContactFromJSON","ContactFromJSONTyped","ignoreDiscriminator","CourseSortOrderFromJSON","CourseSortOrderFromJSONTyped","CpuPaymentApiVersionEnum","CpuPaymentModeEnum","CpuPaymentActionEnum","CpuResponseStatus","ErrorItemType","HellewiCartItemType","CpuPaymentFromJSON","CpuPaymentFromJSONTyped","CpuProductFromJSON","CpuProductFromJSONTyped","GeopointFromJSON","GeopointFromJSONTyped","HellewiAgeLimitsFromJSON","HellewiAgeLimitsFromJSONTyped","HellewiBrandFromJSON","HellewiBrandFromJSONTyped","HellewiLocationFromJSON","HellewiTenantTypeFromJSON","HellewiCalloutFromJSON","HellewiCalloutFromJSONTyped","HellewiCartItemFromJSON","HellewiCartItemFromJSONTyped","HellewiCartItemTypeFromJSON","HellewiCartItemIdFromJSON","HellewiCoursePartialFromJSON","HellewiCourseLessonFromJSON","HellewiCartItemIdFromJSONTyped","HellewiCartItemIdToJSON","id","lessonid","expiry","reqid","unlistedid","hmac","HellewiCartItemTypeFromJSONTyped","HellewiCartStatusFromJSON","HellewiCartStatusFromJSONTyped","HellewiCatalogFromJSON","HellewiCatalogFromJSONTyped","HellewiCatalogItemFromJSON","HellewiCatalogItemType","HellewiCatalogItemFromJSONTyped","HellewiCatalogItemTypeFromJSON","HellewiCatalogItemTextFromJSON","HellewiCatalogItemTextFromJSONTyped","HellewiCatalogItemTypeFromJSONTyped","HellewiCatalogSettingsFromJSON","HellewiCatalogSettingsFromJSONTyped","HellewiCatalogSettingsEnabledCatalogItemTypesFromJSON","HellewiCatalogSettingsEnabledCatalogItemTypesFromJSONTyped","HellewiCourseNotificationLabel","HellewiCourseStatus","HellewiLocationSortfields","HellewiReservationSortfields","HellewiCourseFromJSON","HellewiCourseFromJSONTyped","HellewiCourseStatusFromJSON","HellewiCourseDayFromJSON","HellewiCourseNotificationFromJSON","HellewiCoursePeriodFromJSON","HellewiLanguageFromJSON","HellewiTagFromJSON","HellewiCoursePriceFromJSON","HellewiCourseProductFromJSON","HellewiParticipantCountFromJSON","HellewiFileFromJSON","HellewiImageFromJSON","HellewiCourseCountFromJSON","HellewiCourseCountFromJSONTyped","HellewiCourseDayFromJSONTyped","WeekdayFromJSON","HellewiCourseLessonFromJSONTyped","HellewiLessonParticipantCountFromJSON","HellewiCourseMinimalFromJSON","HellewiCourseMinimalFromJSONTyped","HellewiCourseMinimalParentFromJSON","HellewiCourseMinimalParentFromJSONTyped","HellewiCourseNotificationFromJSONTyped","HellewiCourseNotificationLabelFromJSON","HellewiCourseNotificationLabelFromJSONTyped","HellewiCoursePartialFromJSONTyped","HellewiCoursePeriodFromJSONTyped","HellewiCoursePriceFromJSONTyped","HellewiCoursePriceInstallmentFromJSON","HellewiCoursePriceInstallmentFromJSONTyped","HellewiCoursePriceInstallmentInstallmentsFromJSON","HellewiCoursePriceInstallmentInstallmentsFromJSONTyped","HellewiCourseProductFromJSONTyped","HellewiCourseStatusFromJSONTyped","HellewiFileFromJSONTyped","HellewiGetRegistrationResponseFromJSON","HellewiGetRegistrationResponseFromJSONTyped","PurchaseProductNumberFromJSON","PurchaseAmountNumberFromJSON","PurchaseInvoiceNumberFromJSON","MethodOfPaymentFromJSON","HellewiImageFromJSONTyped","HellewiLanguageFromJSONTyped","HellewiLessonParticipantCountFromJSONTyped","HellewiLocationFromJSONTyped","HellewiMyRegistrationsResponseFromJSON","HellewiMyRegistrationsResponseFromJSONTyped","HellewiParticipantCountFromJSONTyped","HellewiPostRegistrationResponseFromJSON","HellewiPostRegistrationResponseFromJSONTyped","HellewiPromotionFromJSON","HellewiPromotionFromJSONTyped","HellewiTenantType","MethodOfPaymentMethodEnum","MethodOfPaymentInfotext","PaymentServiceName","PaytrailApiAlgorithm","PaytrailApiReceiptStatus","PurchaseProductItemType","PurchaseProductStatus","PurchaseProductType","Sortdir","UpdatableCourseProperties","Weekday","HellewiTagFromJSONTyped","HellewiTenantTypeFromJSONTyped","HellewiTextFromJSON","HellewiTextFromJSONTyped","MethodOfPaymentFromJSONTyped","PaymentServiceNameFromJSON","PaymentFormFieldFromJSON","MethodOfPaymentInfotextFromJSON","MethodOfPaymentInfotextFromJSONTyped","PaymentFormFieldFromJSONTyped","PaymentServiceNameFromJSONTyped","PurchaseAmountNumberFromJSONTyped","PurchaseInvoiceNumberFromJSONTyped","PurchaseProductItemNumberFromJSON","PurchaseProductItemNumberFromJSONTyped","PurchaseProductItemTypeFromJSON","PurchaseProductItemTypeFromJSONTyped","PurchaseProductNumberFromJSONTyped","PurchaseProductTypeFromJSON","PurchaseProductStatusFromJSON","PurchaseProductStatusFromJSONTyped","PurchaseProductTypeFromJSONTyped","RegistrationPriceNumberFromJSON","RegistrationPriceNumberFromJSONTyped","WeekdayFromJSONTyped","GetLocationReservationsSearchkeyEnum","BrandApi","_runtime$BaseAPI","_getBrandRaw","queryParameters","headerParameters","runtime","getBrandRaw","_getBrand","getBrand","_getCatalogItemDetailsRaw","requestParameters","keyword","getCatalogItemDetailsRaw","_getCatalogItemDetails","getCatalogItemDetails","_getHelpRaw","_callee5","_context5","getHelpRaw","_getHelp","getHelp","_listPromotionsRaw","_callee7","_context7","listPromotionsRaw","_listPromotions","_callee8","_context8","listPromotions","CalloutsApi","_getLocalCalloutsRaw","getLocalCalloutsRaw","_getLocalCallouts","getLocalCallouts","CartApi","_addCartItemRaw","hellewiCartItemId","addCartItemRaw","_addCartItem","addCartItem","_deleteCartItemRaw","deleteCartItemRaw","_deleteCartItem","deleteCartItem","_x4","_getCartItemRaw","getCartItemRaw","_x5","_getCartItem","getCartItem","_x6","_getCartStatusRaw","getCartStatusRaw","_getCartStatus","getCartStatus","_listCartItemsRaw","_callee9","_context9","listCartItemsRaw","_listCartItems","_callee10","_context10","listCartItems","CatalogApi","_getCatalogRaw","q","getCatalogRaw","_getCatalog","getCatalog","_getCatalogSettingsRaw","getCatalogSettingsRaw","_getCatalogSettings","getCatalogSettings","CourseApi","_getCourseRaw","preview","getCourseRaw","_getCourse","getCourse","_getCourseCountRaw","getCourseCountRaw","_getCourseCount","getCourseCount","_listCourseParticipantCountsRaw","ids","listCourseParticipantCountsRaw","_listCourseParticipantCounts","listCourseParticipantCounts","_listCoursesRaw","page","limit","sort","sortdir","listCoursesRaw","_x7","_listCourses","listCourses","_x8","RequestState","PaymentApi","_getCpuOkRaw","reference","hash","paymentSum","paymentMethod","timestamp","paymentDescription","getCpuOkRaw","_getCpuOk","getCpuOk","_getEpassiOkRaw","amount","products","sTAMP","pAID","mAC","getEpassiOkRaw","_getEpassiOk","getEpassiOk","_getPaymentTokenRaw","requestBody","getPaymentTokenRaw","_getPaymentToken","getPaymentToken","_getPaytrailApiNotifyRaw","checkoutAccount","checkoutAlgorithm","checkoutAmount","checkoutStamp","checkoutReference","checkoutTransactionId","checkoutStatus","checkoutProvider","signature","getPaytrailApiNotifyRaw","_getPaytrailApiNotify","getPaytrailApiNotify","_getPaytrailApiOkRaw","getPaytrailApiOkRaw","_x9","_getPaytrailApiOk","getPaytrailApiOk","_x10","_getSmartumOkRaw","_callee11","_context11","referenceNumber","jwt","getSmartumOkRaw","_x11","_getSmartumOk","_callee12","_context12","getSmartumOk","_x12","_getTurkuOkRaw","_callee13","_context13","authorization","xTURKUSP","xTURKUTS","getTurkuOkRaw","_x13","_getTurkuOk","_callee14","_context14","getTurkuOk","_x14","_getVismapayNotifyRaw","_callee15","_context15","rETURNCODE","oRDERNUMBER","sETTLED","aUTHCODE","getVismapayNotifyRaw","_x15","_getVismapayNotify","_callee16","_context16","getVismapayNotify","_x16","_getVismapayOkRaw","_callee17","_context17","getVismapayOkRaw","_x17","_getVismapayOk","_callee18","_context18","getVismapayOk","_x18","_postCpuNotifyRaw","_callee19","_context19","postCpuNotifyRaw","_x19","_postCpuNotify","_callee20","_context20","postCpuNotify","_x20","_postEpassiNotifyRaw","_callee21","_context21","postEpassiNotifyRaw","_x21","_postEpassiNotify","_callee22","_context22","postEpassiNotify","_x22","_postEpassiOkRaw","_callee23","_context23","postEpassiOkRaw","_x23","_postEpassiOk","_callee24","_context24","postEpassiOk","_x24","_postTurkuNotifyRaw","_callee25","_context25","postTurkuNotifyRaw","_x25","_postTurkuNotify","_callee26","_context26","postTurkuNotify","_x26","RegistrationApi","_getMyRegistrationsRaw","email","getMyRegistrationsRaw","_getMyRegistrations","getMyRegistrations","_getRegistrationRaw","referencenumber","getRegistrationRaw","_getRegistration","getRegistration","_getRegistrationCancelRaw","cancellationdate","getRegistrationCancelRaw","_getRegistrationCancel","getRegistrationCancel","_getRegistrationFormRaw","getRegistrationFormRaw","_getRegistrationForm","getRegistrationForm","_getRegistrationPriceSchemaRaw","getRegistrationPriceSchemaRaw","_getRegistrationPriceSchema","getRegistrationPriceSchema","_postMyRegistrationsLoginLinkRaw","postMyRegistrationsLoginLinkRaw","_postMyRegistrationsLoginLink","postMyRegistrationsLoginLink","_postRegistrationRaw","postRegistrationRaw","_postRegistration","postRegistration","_postRegistrationPriceRaw","postRegistrationPriceRaw","_postRegistrationPrice","postRegistrationPrice","filterUndefineds","xs","x","translate","i18nKey","parent","translation","toString","onHook","name","callback","vm","getCurrentInstance","merge","config","optionMergeStrategies","prototype","getPrototypeOf","proxy","$options","onBeforeRouteLeave","handleWindowUnload","preventDefault","returnValue","HttpStatusCode","stateHasError","state","computed","stateIsLoading","Loading","stateIsSuccess","Success","ApiEndpointInitialization","api","initial","initialize","Initialized","Uninitialized","watch","useToast","ctx","warnComponents","ref","warnToast","translateMessage","indefinite","push","Snackbar","open","message","duration","position","actionText","queue","clearErrorToasts","close","err","successToast","Toast","useBrandApi","memoize","changeConfiguration","useGetBrand","_useBrandApi","execute","OK","useGetPromotions","_useBrandApi2","_ref2","useGetHelp","_useBrandApi3","_ref3","useCalloutsApi","useGetLocalCallouts","_useCalloutsApi","useCartApi","useCartItems","_useCartApi","setResponse","cartItems","useCartStatus","_useCartApi2","res","manuallyCleared","useAddToCart","_useCartApi3","_useCartStatus","errorMessage","req","_typeof","useDeleteFromCart","_useCartApi4","_useCartStatus2","_ref4","useCatalogApi","useGetCatalogUnfiltered","_useCatalogApi","useGetCatalog","_useCatalogApi2","currentQ","ongoing","isEqual","cancel","PCancelable","resolve","reject","onCancel","catalog","useCatalogSettings","_useCatalogApi3","useCourseApi","useGetCourse","_useCourseApi","_useCourseApi2","requestParams","_response$value","_response$value2","includes","RegistrationToLessons","statuses","useListCourses","count","courses","_useCourseApi3","currentParams","responseRaw","partialCourses","participantCounts","course","isEmpty","Math","min","parseInt","participantcount","find","pc","HellewiPaymentApi","_PaymentApi","_getPaymentRequest","paymentmethod","getPaymentRequest","usePaymentApi","useGetPaymentRequest","_usePaymentApi","useGetPaymentToken","_usePaymentApi2","useRegistrationApi","useGetRegistrationForm","_useRegistrationApi","parse","useGetRegistration","_useRegistrationApi2","usePostRegistration","_useRegistrationApi3","usePostRegistrationPrice","_useRegistrationApi4","_ref5","paramsChanged","_ref6","usePostMyRegistrationsLoginLink","_useRegistrationApi5","_ref7","_ref8","useGetMyRegistrations","_useRegistrationApi6","_ref9","_ref10","slot","scopedSlots","_u","fn","staticStyle","on","$event","setLanguage","$tc","minutesLeft","cartStatus","isOpen","model","$$v","expression","nativeOn","props","CART_REFRESH_TIMER","_useGetBrand","timer","_cartStatus$value","timeleft","round","newLang","curLang","root","$i18n","localStorage","setItem","curLangRe","RegExp","router","currentRoute","newVal","oldVal","Dialog","alert","confirmText","onConfirm","clearInterval","setInterval","onBeforeUnmount","callouts","_l","callout","directives","rawName","_useGetLocalCallouts","location","address","postalcode","city","phoneLink","phone","emailLink","addHttpsToUrl","homepage","accessibilitystatementurl","privacystatementurl","facebook","instagram","linkedin","twitter","backgroundImage","_brand$value","_brand$value2","_brand$value3","_brand$value4","match","metaPixel","b","v","t","fbq","callMethod","_fbq","loaded","version","async","src","parentNode","insertBefore","setupMetaPixel","metaPixelId","components","Header","Callouts","Footer","tenant","required","language","i18n","messages","changeConfigurationBrandApi","changeConfigurationCalloutsApi","changeConfigurationCartApi","changeConfigurationCatalogApi","changeConfigurationCourseApi","changeConfigurationPaymentApi","changeConfigurationRegistrationApi","getBrandState","getBrandStatus","apiConfiguration","proto","protocol","host","hostname","port","setupCookieBot","_root$$i18n","cookiebot","VueCookieBot","cookieBotID","blockingMode","defaultLocale","setupGtm","gtag","VueGtm","defer","compatibility","nonce","enabled","debug","loadScript","vueRouter","trackOnNextTick","$gtm","enable","triggerCookieBot","$cookiebot","consentBanner","changeConfigurations","setLanguageAndScripts","_yield$Promise$all","_yield$Promise$all2","Promise","all","_slicedToArray","setLocaleMessage","updateUserConsent","_window$Cookiebot","Cookiebot","_window$Cookiebot$con","consent","marketing","statistics","metapixel","updateDocumentLanguage","documentElement","setAttribute","setTimeout","getItem","onMounted","addEventListener","removeEventListener","hasError","NOT_FOUND","reload","RegistrationStatus","schema","client","i","clients","availableCartItems","ajvErrors","payerFromClient","validationTrigger","updateClientPrices","updateClient","removeClient","checkValidation","removeCartItem","addClient","updatePayer","updatePayerFromClient","purchase","postRegIsLoading","checkDiscountCode","startValidation","isLoading","shownCartItems","clientCount","selectedItems","item","itemIndex","itemid","toggleCartItem","updateCourseDetails","updateCoursePrices","selectedCourseIds","isPayer","cartItemAgeLimits","updateClientFields","passValidationResults","class","selectedCourse","isSelected","target","currentTarget","toggleItem","getCourseSelectorLabel","ageLimitString","code","$d","lesson","_f","prices","price","selectedPrice","formatLabel","formatAmount","installmentGroups","installmentgroup","selectedInstallmentgroup","installment","courseProducts","product","selectedCourseProducts","questionFields","infoNoPrice","errors","title","questionFieldTypes","questionType","questionAnswers","$set","option","const","domProps","clientAjvErrors","getFieldError","instancePathId","getAgeLimitTranslation","ageLimits","_ageLimits$minAge","minAge","maxInt","maxAge","max","maxPlusOne","age","getRegistrationStatus","courseStatuses","productStatuses","spare","Spare","Cancelled","Interrupted","NotYetStarted","SpareNotStarted","ActualInProgress","InProgress","SpareInProgress","Ended","SpareEnded","ActualEnded","getRegistrationStatusFromCart","ActualNotStarted","registration","fromCart","Boolean","QuestionType","defaultQuestionAnswers","qfs","fromPairs","ValidationProvider","ValidationObserver","Number","selected","coursedetails","showCheckbox","_props$coursedetails","_props$coursedetails2","observer","courseInSchema","_props$item$course","$defs","_getAgeLimitTranslati","_getAgeLimitTranslati2","translationKey","_props$ajvErrors","clientid","_courseInSchema$value","properties","questions","items","_item$required","optionid","oneOf","_priceInSchema$requir","priceInSchema","p","_selectedPrice$value","installmentgroups","installments","valueType","emit","error","split","instancePath","errorFieldId","checkIfCorrectItemOrTrue","splititem","isNaN","courseName","$n","emitChanges","emitted","courseAndLessonId","details","a","deep","clientRequired","inputType","pin","label","pattern","birthday","participantTooYoung","ageLimitDates","minBirthDate","participantTooOld","maxBirthDate","trim","permissionRadioSelectFields","missingProperty","calculateMinAndMaxBirthDates","courseStartDate","courseEndDate","minimumAgeOnFirstDay","startDate","endDate","subYears","referenceDate","endOfDay","parseBirthDateFromPin","day","month","birthYearLastTwoDigits","adjustBirthDateForLastHundredYears","birthDate","currentYear","getFullYear","birthYear","getMonth","getDate","isParticipantTooYoung","isBefore","isParticipantTooOld","isAfter","clientFields","otherFields","initFields","reduce","acc","_field$oneOf4","fieldName","_finnishOpt","_field$oneOf3","finnishOpt","_field$oneOf","_field$oneOf2","currentLang","opt","selectPinField","pinFields","billingIdField","fullPinField","_f$pattern","birthdayPinField","_f$pattern2","propertiesToClientFields","entries","clientField","updateForm","fields","registrations","reg","allOf","fieldCollection","flat","payer","_partition","partition","_partition2","fieldsWithoutPinWithDuplicates","pinField","fieldsWithoutPin","uniqWith","uniq","r","_observer$value","val","validate","_ageLimits$minBirthDa","_ageLimits$maxBirthDa","RegistrationForm","CartItem","selectedCartItemIds","selectedCartItems","ci","index","arr","findIndex","existing","_a$course","_b$course","_a$course2","_b$course2","_a$lesson","_b$lesson","localeCompare","hasPayer","newClient","_course$begins","_course$ends","result","clientpinrequired","onlycompanybilling","companyBilling","payerType","payerAjvErrors","payerFromClientInitial","generateFields","SummerSchool","clientMinimumRequired","getOr","values","pfc","_props$schema$propert","payerFields","billingid","cartItemId","registrationToLessons","courseCode","clientName","subitem","spareamount","productstotal","culturevoucher","sportsvoucher","canpaynow","mustpaynow","discountcode","checkDiscount","termsAccepted","sendRegistration","_cartItem$course","c","cartItem","firstname","lastname","sparecounter","paymentlater","hasAnySpare","some","useTitle","brandIsLoaded","setTitle","handle","ClientCard","PayerCard","CartSummary","_useGetRegistrationFo","registrationForm","_usePostRegistrationP","_useAddToCart","addToCartRequest","addToCartState","addToCartError","_usePostRegistration","postRegStatus","postRegState","postRegResponse","_useDeleteFromCart","deleteFromCart","deleteFromCartState","_useCartItems","getCartItems","setCartItems","cartItemsState","setCartStatus","_useToast","_useTitle","validationResults","deletedItemId","showPayer","_schema$value","registrationsSchemaItemToRegistration","schemaItem","_formCourse$installme","_formCourse$installme2","_schemaItem$propertie","schemaClient","schemaId","schemaClientProps","formClient","_cartItem$itemid","courseId","formCourse","pick","isArray","ret","_cartItems$value","getCoursePriceDefaults","_item$course","defaultPrice","_default","_defaultPrice$install","courseDef","formatAjvErrors","matches","exec","replaceAll","courseDefaults","now","updatedClient","set","newPayer","to","from","_cartItems$value2","_cartStatus$value2","confirm","cancelText","hasIcon","unlisted","onUnmounted","grouped","groupBy","zipAll","cartItemsForDifferentCoursesWithUndefineds","cis","cartItemIds","intersection","_cartItems$value3","_cartItems$value4","$RefParser","_postRegResponse$valu2","_postRegResponse$valu3","_postRegResponse$valu5","_postRegResponse$valu6","CONFLICT","_postRegResponse$valu","duplicateerrors","errormessages","resource","BAD_REQUEST","_postRegResponse$valu4","clError","oldValue","newValue","pricesChanged","removeEmpty","obj","inputIsArray","input","newObj","forEach","k","numberOfForms","sendPayer","payerOnlyValidKeys","every","nextTick","firstError","scrollOffset","elementPosition","getBoundingClientRect","top","offsetPosition","pageYOffset","scrollTo","behavior","CourseInfoDlField","images","notification","askabout","files","file","additionalImages","image","sortBy","periods","period","keywords","lessons","addToCart","Teacher","Location","Ectscredits","latlon","jsonld","horizontal","show","Period","spacing","Periods","lessoncount","Weekdays","MobileMinimal","formattedWeekdays","times","ectscredits","teacher","locationSingleLine","locationMapLink","$emit","RegistrationTime","registrationbegins","registrationendssoft","registrationopen","PriceClasses","formatWeekdays","days","groupedDays","setISODay","_props$course$periods","beginsAndEndsFromLesson","_props$course","_p$keywords","isMobileOneLinerPossible","center","MAP_OPTIONS","URL","markerIcon","zoomSnap","scrollWheelZoom","zoomControl","LMap","LTileLayer","LMarker","LControlZoom","LTooltip","coordinates","latLng","lat","lon","markercolor","colors","primaryColor","divIcon","className","iconSize","html","row","_props$period$lessons","_props$course$lessons","_props$course$lessons2","Availability","full","availability","places","getAvailability","cancelled","interrupted","sparefull","sparesAvailable","spareavailable","almostfull","available","registrationClosed","CourseAvailability","_useGetCourse","getCourseRequest","l","_l$participantcount","newState","oldState","courseInfoDlFields","registrationendshard","getDefaultPrice","getPriceEuros","CourseInfoDl","_props$course2","$route","registrationlink","_props$course3","fullpath","main","mainImage","showFullImage","clickable","showToggle","toggleShowImage","alt","fallbackAltText","imgNatural","imgVisible","imageElement","invisibleImageElement","onImageSizeChange","_imageElement$value","_imageElement$value2","height","offsetHeight","width","offsetWidth","imageSizeObserver","ResizeObserver","observe","setImageNaturalDimensions","naturalHeight","naturalWidth","onload","visibleAspectRatio","naturalAspectRatio","fileName","rawFileName","pop","decodeURIComponent","department","category","subject","hasKeywords","SocialShare","LessonsCollapse","LessonsRegistration","RegistrationBox","CourseMap","CourseImage","CourseFile","BreadCrumbs","parseQueryAndGetCourse","_router$currentRoute$","unlistedIdRaw","reqIdRaw","expiryRaw","hmacRaw","previewRaw","checkError","_course$value","_course$value2","_course$value3","_course$value4","_course$value5","_course$value6","_course$value7","_course$value8","_course$value9","_course$value10","_course$value11","_course$value12","_course$value13","_course$value14","_course$value15","_course$value19","_course$value20","_course$value21","description","numberOfCredits","catalogitems","teaches","learningobjectives","contentLocation","addressLocality","streetAddress","postalCode","geo","latitude","longitude","hasCourseInstance","_course$value16","_course$value17","_course$value18","instructor","inLanguage","offers","flatMap","priceCurrency","provider","author","help","_useGetHelp","getHelpState","setKeyword","setFilters","courseCount","setSorting","useSearchParams","filters","updateValues","filtersInQuery","keywordsInQuery","COURSES_ON_PAGE","changePage","currentPage","notifications","notificationsColumns","mobileCardOpen","mapModalOpen","mobileMinimalSection","addButtonDisabled","toggleMobileCardOpen","_props$participantcou","addButtonShown","showDetailedPrice","_props$course$prices","_props$course$prices2","_props$course$prices$","CourseCard","_useListCourses","listCoursesResponse","listCoursesState","courseListParams","dir","element","getElementById","pos","before","filterGroup","toggleGroupOpen","getFilterGroupToggleButtonLabel","isGroupOpen","disabled","inputId","subFilters","Dateinput","getDatefilterValue","clearDate","dateChanged","filterClick","getFilterCheckboxValue","isIndeterminateFilter","translatelabel","stopPropagation","toggleSubfilterGroup","openSubFilters","subFilter","getSubFilterCheckboxValue","resetFilterGroups","filterNames","getKeyword","catalogItem","filterGroups","selectedParents","useCatalogFilters","_localStorage$getItem","_useSearchParams","queryString","_useCatalogSettings","settings","_useGetCatalog","_useGetCatalogUnfilte","catalogUnfiltered","getCatalogUnfiltered","closedGroups","difference","Categorysubject","Tag","Teachingformat","Department","Category","Subject","Language","Levelofstudy","Unit","combinedCatalog","mapValues","catalogItems","filtered","found","_c$keywords","_ci$keywords","coursecount","desiredGroupOpenState","subFilterKeyword","filterShouldBeHidden","catalogItemToFilter","subCatalogItems","sci","kw","subFilterCourseCountSum","sumBy","siblings","fetchFilters","_combinedCatalog$valu","tag","teachingformat","levelofstudy","educationsector","unit","educationtype","locationgroup","coursetype","updateCourseCounts","_settings$value","_settings$value2","enabledcatalogitemtypes","Educationsector","Locationgroup","Educationtype","Coursetype","_settings$value3","updateFilterGroups","resetCatalogFilterGroups","groupName","fg","getFilterGroupIndividualFilters","filterGroupFilters","findFiltersForKeyword","catalogFilters","filteredFilters","indexOf","parseDateinputFilter","rest","comparatorMatch","dateMatch","codeStr","comparatorStr","dateIsValid","dateStr","toLocaleDateString","getFilterTags","currentFilters","tagsWithDuplicates","_filter$parent","siblingKeyword","uniqBy","_useCatalogFilters","subFilterKeywords","sf","sibling","setHours","coursesbeginsFilter","dateFilter","filterGroupName","filterModalOpen","filterCount","_t","currentKeyword","searchKeyword","inputValue","updateValue","search","updateFilterCount","promotions","cardsShown","promotion","VClamp","_useGetPromotions","getPromotions","windowWidth","innerWidth","onWidthChange","placeholder","iconRight","_k","keyCode","debouncedSearch","debounce","sortModel","setSort","Name","Asc","Desc","Begins","Code","newSort","herotitle","SearchInput","tags","removeFilter","getTagText","resetAllFilters","getSearchAsFilter","clearSearch","getFiltersAndTags","removableFilter","selectedKeyword","selectedParentKeyword","_parseDateinputFilter","scrollToFilters","scrollButtonVisible","mainContainer","headerContainers","getElementsByClassName","scrollTargetOffset","clientHeight","mainContainerRelativePosition","scrollY","onScroll","elements","scrolledPastFilters","throttledOnScroll","throttle","PromotionCarousel","CourseList","Hero","CatalogFilters","MobileFilters","Sort","FilterTags","ScrollToFilters","catalogSettings","defaultCourseSortOrder","_catalogSettings$valu","selectedFilters","selectedSorting","setSortingByCourseSortOrder","sortfield","Datedesc","searchInput","test","titleTranslationKey","showRegistrationPrices","showInvoices","openInvoices","showPaymentColumn","closedInvoices","methodsofpayment","paymenttext","invoices","total","paymenturl","toggleDetails","column","$table","openedDetailed","invoice","toLowerCase","Invoice","showTotalAmountColumn","methodOfPayment","action","idx","infotext","requestdata","handlePaymentRequest","getPaymentTokenState","Paytrail","logo","paytrailLogo","Cpu","cpuLogo","Smartum","smartumLogo","KulttuuriPassi","epassiLogo","SporttiPassi","PaytrailApi","Turku","turkuLogo","Bambora","vismapayLogo","methodsOfPayment","paymentText","_useGetPaymentToken","getPaymentTokenResponse","getPaymentTokenErrorMessage","_getPaymentTokenRespo","hasInvoices","queuetext","twoColumn","cancelRegistration","cancellationurl","lastcancellationdate","showPrices","unitprice","vatpercentage","vat","groupedvatamounts","net","_props$registration","productsgroupedvatamounts","$router","allRegistrationsAreFree","someRegistrationsAreSpare","_onConfirm","axios","go","gva1","gva2","gva","InvoiceCard","PaymentCard","RegistrationCard","closedInvoiceFilter","senttoaccounting","Registration","_useGetMyRegistration","getMyRegisrationsState","emailRaw","submit","_usePostMyRegistratio","_registration$value","_registration$value3","gtm","useGtm","purchaseValue","invoicestotal","googleTagManagerConversion","trackEvent","event","metaPixelConversion","_registration$value2","content_ids","_product$course","_m","_useGetPaymentRequest","getPaymentRequestState","isSuccess","isOkError","isCancel","invalidParameters","_useGetRegistration","getRegisrationState","referencenumberRaw","VueRouter","routes","TenantLanguage","children","Home","Cart","SingleRegistration","Payment","Help","Course","MyRegistrationsLoginLink","NewRegistration","MyRegistrations","alias","NotFound","mode","scrollBehavior","selector","y","initVeeValidate","extend","regex","args","stringArray","minDate","parsedBirthDate","maxDate","Buefy","VueDOMPurifyHTML","default","ADD_ATTR","productionTip","h","App","$mount","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","exports","module","__webpack_modules__","m","deferred","O","chunkIds","priority","notFulfilled","Infinity","fulfilled","j","splice","getter","__esModule","d","definition","o","defineProperty","enumerable","g","globalThis","Function","hmd","create","prop","hasOwnProperty","Symbol","toStringTag","nmd","paths","installedChunks","chunkId","webpackJsonpCallback","parentChunkLoadingFunction","data","moreModules","chunkLoadingGlobal","self","__webpack_exports__"],"sourceRoot":""}