Commit 7d391692 authored by Øyvind Julsrud's avatar Øyvind Julsrud
Browse files

Merge branch 'master' into DEICH-5917-refactor

parents 1435d124 0907d652
# Cronicle
Cronicle is a scheduler for all the periodic jobs that the various components in the system needs to run. This includes backup of databases, and all the different tasks that Koha must perform routinely.
## Backup and restore of Cronicle configuration data
A cronicle-job is set up to take a daily backup of Cronicles configuration. This includes all the jobs and the shcedule, but does not include the output of the historical job runs.
The backup is just a single text file, and is stored in the same place as MariaDB and Virtuoso backups are stored.
To perform a restore, the backuped file must be put in to the mounted data volume at a designated location, so that cronicle will import it on next restart of the container.
To fetch the latest cronicle backup and restore config run the following (se deployment-config repo for environment variables):
```
> docker stop cronicle # cronicle MUST be stopped before restore
> docker run --rm --network bridge -v ${SSH_PRIVATE_KEY_MOUNT}:ro -v ${SSH_PUBLIC_KEY_MOUNT}:ro -v deichman_cronic
le_data:/cronicle-data instrumentisto/rsync-ssh rsync -e "ssh -o StrictHostKeyChecking=no" -azPv --delete --progress
--info=progress2 --copy-unsafe-links datafetch@${DATAFETCH_HOST}:$backup_dir /cronicle-data/restore
> docker start cronicle
```
\ No newline at end of file
......@@ -37,5 +37,13 @@ then
touch $DATA_DIR/.setup_done
fi
# Import backup if it exists
if [ -f /data/restore/config.txt ]
then
echo "restoring config from backup"
/opt/cronicle/bin/control.sh import /data/restore/config.txt
rm /data/restore/config.txt
fi
# Run cronicle
/usr/local/bin/node "$LIB_DIR/main.js"
\ No newline at end of file
#!/bin/bash
set -e
cd /backups
DATE=`date +%Y%m%d%H%M%S`
DIR_NAME="cronicle_$DATE"
mkdir -p $DIR_NAME
echo "exporting cronicle config to $DIR_NAME/config.txt"
/opt/cronicle/bin/control.sh export /backups/$DIR_NAME/config.txt --verbose
chown -R 1000:1000 /backups/$DIR_NAME
echo " { \"chain_data\": { \"dirname\": \"$DIR_NAME\"}}"
......@@ -67,18 +67,20 @@ export default class Autocomplete extends React.Component {
onSelect(item, index) {
this.props.tracker.interaction();
let selected = item.label;
// DEICH-5736: strip all quotes from autocomplete suggestions so we don't end up in phrase search(es)
const label = item.label.replaceAll('"', "");
let selected = label;
if (item.typeLabel === "emne") {
selected = `emne:"${item.label}"`;
selected = `emne:"${label}"`;
} else if (item.typeLabel === "serie") {
selected = `serie:"${item.label}"`;
selected = `serie:"${label}"`;
} else if (item.typeLabel === "tittel") {
selected = `tittel:"${item.label}"`;
selected = `tittel:"${label}"`;
} else if (
item.typeLabel === "person" ||
item.typeLabel === "korporasjon"
) {
selected = `aktør:"${item.label}"`;
selected = `aktør:"${label}"`;
}
this.props.tracker.click(selected, index, this.props.value);
this.setState({ items: [], hidden: true });
......
......@@ -18,7 +18,7 @@ const InspirationSubNav = ({ mediaType }) => (
as: `/inspirasjon/${mediaType}/anbefalte`
},
{
text: "Lister",
text: "Temalister",
url: "/inspirasjon/[mediaType]/lister",
as: `/inspirasjon/${mediaType}/lister`
},
......
......@@ -24,6 +24,36 @@
background-color: var(--col-gray-1);
border-radius: var(--border-radius);
}
.loan-order-box {
.loan-order-box__header {
margin-top: var(--spacing-4);
}
.loan-order-box__button {
margin-top: var(--spacing-4);
}
.loan-order-box__text {
margin-top: var(--spacing-4);
}
}
}
@media (max-width: 600px) {
.loan-order-box {
.loan-order-box__header {
order: 1;
}
.loan-order-box__button {
order: 3;
}
.loan-order-box__text {
order: 2;
}
}
}
/* Print styles for loanlist */
......
import React, { Fragment } from "react";
import PropTypes from "prop-types";
import { Modal } from "@digibib/deichman-ui";
import autoBind from "auto-bind";
import { connect } from "react-redux";
import FocusTrap from "focus-trap-react";
import Router from "next/router";
import {
reservePublication,
remoteReservePublication,
resetReservationError
} from "../../store/reservation";
import PropTypes from "prop-types";
import React, { Fragment } from "react";
import { connect } from "react-redux";
import {
fetchKohaBranches,
kohaBranchesForDropdownSelector
} from "../../store/libraries";
import ReservationForm from "../ReservationForm";
import {
remoteReservePublication,
reservePublication,
resetReservationError
} from "../../store/reservation";
import ModalPortal from "../ModalPortal/ModalPortal";
import RemoteReservationForm from "../RemoteReservationForm";
import { Modal } from "@digibib/deichman-ui";
import "./styles.css";
import ReservationForm from "../ReservationForm";
import ReservationSelector from "./ReservationSelector";
import ModalPortal from "../ModalPortal/ModalPortal";
import "./styles.css";
/**
* Responsible for reserving items from the catalog
......@@ -188,6 +185,7 @@ class ReservationContainer extends React.Component {
limitedToBranches.length > 0
? branches.filter(branch => limitedToBranches.includes(branch.value))
: branches;
return (
<ModalPortal>
<div className="reservation-wrapper">
......@@ -201,7 +199,7 @@ class ReservationContainer extends React.Component {
name="Reserver"
visible={this.state.show}
onClose={this.handleCloseModal(false)}
sizeW="40rem"
sizeW="43.75rem"
showClose
>
<div
......
......@@ -18,6 +18,7 @@ export default function ReservationSelector({
const onFavourite = recordId => dispatch(toggleFavourites({ recordId }));
const userCategory = useSelector(state => state.auth.userData.category);
const kohaUrl = useSelector(state => state.application.externalUrls.koha);
const numPublications = publications?.length > 0 ? publications.length : "";
const reservablePublications = publications.filter(
pub => !notReservableReason(pub, copies)
......@@ -39,7 +40,7 @@ export default function ReservationSelector({
};
return (
<Block top={4}>
<h2>Velg en utgave</h2>
<h2>Velg en utgave {`(${numPublications})`}</h2>
<PublicationList
publications={reservablePublications}
copies={copies}
......
.reservation-wrapper {
.modal__inner {
@media (--medium) {
max-height: 50vh;
max-height: 80%;
}
}
}
......@@ -32,7 +32,7 @@
flex-grow: 0;
}
.reserve-work-widget--availability {
.reserve-work-widget__availability {
margin-top: var(--spacing-4);
}
......
const alerts = {
IDPORTEN_UNDER_15: {
type: "warning",
message:
"Du er under 15 år og kan dessverre ikke registrere deg selv som voksen på deichman.no. Du har derfor blitt automatisk logget ut av ID-porten.",
link: "/registrer-deg/selvreg/barn",
linkText: "Registrer deg som barn her."
}
};
export default alerts;
......@@ -123,12 +123,12 @@ class EmployeePage extends React.Component {
items={items}
totalCount={totalCount}
hideAvatars
headline={`${pluraliseName(displayName)} lister`}
headline={`${pluraliseName(displayName)} temalister`}
showCount={true}
expandHandler={expandCallback}
collapseHandler={collapseCallback}
collapseLabel="Se færre lister"
expandLabel="Se flere lister"
collapseLabel="Se færre temalister"
expandLabel="Se flere temalister"
/>
);
}}
......
import React, { Fragment } from "react";
import Link from "next/link";
import { withRouter } from "next/router";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import autoBind from "auto-bind";
import { getEvents } from "../store/events";
import LANDING_PAGE from "../constants/landingPage";
import ALERTS from "../constants/alerts";
import Head from "../components/Head";
import EventGrid from "../components/EventGrid";
......@@ -19,7 +21,8 @@ import {
Icon,
Flex,
Grid,
GridItem
GridItem,
Alert
} from "@digibib/deichman-ui";
import HighlightedServicesContainer from "../components/HighlightedServices/HighlightedServicesContainer";
import { getCampaigns } from "../store/campaigns";
......@@ -38,7 +41,11 @@ class IndexPage extends React.Component {
};
render() {
const { allEvents = [], highlightedEvents = [] } = this.props.events;
const {
events: { allEvents = [], highlightedEvents = [] },
router
} = this.props;
const { query = {} } = router;
const inlineFilterOptions = [
{
......@@ -87,9 +94,37 @@ class IndexPage extends React.Component {
}
];
// keycloak seems to lowercase query params in redirect URLs,
// so in case it's after an SSO logout, uppercase the alert key
const alertToDisplay = ALERTS[query.alert?.toUpperCase()];
return (
<Fragment>
<Head title="Deichman.no" />
{alertToDisplay && (
<Container>
<Alert
type={alertToDisplay.type}
closeLabel="Lukk"
onClose={() => {
router.replace("/", undefined, { shallow: true });
}}
>
<Block top={4} bottom={4}>
<p>{alertToDisplay.message}</p>
{alertToDisplay.link && (
<Block top={4}>
<p>
<Link href={alertToDisplay.link}>
<a>{alertToDisplay.linkText}</a>
</Link>
</p>
</Block>
)}
</Block>
</Alert>
</Container>
)}
<CampaignZone type="top" page="Forside" />
<Container>
{highlightedEvents.length > 0 && (
......@@ -168,7 +203,8 @@ class IndexPage extends React.Component {
IndexPage.propTypes = {
events: PropTypes.object.isRequired,
getEvents: PropTypes.func.isRequired
getEvents: PropTypes.func.isRequired,
router: PropTypes.object.isRequired
};
function mapStateToProps(state) {
......@@ -188,4 +224,4 @@ function mapDispatchToProps(dispatch) {
export default connect(
mapStateToProps,
mapDispatchToProps
)(IndexPage);
)(withRouter(IndexPage));
......@@ -135,7 +135,7 @@ const InspirationMediaPage = props => {
<Block top={6} responsive>
<ListGrid
items={hits.slice(0, 4)}
headline="Nyeste lister fra ansatte"
headline="Nyeste temalister fra ansatte"
isHorizontal
/>
</Block>
......@@ -143,7 +143,7 @@ const InspirationMediaPage = props => {
<ArrowLink
href="/inspirasjon/[mediaType]/lister"
as={`/inspirasjon/${mediaType}/lister`}
text="Se alle lister"
text="Se alle temalister"
/>
</Fragment>
)}
......
......@@ -51,7 +51,7 @@ const ListPage = props => {
return (
<Fragment>
<Head title={`Lister fra ansatte - ${mediaType} - Deichman.no`} />
<Head title={`Temalister fra ansatte - ${mediaType} - Deichman.no`} />
<A11yJumpTo target="#results" text="Hopp til resultater" />
......@@ -59,7 +59,7 @@ const ListPage = props => {
<Container id="results">
<Block top={6} bottom={8} responsive>
<h2>Lister fra ansatte ({totalHits})</h2>
<h2>Temalister fra ansatte ({totalHits})</h2>
</Block>
<Block top={8} bottom={8} responsive>
<ListGrid
......
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import { useDispatch } from "react-redux";
import Router from "next/router";
import { logout, getStatus } from "../store/auth";
import Head from "../components/Head";
import FullScreen from "../components/FullScreen";
import { Block, Text } from "@digibib/deichman-ui";
const LogoutPage = props => {
const { query } = props;
const dispatch = useDispatch();
useEffect(() => {
(async () => {
const isLoggedIn = await dispatch(getStatus());
if (isLoggedIn) {
dispatch(logout(query));
} else {
Router.push("/");
}
})();
}, []);
return (
<>
<Head title="Logg ut - Deichman.no" />
<FullScreen>
<h1 className="h2">Vennligst vent</h1>
<Block top={4}>
<Text gray>Du blir straks logget ut.</Text>
</Block>
</FullScreen>
</>
);
};
LogoutPage.getInitialProps = ({ query }) => {
return {
query
};
};
LogoutPage.propTypes = {
query: PropTypes.object.isRequired
};
export default LogoutPage;
......@@ -147,27 +147,32 @@ class MyLoans extends React.Component {
hasPurreOrKemnerSak={hasPurreOrKemnerSak}
isJuvenile={isJuvenile}
>
<Block bottom={4}>
<Flex justify="space-between" wrap>
<Block top={4}>
<h3>Lån med gebyr</h3>
</Block>
<Block top={4}>
<Button
disabled={kemnersaker && kemnersaker.length > 0}
loading={isRequestingNetsUrl}
onClick={() => this.props.startPayFine(purreId)}
primary
>
Betal gebyr 100,-
</Button>
</Block>
</Flex>
</Block>
<p>
Du har purregebyr følgende lån. Bibliotekkortet ditt
åpnes igjen når gebyret er betalt.
</p>
<div className="loan-order-box">
<Block bottom={4}>
<Flex justify="space-between" wrap>
<div className="loan-order-box__header">
<h3>Lån med gebyr</h3>
</div>
<div className="loan-order-box__button">
<Button
disabled={kemnersaker && kemnersaker.length > 0}
loading={isRequestingNetsUrl}
onClick={() => this.props.startPayFine(purreId)}
primary
>
Betal gebyr 100,-
</Button>
</div>
<div className="loan-order-box__text">
<p>
Du har purregebyr følgende lån.
Bibliotekkortet ditt åpnes igjen når gebyret er
betalt.
</p>
</div>
</Flex>
</Block>
</div>
</LoanList>
</Outlined>
)}
......
import React, { Fragment } from "react";
import PropTypes from "prop-types";
import Head from "../../components/Head";
import { completeRegistrationPages } from "../../constants/navItems";
import { withAuthSync } from "../../utilities/auth";
import { connect } from "react-redux";
import FullScreen from "../../components/FullScreen";
import autoBind from "auto-bind";
import SubNav from "../../components/SubNav";
import { fetchIdPortenInfo } from "../../store/idporten";
import { logout } from "../../store/auth";
import { Container, Block, Grid, GridItem, Loader } from "@digibib/deichman-ui";
class WelcomePageUnder15 extends React.Component {
constructor(props) {
super(props);
autoBind(this);
}
componentDidMount() {
setTimeout(() => {
this.props.logout();
}, 5000);
this.props.fetchIdPortenInfo();
}
render() {
const { idporten } = this.props;
return (
<Fragment>
<Head
title="Min side - Deichman.no"
description="Velkommen som låner på Deichman.no"
/>
<Head title="Min side - Deichman.no" />
<SubNav items={completeRegistrationPages} />
{idporten.isRequestingIdPortenInfo ? (
<FullScreen>
<Loader />
</FullScreen>
) : (
<Container>
<Block top={8}>
<Grid>
<GridItem large="two-fifths" />
<GridItem large="three-fifths">
<h2 className="h3">
Hei {idporten.name}, {idporten.isRequestingIdPortenInfo}
</h2>
<Block top={4}>
<p>
Du er under 15 år og kan desverre ikke registrere deg selv
som voksen Deichman.no. Du vil bli logget ut
automatisk.
</p>
</Block>
</GridItem>
</Grid>
</Block>
</Container>
)}
</Fragment>
);
}
}
WelcomePageUnder15.propTypes = {
idporten: PropTypes.object.isRequired,
fetchIdPortenInfo: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired
};
function mapDispatchToProps(dispatch) {
return {
logout: () => dispatch(logout()),
fetchIdPortenInfo: () => dispatch(fetchIdPortenInfo())
};
}
const mapStateToProps = state => ({
idporten: state.idporten
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(withAuthSync(WelcomePageUnder15));
......@@ -123,7 +123,7 @@ routes.get("/idporten", async (request, response) => {
} else if (authorities.includes("ROLE_IDPORTEN_UNDER_15")) {
request.session.jwt_token = resolvedResponse.access_token;
request.session.refresh_token = resolvedResponse.refresh_token;
response.redirect("/min-side/under-15");
response.redirect("/logg-ut?alert=IDPORTEN_UNDER_15");
} else if (authorities.includes("ROLE_IDPORTEN_MINIMAL_TOKEN")) {
request.session.jwt_token = resolvedResponse.access_token;
request.session.refresh_token = resolvedResponse.refresh_token;
......
......@@ -169,7 +169,7 @@ export function login(data, preventRedirect, target) {
};
}
export function logout() {
export function logout(redirectQueryParams) {
return dispatch => {
dispatch(logoutRequest());
fetch("/api/auth/logout", {
......@@ -181,9 +181,11 @@ export function logout() {
dispatch(reset());
authLogout();
if (res && res.authServerUrl) {
window.location = `${res.authServerUrl}/exit?logoutUrl=${
window.location.origin
}`;
const queryParams = redirectQueryParams
? `?${new URLSearchParams(redirectQueryParams).toString()}`