mirror of https://gitlab.crans.org/nounous/nixos
Merge branch 'main' into hosts-v2
commit
a7a5b59fdb
|
|
@ -1 +1,2 @@
|
|||
result
|
||||
*.qcow2
|
||||
|
|
|
|||
24
flake.lock
24
flake.lock
|
|
@ -50,11 +50,11 @@
|
|||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778716662,
|
||||
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
||||
"lastModified": 1787559586,
|
||||
"narHash": "sha256-onL0VLf9vPllmT0H/OlURIU5r5t5WIEl7t4tVNKT0Nw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
||||
"rev": "9d0d87172c374f89da73c1cfe6d81ae62feac1f1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -86,11 +86,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1780902259,
|
||||
"narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=",
|
||||
"lastModified": 1787848966,
|
||||
"narHash": "sha256-7gDpu5hpq0rOYnYMcOWqSzquK4HA/xU8Xf3CjUBOOJA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca",
|
||||
"rev": "d57af924f160a5084293c71c2043f058bd1cdb60",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -102,11 +102,11 @@
|
|||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1777168982,
|
||||
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
|
||||
"lastModified": 1785031560,
|
||||
"narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
|
||||
"rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -145,11 +145,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780220602,
|
||||
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
|
||||
"lastModified": 1786901030,
|
||||
"narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
|
||||
"rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
40
flake.nix
40
flake.nix
|
|
@ -38,18 +38,17 @@
|
|||
./modules
|
||||
agenix.nixosModules.default
|
||||
];
|
||||
get_hosts_names = path:
|
||||
attrNames (
|
||||
attrsets.filterAttrs (name: type: type == "directory")
|
||||
(readDir path)
|
||||
) ;
|
||||
get_hosts_names =
|
||||
path: attrNames (attrsets.filterAttrs (name: type: type == "directory") (readDir path));
|
||||
hostVM = get_hosts_names ./hosts/vm;
|
||||
hostPhysique = get_hosts_names ./hosts/physiques;
|
||||
get_info =
|
||||
path: lists:
|
||||
filter (x: x != null)
|
||||
(map (name:
|
||||
let filePath = path + "/${name}/info.nix";
|
||||
filter (x: x != null) (
|
||||
map (
|
||||
name:
|
||||
let
|
||||
filePath = path + "/${name}/info.nix";
|
||||
in
|
||||
if builtins.pathExists filePath then
|
||||
# let eval = evalModules {
|
||||
|
|
@ -77,19 +76,21 @@
|
|||
listInfoToAttrs =
|
||||
path: infos:
|
||||
listToAttrs (
|
||||
map (info:
|
||||
{
|
||||
map (info: {
|
||||
name = info.crans.name;
|
||||
value = info;
|
||||
}
|
||||
) infos)
|
||||
;
|
||||
attrsToNixosSystem = path: all_hosts: hosts:
|
||||
mapAttrs (name: info:
|
||||
}) infos
|
||||
);
|
||||
attrsToNixosSystem =
|
||||
path: all_hosts: hosts:
|
||||
mapAttrs (
|
||||
name: info:
|
||||
nixosSystem {
|
||||
specialArgs = { hosts = all_hosts; inherit inputs;};
|
||||
modules = [(path + "/${name}")] ++ [info] ++ baseModules
|
||||
;
|
||||
specialArgs = {
|
||||
hosts = all_hosts;
|
||||
inherit inputs;
|
||||
};
|
||||
modules = [ (path + "/${name}") ] ++ [ info ] ++ baseModules;
|
||||
}
|
||||
) hosts;
|
||||
attrs_vm_info = listInfoToAttrs ./hosts/vm hosts_vm_info;
|
||||
|
|
@ -97,8 +98,7 @@
|
|||
attrs_all_info = attrs_physique_info // attrs_vm_info;
|
||||
in
|
||||
(attrsToNixosSystem ./hosts/vm attrs_all_info attrs_vm_info)
|
||||
//
|
||||
{
|
||||
// {
|
||||
cransIso = nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = inputs;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
environment.systemPackages = with pkgs; [
|
||||
imagemagick # Pour la compression des PDFs
|
||||
];
|
||||
|
||||
services.stirling-pdf = {
|
||||
enable = true;
|
||||
environment = {
|
||||
|
|
@ -17,6 +18,11 @@
|
|||
};
|
||||
};
|
||||
|
||||
systemd.services.stirling-pdf.serviceConfig = {
|
||||
Restart = "always";
|
||||
RestartSec = "5s";
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
$magicWords = [];
|
||||
$magicWords['en'] = [
|
||||
'calendar' => [ 0, 'calendar' ],
|
||||
];
|
||||
$magicWords['fr'] = [
|
||||
'calendar' => [ 0, 'calendrier' ],
|
||||
];
|
||||
?>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "FullCalendar",
|
||||
"version": "0.1.0",
|
||||
"author": [
|
||||
"Crans"
|
||||
],
|
||||
"url": "",
|
||||
"license-name": "GPL-3.0+",
|
||||
"description": "",
|
||||
"type": "parserhook",
|
||||
"requires": {
|
||||
"MediaWiki": ">= 1.39.0"
|
||||
},
|
||||
"ResourceModules": {
|
||||
"ext.fullcalendar": {
|
||||
"scripts": [
|
||||
"ical.es5.cjs",
|
||||
"fullcalendar7/fullcalendar.global.js",
|
||||
"fullcalendar7/themes/breezy/global.js",
|
||||
"fullcalendar7/locales-all/global.js",
|
||||
"fullcalendar7/fullcalendar-ical.js",
|
||||
"init.js"
|
||||
],
|
||||
"styles": [
|
||||
"fullcalendar7/skeleton.css",
|
||||
"fullcalendar7/themes/breezy/theme.css",
|
||||
"fullcalendar7/themes/breezy/palettes/rose.css"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ResourceFileModulePaths": {
|
||||
"localBasePath": "resources",
|
||||
"remoteExtPath": "fullcalendar/resources"
|
||||
},
|
||||
"AutoloadClasses": {
|
||||
"FullCalendar": "includes/FullCalendar.php"
|
||||
},
|
||||
"ExtensionMessagesFiles": {
|
||||
"FullCalendarMagic": "FullCalendarMagic.i18n.php"
|
||||
},
|
||||
"Hooks": {
|
||||
"ParserFirstCallInit": "FullCalendar::onParserFirstCallInit"
|
||||
},
|
||||
"manifest_version": 2
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Html\Html;
|
||||
|
||||
class FullCalendar {
|
||||
|
||||
// Register any render callbacks with the parser
|
||||
public static function onParserFirstCallInit( Parser $parser ) {
|
||||
// Create a function hook associating the <code>example</code> magic word with renderExample()
|
||||
$parser->setFunctionHook( 'calendar', [ self::class, 'renderCalendar' ] );
|
||||
}
|
||||
|
||||
public static function renderCalendar( Parser $parser, $param1 = '', ) {
|
||||
$parser->getOutput()->addModules( [ 'ext.fullcalendar' ] );
|
||||
|
||||
$resultHtml = Html::element( 'div', [ 'class' => 'mw-crans-fullcalendar', 'data-ics' => $param1 ] );
|
||||
return [ $resultHtml, 'noparse' => true, 'isHTML' => true ];
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Adam Shaw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
/*!
|
||||
FullCalendar iCalendar Plugin v7.0.1
|
||||
Docs & License: https://fullcalendar.io/docs/icalendar
|
||||
(c) 2026 Adam Shaw
|
||||
*/
|
||||
(function (ICAL) {
|
||||
'use strict';
|
||||
|
||||
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
||||
|
||||
var ICAL__default = /*#__PURE__*/_interopDefaultLegacy(ICAL);
|
||||
|
||||
function addDays(m, n) {
|
||||
let a = dateToUtcArray(m);
|
||||
a[2] += n;
|
||||
return arrayToUtcDate(a);
|
||||
}
|
||||
function dateToUtcArray(date) {
|
||||
return [
|
||||
date.getUTCFullYear(),
|
||||
date.getUTCMonth(),
|
||||
date.getUTCDate(),
|
||||
date.getUTCHours(),
|
||||
date.getUTCMinutes(),
|
||||
date.getUTCSeconds(),
|
||||
date.getUTCMilliseconds(),
|
||||
];
|
||||
}
|
||||
function arrayToUtcDate(a) {
|
||||
// according to web standards (and Safari), a month index is required.
|
||||
// massage if only given a year.
|
||||
if (a.length === 1) {
|
||||
a = a.concat([0]);
|
||||
}
|
||||
return new Date(Date.UTC(...a));
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
class IcalExpander {
|
||||
constructor(opts) {
|
||||
this.maxIterations = opts.maxIterations != null ? opts.maxIterations : 1000;
|
||||
this.skipInvalidDates = opts.skipInvalidDates != null ? opts.skipInvalidDates : false;
|
||||
this.jCalData = ICAL__default["default"].parse(opts.ics);
|
||||
this.component = new ICAL__default["default"].Component(this.jCalData);
|
||||
this.events = this.component.getAllSubcomponents('vevent').map(vevent => new ICAL__default["default"].Event(vevent));
|
||||
if (this.skipInvalidDates) {
|
||||
this.events = this.events.filter((evt) => {
|
||||
try {
|
||||
evt.startDate.toJSDate();
|
||||
evt.endDate.toJSDate();
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
// skipping events with invalid time
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
between(after, before) {
|
||||
function isEventWithinRange(startTime, endTime) {
|
||||
return (!after || endTime >= after.getTime()) &&
|
||||
(!before || startTime <= before.getTime());
|
||||
}
|
||||
function getTimes(eventOrOccurrence) {
|
||||
const startTime = eventOrOccurrence.startDate.toJSDate().getTime();
|
||||
let endTime = eventOrOccurrence.endDate.toJSDate().getTime();
|
||||
// If it is an all day event, the end date is set to 00:00 of the next day
|
||||
// So we need to make it be 23:59:59 to compare correctly with the given range
|
||||
if (eventOrOccurrence.endDate.isDate && (endTime > startTime)) {
|
||||
endTime -= 1;
|
||||
}
|
||||
return { startTime, endTime };
|
||||
}
|
||||
const exceptions = [];
|
||||
this.events.forEach((event) => {
|
||||
if (event.isRecurrenceException())
|
||||
exceptions.push(event);
|
||||
});
|
||||
const ret = {
|
||||
events: [],
|
||||
occurrences: [],
|
||||
};
|
||||
this.events.filter(e => !e.isRecurrenceException()).forEach((event) => {
|
||||
const exdates = [];
|
||||
event.component.getAllProperties('exdate').forEach((exdateProp) => {
|
||||
const exdate = exdateProp.getFirstValue();
|
||||
exdates.push(exdate.toJSDate().getTime());
|
||||
});
|
||||
// Recurring event is handled differently
|
||||
if (event.isRecurring()) {
|
||||
const iterator = event.iterator();
|
||||
let next;
|
||||
let i = 0;
|
||||
do {
|
||||
i += 1;
|
||||
next = iterator.next();
|
||||
if (next) {
|
||||
const occurrence = event.getOccurrenceDetails(next);
|
||||
const { startTime, endTime } = getTimes(occurrence);
|
||||
const isOccurrenceExcluded = exdates.indexOf(startTime) !== -1;
|
||||
// TODO check that within same day?
|
||||
const exception = exceptions.find(ex => ex.uid === event.uid && ex.recurrenceId.toJSDate().getTime() === occurrence.startDate.toJSDate().getTime());
|
||||
// We have passed the max date, stop
|
||||
if (before && startTime > before.getTime())
|
||||
break;
|
||||
// Check that we are within our range
|
||||
if (isEventWithinRange(startTime, endTime)) {
|
||||
if (exception) {
|
||||
ret.events.push(exception);
|
||||
}
|
||||
else if (!isOccurrenceExcluded) {
|
||||
ret.occurrences.push(occurrence);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (next && (!this.maxIterations || i < this.maxIterations));
|
||||
return;
|
||||
}
|
||||
// Non-recurring event:
|
||||
const { startTime, endTime } = getTimes(event);
|
||||
if (isEventWithinRange(startTime, endTime))
|
||||
ret.events.push(event);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
before(before) {
|
||||
return this.between(undefined, before);
|
||||
}
|
||||
after(after) {
|
||||
return this.between(after);
|
||||
}
|
||||
all() {
|
||||
return this.between();
|
||||
}
|
||||
}
|
||||
|
||||
const eventSourceDef = {
|
||||
parseMeta(refined) {
|
||||
if (refined.url && refined.format === 'ics') {
|
||||
return {
|
||||
url: refined.url,
|
||||
format: 'ics',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
fetch(arg, successCallback, // any
|
||||
errorCallback) {
|
||||
let meta = arg.eventSource.meta;
|
||||
let { internalState } = meta;
|
||||
/*
|
||||
NOTE: isRefetch is a HACK. we would do the recurring-expanding in a separate plugin hook,
|
||||
but we couldn't leverage built-in allDay-guessing, among other things.
|
||||
*/
|
||||
if (!internalState || arg.isRefetch) {
|
||||
internalState = meta.internalState = {
|
||||
response: null,
|
||||
iCalExpanderPromise: fetch(meta.url, { method: 'GET' }).then((response) => {
|
||||
return response.text().then((icsText) => {
|
||||
internalState.response = response;
|
||||
return new IcalExpander({
|
||||
ics: icsText,
|
||||
skipInvalidDates: true,
|
||||
});
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
internalState.iCalExpanderPromise.then((iCalExpander) => {
|
||||
successCallback({
|
||||
rawEvents: expandICalEvents(iCalExpander, arg.range),
|
||||
response: internalState.response,
|
||||
});
|
||||
}, errorCallback);
|
||||
},
|
||||
};
|
||||
function expandICalEvents(iCalExpander, range) {
|
||||
// expand the range. because our `range` is timeZone-agnostic UTC
|
||||
// or maybe because ical.js always produces dates in local time? i forget
|
||||
let rangeStart = addDays(range.start, -1);
|
||||
let rangeEnd = addDays(range.end, 1);
|
||||
let iCalRes = iCalExpander.between(rangeStart, rangeEnd); // end inclusive. will give extra results
|
||||
let expanded = [];
|
||||
// TODO: instead of using startDate/endDate.toString to communicate allDay,
|
||||
// we can query startDate/endDate.isDate. More efficient to avoid formatting/reparsing.
|
||||
// single events
|
||||
for (let iCalEvent of iCalRes.events) {
|
||||
expanded.push({
|
||||
...buildNonDateProps(iCalEvent),
|
||||
start: iCalEvent.startDate.toString(),
|
||||
end: (specifiesEnd(iCalEvent) && iCalEvent.endDate)
|
||||
? iCalEvent.endDate.toString()
|
||||
: null,
|
||||
});
|
||||
}
|
||||
// recurring event instances
|
||||
for (let iCalOccurence of iCalRes.occurrences) {
|
||||
let iCalEvent = iCalOccurence.item;
|
||||
expanded.push({
|
||||
...buildNonDateProps(iCalEvent),
|
||||
start: iCalOccurence.startDate.toString(),
|
||||
end: (specifiesEnd(iCalEvent) && iCalOccurence.endDate)
|
||||
? iCalOccurence.endDate.toString()
|
||||
: null,
|
||||
});
|
||||
}
|
||||
return expanded;
|
||||
}
|
||||
function buildNonDateProps(iCalEvent) {
|
||||
return {
|
||||
title: iCalEvent.summary,
|
||||
url: extractEventUrl(iCalEvent),
|
||||
extendedProps: {
|
||||
location: iCalEvent.location,
|
||||
organizer: iCalEvent.organizer,
|
||||
description: iCalEvent.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
function extractEventUrl(iCalEvent) {
|
||||
let urlProp = iCalEvent.component.getFirstProperty('url');
|
||||
return urlProp ? urlProp.getFirstValue() : '';
|
||||
}
|
||||
function specifiesEnd(iCalEvent) {
|
||||
return Boolean(iCalEvent.component.getFirstProperty('dtend')) ||
|
||||
Boolean(iCalEvent.component.getFirstProperty('duration'));
|
||||
}
|
||||
|
||||
var plugin = {
|
||||
name: 'icalendar',
|
||||
eventSourceDefs: [eventSourceDef],
|
||||
};
|
||||
|
||||
FullCalendar.globalPlugins.push(plugin);
|
||||
|
||||
})(ICAL);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,469 @@
|
|||
|
||||
:root {
|
||||
--fc-sticky-header-footer-z: 3;
|
||||
--fc-popover-z: 4;
|
||||
}
|
||||
|
||||
.fc-11 {
|
||||
z-index: var(--fc-popover-z) !important;
|
||||
}
|
||||
|
||||
.fc-HY {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/*
|
||||
This will prevent all ancestors from customizing their box size unless they use .contentBox
|
||||
*/
|
||||
.fc-jz,
|
||||
.fc-jz *,
|
||||
.fc-jz *:before,
|
||||
.fc-jz *:after {
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* classes attached to <body> */
|
||||
.fc-j2,
|
||||
.fc-j2 * {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.fc-Ki {
|
||||
-ms-overflow-style: none !important; /* IE and Edge */
|
||||
scrollbar-width: none !important; /* Firefox */
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.fc-Ki::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fc-ST {
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.fc-Z9 .fc-1g {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
|
||||
/* HACK for Safari. Can't do break-inside:avoid with flexbox items, likely b/c it's not standard:
|
||||
https://stackoverflow.com/a/60256345 */
|
||||
.fc-HC .fc-1g > * {
|
||||
float: left !important;
|
||||
}
|
||||
|
||||
[dir=rtl] .fc-HC .fc-1g > *,
|
||||
.fc-HC[dir=rtl] .fc-1g > * {
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
.fc-HC .fc-1g::after {
|
||||
content: "" !important;
|
||||
display: block !important;
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.fc-ZB {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.fc-Ld {
|
||||
cursor: n-resize !important;
|
||||
}
|
||||
|
||||
.fc-lJ {
|
||||
cursor: s-resize !important;
|
||||
}
|
||||
|
||||
.fc-Zm {
|
||||
cursor: w-resize !important;
|
||||
}
|
||||
|
||||
[dir=rtl] .fc-Zm {
|
||||
cursor: e-resize !important;
|
||||
}
|
||||
|
||||
.fc-n5 {
|
||||
cursor: e-resize !important;
|
||||
}
|
||||
|
||||
[dir=rtl] .fc-n5 {
|
||||
cursor: w-resize !important;
|
||||
}
|
||||
|
||||
.fc-Px {
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
.fc-QK,
|
||||
.fc-eh,
|
||||
.fc-M9 {
|
||||
position: absolute !important;
|
||||
box-sizing: content-box !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.fc-QK {
|
||||
padding: 10px !important;
|
||||
margin: -10px !important;
|
||||
}
|
||||
|
||||
.fc-eh {
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
margin-left: -10px !important;
|
||||
margin-right: -10px !important;
|
||||
}
|
||||
|
||||
.fc-M9 {
|
||||
padding-top: 10px !important;
|
||||
padding-bottom: 10px !important;
|
||||
margin-top: -10px !important;
|
||||
margin-bottom: -10px !important;
|
||||
}
|
||||
|
||||
/* QUESTION: why not use -left -right instead of negative margins/padding for ALL? */
|
||||
.fc-SC {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -5px;
|
||||
right: -5px;
|
||||
}
|
||||
|
||||
.fc-hi {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fc-F0 {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Border Utils */
|
||||
/* ------------------------------------------------------------------------------------------------- */
|
||||
|
||||
/* TODO: break-out to x and y. don't have "only" all */
|
||||
/* TODO: instead of "border" utils, call them "borderless" to subtract? */
|
||||
|
||||
.fc-6s {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.fc-8B {
|
||||
border-left: 0 !important;
|
||||
border-right: 0 !important;
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.fc-RJ {
|
||||
border-top: 0 !important;
|
||||
border-left: 0 !important;
|
||||
border-right: 0 !important;
|
||||
}
|
||||
|
||||
.fc-Hm,
|
||||
.fc-ge {
|
||||
border-top: 0 !important;
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.fc-Hm {
|
||||
border-inline-end: 0 !important;
|
||||
}
|
||||
|
||||
.fc-ge {
|
||||
border-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.fc-KC {
|
||||
border-left: 0 !important;
|
||||
border-right: 0 !important;
|
||||
}
|
||||
|
||||
.fc-mE {
|
||||
border-top: 0 !important;
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* for matching cell start-border, assumed to be 1px, which can't be guaranteed */
|
||||
.fc-FA {
|
||||
border-inline-start: 1px solid transparent !important;
|
||||
}
|
||||
|
||||
/* Flexbox Utils */
|
||||
/* ------------------------------------------------------------------------------------------------- */
|
||||
|
||||
.fc-5g {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.fc-Fa {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.fc-16 {
|
||||
flex-grow: 1 !important;
|
||||
}
|
||||
|
||||
.fc-Im {
|
||||
flex-grow: 1 !important;
|
||||
flex-basis: 0 !important;
|
||||
min-width: 0 !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.fc-tZ {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: use liquidX/Y elsewhere because frees up min other dimension; less collisions
|
||||
*/
|
||||
.fc-Pw {
|
||||
flex-grow: 1 !important;
|
||||
flex-basis: 0 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* Print-Safe Utils (media:screen ONLY) */
|
||||
/* ------------------------------------------------------------------------------------------------- */
|
||||
|
||||
.fc-Z9 .fc-kE,
|
||||
.fc-Z9 .fc-Xg {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
/* Table Utils */
|
||||
/* ------------------------------------------------------------------------------------------------- */
|
||||
|
||||
.fc-np {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.fc-gd {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.fc-hB {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.fc-uk {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.fc-he {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.fc-Bv {
|
||||
white-space: pre !important;
|
||||
}
|
||||
|
||||
/* Misc Utils */
|
||||
/* ------------------------------------------------------------------------------------------------- */
|
||||
|
||||
.fc-U2 {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
.fc-bB {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: eventually use this on daygrid/timegrid events' inner, time, and title, FOR PRINT
|
||||
Needed to prevent wrapping to multiple lines, increasing height, throwing off print-positioning,
|
||||
which can't rely on dynamic height detection after print-flag activated.
|
||||
*/
|
||||
.fc-pp {
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.fc-8A {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.fc-7t {
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
.fc-pK {
|
||||
inset-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.fc-wP {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
}
|
||||
|
||||
.fc-yW,
|
||||
.fc-5j {
|
||||
position: absolute !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
.fc-1K,
|
||||
.fc-bH {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
bottom: 0 !important;
|
||||
}
|
||||
|
||||
.fc-yW {
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
.fc-bH {
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
@media not print {
|
||||
.fc-dY {
|
||||
position: sticky !important;
|
||||
}
|
||||
|
||||
.fc-aX {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
/* Sticks to either left or right (the flex "start") depending on ltr/rtl */
|
||||
.fc-pZ {
|
||||
position: sticky !important;
|
||||
inset-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.fc-Mw {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
z-index: var(--fc-sticky-header-footer-z) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Only needed when padding/border must be separate from width/height */
|
||||
.fc-CU {
|
||||
box-sizing: content-box !important;
|
||||
}
|
||||
|
||||
.fc-jm {
|
||||
position: absolute !important;
|
||||
left: -10000px !important;
|
||||
}
|
||||
|
||||
.fc-oO {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.fc-MM {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.fc-Qk {
|
||||
align-items: flex-end !important;
|
||||
}
|
||||
|
||||
/* Footer Scrollbar */
|
||||
/* ------------------------------------------------------------------------------------------------- */
|
||||
|
||||
.fc-Q6 {
|
||||
position: sticky !important;
|
||||
bottom: 0 !important;
|
||||
z-index: var(--fc-sticky-header-footer-z) !important;
|
||||
}
|
||||
|
||||
.fc-bD > * {
|
||||
margin-top: -1px !important;
|
||||
}
|
||||
|
||||
.fc-bD > * > * {
|
||||
height: 1px !important;
|
||||
}
|
||||
|
||||
/* Print-Safe Utils */
|
||||
/* ------------------------------------------------------------------------------------------------- */
|
||||
|
||||
.fc-HC .fc-lF {
|
||||
-moz-column-break-inside: avoid !important;
|
||||
break-inside: avoid !important;
|
||||
}
|
||||
|
||||
.fc-HC .fc-kE {
|
||||
display: table !important;
|
||||
table-layout: fixed !important;
|
||||
width: 100% !important;
|
||||
border-spacing: 0 !important;
|
||||
border-collapse: separate !important;
|
||||
}
|
||||
|
||||
.fc-HC .fc-Xg {
|
||||
display: table-header-group !important;
|
||||
-moz-column-break-inside: avoid !important;
|
||||
break-inside: avoid !important;
|
||||
background: #fff !important;
|
||||
z-index: 9999 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.fc-Vf {
|
||||
/* min-height when multiple rows coexist, provides sane aspect-ratio for cells */
|
||||
min-height: 6em !important;
|
||||
}
|
||||
|
||||
/* Z-index */
|
||||
/* ------------------------------------------------------------------------------------------------- */
|
||||
|
||||
.fc-BP {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.fc-XV {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fc-Ah:focus-visible {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* INTERNAL MARKER -- used to mark an element for internal significance */
|
||||
/* ------------------------------------------------------------------------------------------------- */
|
||||
|
||||
.fc-vz {}
|
||||
|
||||
.fc-4B {}
|
||||
.fc-D7 {}
|
||||
.fc-wp {}
|
||||
.fc-Ex {}
|
||||
.fc-wl {}
|
||||
.fc-yM {}
|
||||
.fc-2f {}
|
||||
.fc-zR {}
|
||||
|
||||
.fc-0j {}
|
||||
|
||||
.fc-M7 {}
|
||||
|
||||
.fc-tb {}
|
||||
|
||||
.fc-ew {}
|
||||
|
||||
.fc-IZ {}
|
||||
|
||||
.fc-UH {}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
/*!
|
||||
FullCalendar (Vanilla JS) v7.0.1
|
||||
Docs & License: https://fullcalendar.io
|
||||
(c) 2026 Adam Shaw
|
||||
*/
|
||||
(function ({ H: joinClassNames, u, S, G: globalPlugins }) {
|
||||
|
||||
|
||||
// usually 11px font / 12px line-height
|
||||
const xxsTextClass = "fc-breezy-vQz";
|
||||
// outline
|
||||
const outlineWidthClass = "fc-breezy-0Bj";
|
||||
const outlineWidthFocusClass = "fc-breezy-uqo";
|
||||
const outlineWidthGroupFocusClass = "fc-breezy-nPR";
|
||||
const outlineOffsetClass = "fc-breezy-3Xj";
|
||||
const primaryOutlineColorClass = "fc-breezy-yg7";
|
||||
const primaryOutlineFocusClass = `${outlineWidthFocusClass} ${primaryOutlineColorClass}`;
|
||||
// neutral buttons
|
||||
const strongSolidPressableClass = joinClassNames("fc-breezy-hJa", "fc-breezy-AT7", "fc-breezy-b3B");
|
||||
const mutedHoverClass = "fc-breezy-qiG";
|
||||
const mutedHoverPressableClass = `${mutedHoverClass} fc-breezy-WfX`;
|
||||
const faintHoverClass = "fc-breezy-1EL";
|
||||
const faintHoverPressableClass = `${faintHoverClass} fc-breezy-qWF fc-breezy-UTk`;
|
||||
// controls
|
||||
const selectedClass = `fc-breezy-GfU fc-breezy-Oiq ${primaryOutlineFocusClass}`;
|
||||
const unselectedClass = `fc-breezy-t4l fc-breezy-hnE ${primaryOutlineFocusClass}`;
|
||||
// primary
|
||||
const primaryClass = "fc-breezy-Anp fc-breezy-ECg";
|
||||
const primaryPressableClass = `${primaryClass} fc-breezy-8mf`;
|
||||
const primaryPressableGroupClass = `${primaryClass} fc-breezy-t8l`;
|
||||
const primaryButtonClass = `${primaryPressableClass} fc-breezy-d0j ${primaryOutlineFocusClass} ${outlineOffsetClass}`;
|
||||
// secondary
|
||||
const secondaryClass = "fc-breezy-nyY fc-breezy-NK3";
|
||||
const secondaryPressableClass = `${secondaryClass} fc-breezy-StK`;
|
||||
const secondaryButtonClass = `${secondaryPressableClass} fc-breezy-nuP ${primaryOutlineFocusClass} fc-breezy-07j`;
|
||||
const secondaryButtonIconClass = "fc-breezy-XUJ fc-breezy-J7Y fc-breezy-b7A fc-breezy-tRE";
|
||||
// event content
|
||||
const eventMutedFgClass = "fc-breezy-WLU";
|
||||
const eventFaintBgClass = "fc-breezy-GjO";
|
||||
const eventFaintPressableClass = joinClassNames(eventFaintBgClass, "fc-breezy-yE8", "fc-breezy-x2T");
|
||||
// interactive neutral foregrounds
|
||||
const mutedFgPressableGroupClass = "fc-breezy-t4l fc-breezy-Wxh fc-breezy-x7E";
|
||||
// transparent resizer for mouse
|
||||
const blockPointerResizerClass = "fc-breezy-1EY fc-breezy-pps fc-breezy-vs6";
|
||||
const rowPointerResizerClass = `${blockPointerResizerClass} fc-breezy-AWB fc-breezy-hza`;
|
||||
const columnPointerResizerClass = `${blockPointerResizerClass} fc-breezy-MaV fc-breezy-uuA`;
|
||||
// circle resizer for touch
|
||||
const blockTouchResizerClass = "fc-breezy-1EY fc-breezy-3wQ fc-breezy-wsy fc-breezy-lNM fc-breezy-gmc fc-breezy-AAA";
|
||||
const rowTouchResizerClass = `${blockTouchResizerClass} fc-breezy-ERR fc-breezy-Dq8`;
|
||||
const columnTouchResizerClass = `${blockTouchResizerClass} fc-breezy-1V6 fc-breezy-F99`;
|
||||
const getNormalDayHeaderBorderClass = (info) => joinClassNames(!info.inPopover && (info.isMajor ? "fc-breezy-wsy fc-breezy-OFc" :
|
||||
!info.isNarrow && "fc-breezy-wsy fc-breezy-EAo"));
|
||||
const getMutedDayHeaderBorderClass = (info) => joinClassNames(!info.inPopover && (info.isMajor ? "fc-breezy-wsy fc-breezy-OFc" :
|
||||
!info.isNarrow && "fc-breezy-wsy fc-breezy-tTN"));
|
||||
const getNormalDayCellBorderColorClass = (info) => (info.isMajor ? "fc-breezy-OFc" : "fc-breezy-EAo");
|
||||
const getMutedDayCellBorderColorClass = (info) => (info.isMajor ? "fc-breezy-OFc" : "fc-breezy-tTN");
|
||||
const tallDayCellBottomClass = "fc-breezy-mhE";
|
||||
const getShortDayCellBottomClass = (info) => joinClassNames(!info.isNarrow && "fc-breezy-toR");
|
||||
const mutedHoverButtonClass = joinClassNames(mutedHoverPressableClass, outlineWidthFocusClass, primaryOutlineColorClass);
|
||||
const dayRowCommonClasses = {
|
||||
/* Day Row > List-Item Event
|
||||
----------------------------------------------------------------------------------------------- */
|
||||
listItemEventClass: (info) => joinClassNames("fc-breezy-Ika fc-breezy-7A6", info.isNarrow
|
||||
? "fc-breezy-148 fc-breezy-Fvv"
|
||||
: "fc-breezy-rVY fc-breezy-KzJ", info.isSelected
|
||||
? "fc-breezy-pZQ"
|
||||
: info.isInteractive
|
||||
? mutedHoverPressableClass
|
||||
: mutedHoverClass),
|
||||
listItemEventInnerClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-N2M", info.isNarrow
|
||||
? `fc-breezy-z5u ${xxsTextClass}`
|
||||
: "fc-breezy-2rx fc-breezy-a3B"),
|
||||
listItemEventTimeClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-F1o" : "fc-breezy-oQ2", "fc-breezy-t4l fc-breezy-NPw fc-breezy-TZ4 fc-breezy-pKG fc-breezy-1Zl"),
|
||||
listItemEventTitleClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-F1o" : "fc-breezy-oQ2", "fc-breezy-Oiq fc-breezy-1OT fc-breezy-TZ4 fc-breezy-pKG fc-breezy-OLq", info.timeText && "fc-breezy-IPx"),
|
||||
/* Day Row > Row Event
|
||||
----------------------------------------------------------------------------------------------- */
|
||||
rowEventClass: (info) => joinClassNames(info.isStart && (info.isNarrow ? "fc-breezy-Jzj" : "fc-breezy-Wga"), info.isEnd && (info.isNarrow ? "fc-breezy-3e1" : "fc-breezy-KYn")),
|
||||
rowEventInnerClass: (info) => info.isNarrow ? "fc-breezy-z5u" : "fc-breezy-2rx",
|
||||
/* Day Row > More-Link
|
||||
----------------------------------------------------------------------------------------------- */
|
||||
rowMoreLinkClass: (info) => joinClassNames("fc-breezy-Ika fc-breezy-wsy", info.isNarrow
|
||||
? "fc-breezy-148 fc-breezy-UIT fc-breezy-Fvv"
|
||||
: "fc-breezy-sI7 fc-breezy-rVY fc-breezy-d0j fc-breezy-KzJ", mutedHoverPressableClass),
|
||||
rowMoreLinkInnerClass: (info) => joinClassNames(info.isNarrow
|
||||
? `fc-breezy-7A6 ${xxsTextClass}`
|
||||
: "fc-breezy-KUX fc-breezy-a3B", "fc-breezy-Oiq"),
|
||||
};
|
||||
var index = {
|
||||
name: "theme-breezy",
|
||||
optionDefaults: {
|
||||
className: (info) => joinClassNames("fc-breezy-gmc fc-breezy-n5m", !(info.borderlessTop || info.borderlessBottom || info.borderlessX) && "fc-breezy-hny"),
|
||||
viewClass: (info) => {
|
||||
const hasBorderTop = !info.options.headerToolbar && !info.borderlessTop;
|
||||
const hasBorderBottom = !info.options.footerToolbar && !info.borderlessBottom;
|
||||
const hasBorderX = !info.borderlessX;
|
||||
return joinClassNames("fc-breezy-EAo", hasBorderTop && "fc-breezy-ku3", hasBorderBottom && "fc-breezy-zi1", hasBorderX && "fc-breezy-1Wx", (hasBorderTop && hasBorderX) && "fc-breezy-wko", (hasBorderBottom && hasBorderX) && "fc-breezy-L2o", !info.isHeightAuto && "fc-breezy-pKG");
|
||||
},
|
||||
/* Toolbar
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
toolbarClass: (info) => joinClassNames("fc-breezy-KRz fc-breezy-OEz fc-breezy-nYK fc-breezy-dl1 fc-breezy-1sP fc-breezy-dNl fc-breezy-XpK fc-breezy-N2M fc-breezy-Pms fc-breezy-pKG fc-breezy-EAo", !info.borderlessX && "fc-breezy-1Wx"),
|
||||
headerToolbarClass: (info) => joinClassNames("fc-breezy-zi1", !info.borderlessTop && "fc-breezy-ku3", !(info.borderlessTop || info.borderlessX) && "fc-breezy-wko"),
|
||||
footerToolbarClass: (info) => joinClassNames("fc-breezy-ku3", !info.borderlessBottom && "fc-breezy-zi1", !(info.borderlessBottom || info.borderlessX) && "fc-breezy-L2o"),
|
||||
toolbarSectionClass: "fc-breezy-yi0 fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-Pms",
|
||||
toolbarTitleClass: "fc-breezy-9ZS fc-breezy-C8a fc-breezy-Oiq",
|
||||
buttonGroupClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", !info.hasSelection && "fc-breezy-KzJ fc-breezy-eSM"),
|
||||
buttonClass: (info) => joinClassNames("fc-breezy-bCs fc-breezy-dl6 fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-9yp fc-breezy-Z9U", info.isIconOnly ? "fc-breezy-Nca" : "fc-breezy-Apf", info.buttonGroup?.hasSelection ? joinClassNames("fc-breezy-KzJ fc-breezy-1OT", info.isSelected
|
||||
? selectedClass
|
||||
: unselectedClass) : joinClassNames("fc-breezy-C8a", info.isPrimary
|
||||
? primaryButtonClass
|
||||
: secondaryButtonClass, info.buttonGroup
|
||||
? "fc-breezy-Ps8 fc-breezy-H1W fc-breezy-g3A fc-breezy-7ss fc-breezy-JIC"
|
||||
: "fc-breezy-KzJ fc-breezy-eSM fc-breezy-wsy")),
|
||||
buttons: {
|
||||
prev: {
|
||||
iconContent: () => chevronDown(joinClassNames(secondaryButtonIconClass, "fc-breezy-z44 fc-breezy-keW")),
|
||||
},
|
||||
next: {
|
||||
iconContent: () => chevronDown(joinClassNames(secondaryButtonIconClass, "fc-breezy-KxI fc-breezy-ZW3")),
|
||||
},
|
||||
prevYear: {
|
||||
iconContent: () => chevronDoubleLeft(joinClassNames(secondaryButtonIconClass, "fc-breezy-asP"))
|
||||
},
|
||||
nextYear: {
|
||||
iconContent: () => chevronDoubleLeft(joinClassNames(secondaryButtonIconClass, "fc-breezy-jmT fc-breezy-jY6"))
|
||||
},
|
||||
},
|
||||
/* Abstract Event
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
eventShortHeight: 50,
|
||||
eventColor: "var(--fc-breezy-event)",
|
||||
eventContrastColor: "var(--fc-breezy-event-contrast)",
|
||||
eventClass: (info) => joinClassNames(info.isDragging && "fc-breezy-n5m", info.event.url && "fc-breezy-JiE", info.isSelected
|
||||
? joinClassNames(outlineWidthClass, info.isDragging && "fc-breezy-tkw")
|
||||
: outlineWidthFocusClass, primaryOutlineColorClass),
|
||||
/* Background Event
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
backgroundEventColor: "var(--fc-breezy-background-event)",
|
||||
backgroundEventClass: "fc-breezy-aPk fc-breezy-jsy fc-breezy-DO7",
|
||||
backgroundEventTitleClass: (info) => joinClassNames("fc-breezy-lMo fc-breezy-L1Y", info.isNarrow
|
||||
? `fc-breezy-iS4 ${xxsTextClass}`
|
||||
: "fc-breezy-3N5 fc-breezy-a3B", "fc-breezy-sI1"),
|
||||
/* Block Event
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
blockEventClass: (info) => joinClassNames("fc-breezy-bCs fc-breezy-eYX fc-breezy-vwH fc-breezy-d0j fc-breezy-DO7", info.isInteractive ? eventFaintPressableClass : eventFaintBgClass, (info.isDragging && !info.isSelected) && "fc-breezy-iTG"),
|
||||
blockEventInnerClass: eventMutedFgClass,
|
||||
blockEventTimeClass: "fc-breezy-TZ4 fc-breezy-pKG fc-breezy-1Zl",
|
||||
blockEventTitleClass: "fc-breezy-TZ4 fc-breezy-pKG fc-breezy-OLq",
|
||||
/* Row Event
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
rowEventClass: (info) => joinClassNames("fc-breezy-Ika fc-breezy-JIC", info.isStart && joinClassNames("fc-breezy-3J4", info.isNarrow ? "fc-breezy-kmj" : "fc-breezy-QUg"), info.isEnd && joinClassNames("fc-breezy-USt", info.isNarrow ? "fc-breezy-Skl" : "fc-breezy-RNO")),
|
||||
rowEventBeforeClass: (info) => joinClassNames(info.isStartResizable && joinClassNames(info.isSelected ? rowTouchResizerClass : rowPointerResizerClass, "fc-breezy-11a")),
|
||||
rowEventAfterClass: (info) => joinClassNames(info.isEndResizable && joinClassNames(info.isSelected ? rowTouchResizerClass : rowPointerResizerClass, "fc-breezy-Tuc")),
|
||||
rowEventInnerClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", info.isNarrow ? xxsTextClass : "fc-breezy-a3B"),
|
||||
rowEventTimeClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-a7i" : "fc-breezy-C2j", "fc-breezy-1OT"),
|
||||
rowEventTitleClass: (info) => (info.isNarrow ? "fc-breezy-oQ2" : "fc-breezy-aCI"),
|
||||
/* Column Event
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
columnEventClass: (info) => joinClassNames("fc-breezy-1Wx fc-breezy-A3h fc-breezy-9Iz", info.isStart && joinClassNames("fc-breezy-ku3 fc-breezy-wko", info.isNarrow ? "fc-breezy-sEX" : "fc-breezy-zZM"), info.isEnd && joinClassNames("fc-breezy-zi1 fc-breezy-L2o", info.isNarrow ? "fc-breezy-Ika" : "fc-breezy-vUo")),
|
||||
columnEventBeforeClass: (info) => joinClassNames(info.isStartResizable && joinClassNames(info.isSelected ? columnTouchResizerClass : columnPointerResizerClass, "fc-breezy-YDC")),
|
||||
columnEventAfterClass: (info) => joinClassNames(info.isEndResizable && joinClassNames(info.isSelected ? columnTouchResizerClass : columnPointerResizerClass, "fc-breezy-fJL")),
|
||||
columnEventInnerClass: (info) => joinClassNames("fc-breezy-dl1", info.isShort
|
||||
? "fc-breezy-1sP fc-breezy-XpK fc-breezy-NWN fc-breezy-iS4"
|
||||
: joinClassNames("fc-breezy-sgX", info.isNarrow ? "fc-breezy-aCI fc-breezy-2rx" : "fc-breezy-Nca fc-breezy-Jhn"), (info.isShort || info.isNarrow) ? xxsTextClass : "fc-breezy-a3B"),
|
||||
columnEventTimeClass: (info) => (!info.isShort && (info.isNarrow ? "fc-breezy-166" : "fc-breezy-4dx")),
|
||||
columnEventTitleClass: (info) => joinClassNames(!info.isShort && (info.isNarrow ? "fc-breezy-2rx" : "fc-breezy-Jhn"), "fc-breezy-C8a"),
|
||||
/* More-Link
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
moreLinkClass: `${outlineWidthFocusClass} ${primaryOutlineColorClass}`,
|
||||
moreLinkInnerClass: "fc-breezy-TZ4 fc-breezy-pKG",
|
||||
columnMoreLinkClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-SEP" : "fc-breezy-V9v", `fc-breezy-wsy fc-breezy-d0j fc-breezy-4MR fc-breezy-KzJ ${strongSolidPressableClass} fc-breezy-vwH fc-breezy-A3h fc-breezy-9Iz`),
|
||||
columnMoreLinkInnerClass: (info) => joinClassNames(info.isNarrow
|
||||
? `fc-breezy-KUX ${xxsTextClass}`
|
||||
: "fc-breezy-iS4 fc-breezy-a3B", "fc-breezy-sI1"),
|
||||
/* Day Header
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
dayHeaderAlign: (info) => info.inPopover ? "start" : "center",
|
||||
dayHeaderClass: (info) => joinClassNames("fc-breezy-E9P", info.inPopover && "fc-breezy-zi1 fc-breezy-EAo fc-breezy-nYK"),
|
||||
dayHeaderInnerClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", (!info.dayNumberText && !info.inPopover)
|
||||
? joinClassNames("fc-breezy-Jhn fc-breezy-Fvv fc-breezy-a3B", info.isNarrow
|
||||
? "fc-breezy-aCI fc-breezy-gMS fc-breezy-t4l"
|
||||
: "fc-breezy-ZrE fc-breezy-bvX fc-breezy-C8a fc-breezy-sI1", info.hasNavLink && mutedHoverButtonClass)
|
||||
: (info.isToday && info.dayNumberText && !info.inPopover)
|
||||
? joinClassNames("fc-breezy-bCs fc-breezy-bvX fc-breezy-hS8", info.isNarrow ? "fc-breezy-TFV" : "fc-breezy-I1A")
|
||||
: joinClassNames("fc-breezy-Fvv", info.inPopover
|
||||
? "fc-breezy-bvX fc-breezy-aCI fc-breezy-2rx"
|
||||
: joinClassNames("fc-breezy-fn8 fc-breezy-TFV fc-breezy-ZrE", info.isNarrow ? "fc-breezy-2tF" : "fc-breezy-X6C"), info.hasNavLink && mutedHoverButtonClass)),
|
||||
dayHeaderContent: (info) => ((!info.dayNumberText && !info.inPopover) ? (u(S, { children: info.text })) : (u(S, { children: info.textParts.map((textPart, i) => (u("span", { className: joinClassNames("fc-breezy-jm6", info.isNarrow ? "fc-breezy-a3B" : "fc-breezy-9yp", textPart.type === "day"
|
||||
? joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", !info.isNarrow && "fc-breezy-C8a", (info.isToday && !info.inPopover)
|
||||
? joinClassNames("fc-breezy-cKZ fc-breezy-AAA fc-breezy-E9P", info.isNarrow ? "fc-breezy-MSG" : "fc-breezy-n6w", info.hasNavLink
|
||||
? `${primaryPressableGroupClass} ${outlineWidthGroupFocusClass} ${outlineOffsetClass} ${primaryOutlineColorClass}`
|
||||
: primaryClass)
|
||||
: "fc-breezy-Oiq")
|
||||
: "fc-breezy-t4l"), children: textPart.value }, i))) }))),
|
||||
/* Day Cell
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
dayCellClass: (info) => joinClassNames("fc-breezy-wsy", ((info.isOther || info.isDisabled) && !info.options.businessHours) && "fc-breezy-nYK"),
|
||||
dayCellTopClass: (info) => joinClassNames(info.isNarrow ? "fc-breezy-84e" : "fc-breezy-p7s", "fc-breezy-dl1 fc-breezy-1sP"),
|
||||
dayCellTopInnerClass: (info) => joinClassNames("fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-E9P fc-breezy-TZ4", info.isNarrow
|
||||
? `fc-breezy-SEP fc-breezy-oM6 ${xxsTextClass}`
|
||||
: "fc-breezy-V9v fc-breezy-TFV fc-breezy-a3B", info.isToday
|
||||
? joinClassNames("fc-breezy-AAA fc-breezy-C8a", info.isNarrow ? "fc-breezy-qvL" : "fc-breezy-Wga", info.text === info.dayNumberText
|
||||
? (info.isNarrow ? "fc-breezy-79F" : "fc-breezy-ilz")
|
||||
: (info.isNarrow ? "fc-breezy-aCI" : "fc-breezy-Nca"), info.hasNavLink
|
||||
? `${primaryPressableClass} ${outlineOffsetClass}`
|
||||
: primaryClass)
|
||||
: joinClassNames("fc-breezy-Skl", info.isNarrow ? "fc-breezy-aCI" : "fc-breezy-Nca", info.hasNavLink && mutedHoverPressableClass, info.isOther
|
||||
? "fc-breezy-mNS"
|
||||
: (info.monthText ? "fc-breezy-sI1" : "fc-breezy-t4l"), info.monthText && "fc-breezy-DIS")),
|
||||
dayCellInnerClass: (info) => joinClassNames(info.inPopover && "fc-breezy-3N5"),
|
||||
/* Popover
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
popoverClass: "fc-breezy-uhF fc-breezy-wsy fc-breezy-LDJ fc-breezy-hny fc-breezy-pKG fc-breezy-1kP fc-breezy-gMS fc-breezy-aNc fc-breezy-n5m",
|
||||
popoverCloseClass: `fc-breezy-bCs fc-breezy-1EY fc-breezy-SKv fc-breezy-aYN fc-breezy-KUX fc-breezy-Fvv ${mutedHoverButtonClass} fc-breezy-Z9U`,
|
||||
popoverCloseContent: () => x(`fc-breezy-XUJ ${mutedFgPressableGroupClass}`),
|
||||
/* Lane
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
dayLaneClass: (info) => joinClassNames("fc-breezy-wsy", info.isMajor ? "fc-breezy-OFc" : "fc-breezy-tTN", info.isDisabled && "fc-breezy-nYK"),
|
||||
dayLaneInnerClass: (info) => (info.isStack
|
||||
? "fc-breezy-gMS"
|
||||
: info.isNarrow ? "fc-breezy-148" : "fc-breezy-rVY"),
|
||||
slotLaneClass: (info) => joinClassNames("fc-breezy-wsy fc-breezy-tTN", info.isMinor && "fc-breezy-TN2"),
|
||||
/* List Day
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
listDaysClass: "fc-breezy-8T7 fc-breezy-g0K fc-breezy-6u7 fc-breezy-E7b fc-breezy-KRz",
|
||||
listDayClass: (info) => joinClassNames(!info.isLast && "fc-breezy-zi1 fc-breezy-tTN", "fc-breezy-dl1 fc-breezy-1sP fc-breezy-EF4 fc-breezy-tgZ"),
|
||||
listDayHeaderClass: "fc-breezy-SEP fc-breezy-yi0 fc-breezy-vVE fc-breezy-kMV fc-breezy-MKw fc-breezy-dl1 fc-breezy-sgX fc-breezy-EF4",
|
||||
listDayHeaderInnerClass: (info) => joinClassNames("fc-breezy-cJ3 fc-breezy-2rx fc-breezy-Nca fc-breezy-PtF fc-breezy-AAA fc-breezy-9yp", !info.level
|
||||
? joinClassNames(info.isToday
|
||||
? joinClassNames("fc-breezy-C8a", info.hasNavLink ? primaryPressableClass : primaryClass)
|
||||
: joinClassNames("fc-breezy-1OT fc-breezy-Oiq", info.hasNavLink && mutedHoverPressableClass))
|
||||
: joinClassNames("fc-breezy-mNS", info.hasNavLink && `${mutedHoverPressableClass} fc-breezy-i3P`)),
|
||||
listDayBodyClass: "fc-breezy-9Qs fc-breezy-1El fc-breezy-2KU fc-breezy-wsy fc-breezy-EAo fc-breezy-KzJ",
|
||||
/* Single Month (in Multi-Month)
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
singleMonthClass: (info) => joinClassNames(info.multiMonthColumns > 1 && "fc-breezy-jD5", (info.multiMonthColumns === 1 && !info.isLast) && "fc-breezy-zi1 fc-breezy-EAo"),
|
||||
singleMonthHeaderClass: (info) => joinClassNames(info.multiMonthColumns > 1
|
||||
? "fc-breezy-x96"
|
||||
: "fc-breezy-End fc-breezy-gmc fc-breezy-zi1 fc-breezy-EAo", "fc-breezy-XpK"),
|
||||
singleMonthHeaderInnerClass: (info) => joinClassNames("fc-breezy-Jhn fc-breezy-Nca fc-breezy-KzJ fc-breezy-9yp fc-breezy-Oiq fc-breezy-C8a", info.hasNavLink && mutedHoverPressableClass),
|
||||
/* Misc Table
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
tableHeaderClass: "fc-breezy-gmc",
|
||||
fillerClass: "fc-breezy-wsy fc-breezy-tTN",
|
||||
dayNarrowWidth: 100,
|
||||
dayHeaderRowClass: "fc-breezy-wsy fc-breezy-tTN",
|
||||
dayRowClass: "fc-breezy-wsy fc-breezy-EAo",
|
||||
slotHeaderRowClass: "fc-breezy-wsy fc-breezy-EAo",
|
||||
slotHeaderInnerClass: "fc-breezy-mNS fc-breezy-XHd",
|
||||
/* Misc Content
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
navLinkClass: `${outlineWidthFocusClass} ${primaryOutlineColorClass}`,
|
||||
inlineWeekNumberClass: (info) => joinClassNames("fc-breezy-1EY fc-breezy-n9G fc-breezy-iD1 fc-breezy-gmc fc-breezy-t4l fc-breezy-TZ4 fc-breezy-4In fc-breezy-zi1 fc-breezy-k8g fc-breezy-3J4 fc-breezy-Hhp", info.isNarrow
|
||||
? `fc-breezy-KUX ${xxsTextClass}`
|
||||
: "fc-breezy-XJa fc-breezy-a3B", info.hasNavLink
|
||||
? `${mutedHoverPressableClass} fc-breezy-07j`
|
||||
: mutedHoverClass),
|
||||
highlightClass: "fc-breezy-xAy",
|
||||
nonBusinessHoursClass: "fc-breezy-nYK",
|
||||
nowIndicatorLineClass: "fc-breezy-CH7 fc-breezy-qQW fc-breezy-D9l",
|
||||
nowIndicatorDotClass: "fc-breezy-aAW fc-breezy-Vpk fc-breezy-D9l fc-breezy-63n fc-breezy-AAA fc-breezy-GBJ fc-breezy-9Iz",
|
||||
/* Resource Day Header
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
resourceDayHeaderAlign: "center",
|
||||
resourceDayHeaderClass: "fc-breezy-wsy",
|
||||
resourceDayHeaderInnerClass: (info) => joinClassNames("fc-breezy-bvX fc-breezy-sI1 fc-breezy-C8a", info.isNarrow ? "fc-breezy-a3B" : "fc-breezy-9yp"),
|
||||
/* Resource Data Grid
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
resourceColumnHeaderClass: "fc-breezy-wsy fc-breezy-tTN fc-breezy-E9P",
|
||||
resourceColumnHeaderInnerClass: "fc-breezy-bvX fc-breezy-sI1 fc-breezy-9yp",
|
||||
resourceColumnResizerClass: "fc-breezy-1EY fc-breezy-AWB fc-breezy-4Tv fc-breezy-dnf",
|
||||
resourceGroupHeaderClass: "fc-breezy-wsy fc-breezy-EAo fc-breezy-pZQ",
|
||||
resourceGroupHeaderInnerClass: "fc-breezy-bvX fc-breezy-sI1 fc-breezy-9yp",
|
||||
resourceCellClass: "fc-breezy-wsy fc-breezy-tTN",
|
||||
resourceCellInnerClass: "fc-breezy-bvX fc-breezy-sI1 fc-breezy-9yp",
|
||||
resourceIndentClass: "fc-breezy-Wga fc-breezy-p9t fc-breezy-E9P",
|
||||
resourceExpanderClass: `fc-breezy-bCs fc-breezy-KUX fc-breezy-AAA ${mutedHoverPressableClass} ${outlineWidthFocusClass} ${primaryOutlineColorClass}`,
|
||||
resourceExpanderContent: (info) => chevronDown(joinClassNames(`fc-breezy-XUJ ${mutedFgPressableGroupClass}`, !info.isExpanded && "fc-breezy-KxI fc-breezy-ZW3")),
|
||||
resourceHeaderRowClass: "fc-breezy-wsy fc-breezy-EAo",
|
||||
resourceRowClass: "fc-breezy-wsy fc-breezy-EAo",
|
||||
resourceColumnDividerClass: "fc-breezy-USt fc-breezy-OFc",
|
||||
/* Timeline Lane
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
resourceGroupLaneClass: "fc-breezy-wsy fc-breezy-EAo fc-breezy-pZQ",
|
||||
resourceLaneClass: "fc-breezy-wsy fc-breezy-EAo",
|
||||
resourceLaneBottomClass: (info) => joinClassNames(info.options.eventOverlap && "fc-breezy-uuA"),
|
||||
timelineBottomClass: "fc-breezy-uuA",
|
||||
},
|
||||
views: {
|
||||
dayGrid: {
|
||||
...dayRowCommonClasses,
|
||||
dayHeaderClass: getNormalDayHeaderBorderClass,
|
||||
dayHeaderDividerClass: "fc-breezy-zi1 fc-breezy-OFc",
|
||||
dayCellClass: getNormalDayCellBorderColorClass,
|
||||
dayCellBottomClass: getShortDayCellBottomClass,
|
||||
backgroundEventInnerClass: "fc-breezy-dl1 fc-breezy-1sP fc-breezy-LMv",
|
||||
},
|
||||
multiMonth: {
|
||||
...dayRowCommonClasses,
|
||||
dayHeaderClass: getNormalDayHeaderBorderClass,
|
||||
dayHeaderDividerClass: (info) => joinClassNames(info.multiMonthColumns === 1 && "fc-breezy-zi1 fc-breezy-OFc fc-breezy-qNs"),
|
||||
dayCellClass: getNormalDayCellBorderColorClass,
|
||||
dayCellBottomClass: getShortDayCellBottomClass,
|
||||
tableBodyClass: (info) => joinClassNames(info.multiMonthColumns > 1 && "fc-breezy-wsy fc-breezy-EAo fc-breezy-KzJ fc-breezy-eSM fc-breezy-pKG"),
|
||||
},
|
||||
timeGrid: {
|
||||
...dayRowCommonClasses,
|
||||
dayHeaderClass: getMutedDayHeaderBorderClass,
|
||||
dayHeaderDividerClass: (info) => joinClassNames("fc-breezy-zi1", info.options.allDaySlot
|
||||
? "fc-breezy-EAo"
|
||||
: "fc-breezy-OFc fc-breezy-rf6"),
|
||||
dayCellClass: getMutedDayCellBorderColorClass,
|
||||
dayCellBottomClass: tallDayCellBottomClass,
|
||||
/* TimeGrid > Week Number Header
|
||||
------------------------------------------------------------------------------------------- */
|
||||
weekNumberHeaderClass: "fc-breezy-XpK fc-breezy-LMv",
|
||||
weekNumberHeaderInnerClass: (info) => joinClassNames("fc-breezy-cGD fc-breezy-TFV fc-breezy-ZrE fc-breezy-t4l fc-breezy-Fvv fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK", info.hasNavLink && mutedHoverPressableClass, info.isNarrow ? "fc-breezy-a3B" : "fc-breezy-9yp"),
|
||||
/* TimeGrid > All-Day Header
|
||||
------------------------------------------------------------------------------------------- */
|
||||
allDayHeaderClass: "fc-breezy-XpK",
|
||||
allDayHeaderInnerClass: (info) => joinClassNames("fc-breezy-rUb fc-breezy-mNS", info.isNarrow ? xxsTextClass : "fc-breezy-a3B"),
|
||||
allDayDividerClass: "fc-breezy-zi1 fc-breezy-OFc fc-breezy-rf6",
|
||||
/* TimeGrid > Slot Header
|
||||
------------------------------------------------------------------------------------------- */
|
||||
slotHeaderClass: "fc-breezy-LMv",
|
||||
slotHeaderInnerClass: (info) => joinClassNames("fc-breezy-eYX fc-breezy-GFf fc-breezy-2tF", info.isNarrow
|
||||
? `fc-breezy-Cy2 ${xxsTextClass}`
|
||||
: "fc-breezy-uqG fc-breezy-a3B", info.isFirst && "fc-breezy-pps"),
|
||||
slotHeaderDividerClass: "fc-breezy-USt fc-breezy-tTN",
|
||||
},
|
||||
list: {
|
||||
/* List-View > List-Item Event
|
||||
------------------------------------------------------------------------------------------- */
|
||||
listItemEventClass: (info) => joinClassNames("fc-breezy-bCs fc-breezy-lqx fc-breezy-XpK fc-breezy-wwb", !info.isLast && "fc-breezy-zi1 fc-breezy-tTN", info.isInteractive
|
||||
? faintHoverPressableClass
|
||||
: faintHoverClass),
|
||||
listItemEventBeforeClass: "fc-breezy-5JF fc-breezy-lNM fc-breezy-AAA",
|
||||
listItemEventInnerClass: "fc-breezy-dl1 fc-breezy-1sP fc-breezy-XpK fc-breezy-wwb fc-breezy-9yp",
|
||||
listItemEventTimeClass: "fc-breezy-yi0 fc-breezy-roZ fc-breezy-kMV fc-breezy-TZ4 fc-breezy-pKG fc-breezy-IPx fc-breezy-t4l",
|
||||
listItemEventTitleClass: (info) => joinClassNames("fc-breezy-1El fc-breezy-2KU fc-breezy-1OT fc-breezy-TZ4 fc-breezy-pKG fc-breezy-sI1", info.event.url && "fc-breezy-Ogp"),
|
||||
/* No-Events Screen
|
||||
------------------------------------------------------------------------------------------- */
|
||||
noEventsClass: "fc-breezy-1El fc-breezy-dl1 fc-breezy-sgX fc-breezy-XpK fc-breezy-E9P",
|
||||
noEventsInnerClass: "fc-breezy-P9h fc-breezy-t4l",
|
||||
},
|
||||
resourceDayGrid: {
|
||||
resourceDayHeaderClass: (info) => (info.isMajor
|
||||
? "fc-breezy-OFc"
|
||||
: "fc-breezy-EAo"),
|
||||
},
|
||||
resourceTimeGrid: {
|
||||
resourceDayHeaderClass: (info) => (info.isMajor
|
||||
? "fc-breezy-OFc"
|
||||
: "fc-breezy-tTN"),
|
||||
},
|
||||
timeline: {
|
||||
/* Timeline > Row Event
|
||||
------------------------------------------------------------------------------------------- */
|
||||
rowEventClass: (info) => info.isEnd && "fc-breezy-9hC",
|
||||
rowEventInnerClass: (info) => info.options.eventOverlap ? "fc-breezy-Jhn" : "fc-breezy-dl6",
|
||||
/* Timeline > More-Link
|
||||
------------------------------------------------------------------------------------------- */
|
||||
rowMoreLinkClass: `fc-breezy-9hC fc-breezy-Ika fc-breezy-wsy fc-breezy-d0j fc-breezy-4MR fc-breezy-KzJ ${strongSolidPressableClass} fc-breezy-vwH`,
|
||||
rowMoreLinkInnerClass: "fc-breezy-iS4 fc-breezy-sI1 fc-breezy-a3B",
|
||||
/* Timeline > Slot Header
|
||||
------------------------------------------------------------------------------------------- */
|
||||
slotHeaderAlign: (info) => info.isTime ? "start" : "center",
|
||||
slotHeaderClass: (info) => joinClassNames(info.level > 0 && "fc-breezy-wsy fc-breezy-tTN", "fc-breezy-LMv"),
|
||||
slotHeaderInnerClass: (info) => joinClassNames("fc-breezy-GFf fc-breezy-2tF fc-breezy-a3B", info.isTime && joinClassNames("fc-breezy-eYX fc-breezy-OAt", info.isFirst && "fc-breezy-pps"), info.hasNavLink && "fc-breezy-Eu0"),
|
||||
slotHeaderDividerClass: "fc-breezy-zi1 fc-breezy-OFc fc-breezy-qNs",
|
||||
},
|
||||
},
|
||||
};
|
||||
/* SVGs
|
||||
------------------------------------------------------------------------------------------------- */
|
||||
function chevronDown(className) {
|
||||
return u("svg", { className: className, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: u("path", { fillRule: "evenodd", d: "M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z", clipRule: "evenodd" }) });
|
||||
}
|
||||
function chevronDoubleLeft(className) {
|
||||
return u("svg", { className: className, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: u("path", { fillRule: "evenodd", d: "M4.72 9.47a.75.75 0 0 0 0 1.06l4.25 4.25a.75.75 0 1 0 1.06-1.06L6.31 10l3.72-3.72a.75.75 0 1 0-1.06-1.06L4.72 9.47Zm9.25-4.25L9.72 9.47a.75.75 0 0 0 0 1.06l4.25 4.25a.75.75 0 1 0 1.06-1.06L11.31 10l3.72-3.72a.75.75 0 0 0-1.06-1.06Z", clipRule: "evenodd" }) });
|
||||
}
|
||||
function x(className) {
|
||||
return u("svg", { className: className, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: u("path", { d: "M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" }) });
|
||||
}
|
||||
|
||||
globalPlugins.push(index);
|
||||
|
||||
})(FullCalendar.Shared);
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
|
||||
:root {
|
||||
/* primary */
|
||||
--fc-breezy-primary: #AD1F1F;
|
||||
--fc-breezy-primary-over: #AD1F1F;
|
||||
--fc-breezy-primary-foreground: #ffffff;
|
||||
|
||||
/* secondary */
|
||||
--fc-breezy-secondary: var(--fc-breezy-background);
|
||||
--fc-breezy-secondary-over: var(--fc-breezy-faint);
|
||||
--fc-breezy-secondary-border: var(--fc-breezy-strong-border);
|
||||
--fc-breezy-secondary-foreground: var(--fc-breezy-strong-foreground);
|
||||
--fc-breezy-secondary-icon: var(--fc-breezy-faint-foreground);
|
||||
--fc-breezy-secondary-icon-over: var(--fc-breezy-muted-foreground);
|
||||
|
||||
/* calendar content */
|
||||
--fc-breezy-event: #1FAD95;
|
||||
--fc-breezy-event-contrast: #fff;
|
||||
--fc-breezy-background-event: #1F95AD;
|
||||
--fc-breezy-highlight: #ef444414;
|
||||
--fc-breezy-now: #ef4444;
|
||||
|
||||
/* controls */
|
||||
--fc-breezy-selected: var(--fc-breezy-strong);
|
||||
|
||||
/* popover */
|
||||
--fc-breezy-popover: var(--fc-breezy-background);
|
||||
--fc-breezy-popover-border: var(--fc-breezy-strong-border);
|
||||
|
||||
/* neutral backgrounds */
|
||||
--fc-breezy-background: #ffffff;
|
||||
--fc-breezy-faint: #00000005;
|
||||
--fc-breezy-muted: #0000000D;
|
||||
--fc-breezy-strong: #0000001A;
|
||||
--fc-breezy-stronger: #00000024;
|
||||
--fc-breezy-strongest: #0000002E;
|
||||
|
||||
/* neutral foregrounds */
|
||||
--fc-breezy-foreground: #374151;
|
||||
--fc-breezy-faint-foreground: #9ca3af;
|
||||
--fc-breezy-muted-foreground: #6b7280;
|
||||
--fc-breezy-strong-foreground: #111827;
|
||||
|
||||
/* neutral borders */
|
||||
--fc-breezy-border: #e5e7eb;
|
||||
--fc-breezy-muted-border: #f3f4f6;
|
||||
--fc-breezy-strong-border: #d1d5db;
|
||||
}
|
||||
|
||||
@media not print {
|
||||
:root.skin-theme-clientpref-night {
|
||||
/* secondary */
|
||||
--fc-breezy-secondary: var(--fc-breezy-muted);
|
||||
--fc-breezy-secondary-over: var(--fc-breezy-strong);
|
||||
--fc-breezy-secondary-border: var(--fc-breezy-muted-border);
|
||||
--fc-breezy-secondary-foreground: var(--fc-breezy-strong-foreground);
|
||||
--fc-breezy-secondary-icon: var(--fc-breezy-muted-foreground);
|
||||
--fc-breezy-secondary-icon-over: var(--fc-breezy-foreground);
|
||||
|
||||
/* calendar content */
|
||||
--fc-breezy-highlight: #f8717114;
|
||||
|
||||
/* controls */
|
||||
--fc-breezy-selected: var(--fc-breezy-muted);
|
||||
|
||||
/* popovers */
|
||||
--fc-breezy-popover: #1f2937;
|
||||
--fc-breezy-popover-border: var(--fc-breezy-border);
|
||||
|
||||
/* neutral backgrounds */
|
||||
--fc-breezy-background: #111827;
|
||||
--fc-breezy-faint: #ffffff08;
|
||||
--fc-breezy-muted: #ffffff12;
|
||||
--fc-breezy-strong: #ffffff1F;
|
||||
--fc-breezy-stronger: #ffffff29;
|
||||
--fc-breezy-strongest: #ffffff33;
|
||||
|
||||
/* neutral foregrounds */
|
||||
--fc-breezy-foreground: #d1d5db;
|
||||
--fc-breezy-muted-foreground: #9ca3af;
|
||||
--fc-breezy-faint-foreground: #6b7280;
|
||||
--fc-breezy-strong-foreground: #ffffff;
|
||||
|
||||
/* neutral borders */
|
||||
--fc-breezy-border: #ffffff1A;
|
||||
--fc-breezy-muted-border: #ffffff0D;
|
||||
--fc-breezy-strong-border: #ffffff26;
|
||||
}
|
||||
}
|
||||
|
||||
@media not print and (prefers-color-scheme:dark){
|
||||
/* Copie du thème night */
|
||||
:root.skin-theme-clientpref-os {
|
||||
/* secondary */
|
||||
--fc-breezy-secondary: var(--fc-breezy-muted);
|
||||
--fc-breezy-secondary-over: var(--fc-breezy-strong);
|
||||
--fc-breezy-secondary-border: var(--fc-breezy-muted-border);
|
||||
--fc-breezy-secondary-foreground: var(--fc-breezy-strong-foreground);
|
||||
--fc-breezy-secondary-icon: var(--fc-breezy-muted-foreground);
|
||||
--fc-breezy-secondary-icon-over: var(--fc-breezy-foreground);
|
||||
|
||||
/* calendar content */
|
||||
--fc-breezy-highlight: #f8717114;
|
||||
|
||||
/* controls */
|
||||
--fc-breezy-selected: var(--fc-breezy-muted);
|
||||
|
||||
/* popovers */
|
||||
--fc-breezy-popover: #1f2937;
|
||||
--fc-breezy-popover-border: var(--fc-breezy-border);
|
||||
|
||||
/* neutral backgrounds */
|
||||
--fc-breezy-background: #111827;
|
||||
--fc-breezy-faint: #ffffff08;
|
||||
--fc-breezy-muted: #ffffff12;
|
||||
--fc-breezy-strong: #ffffff1F;
|
||||
--fc-breezy-stronger: #ffffff29;
|
||||
--fc-breezy-strongest: #ffffff33;
|
||||
|
||||
/* neutral foregrounds */
|
||||
--fc-breezy-foreground: #d1d5db;
|
||||
--fc-breezy-muted-foreground: #9ca3af;
|
||||
--fc-breezy-faint-foreground: #6b7280;
|
||||
--fc-breezy-strong-foreground: #ffffff;
|
||||
|
||||
/* neutral borders */
|
||||
--fc-breezy-border: #ffffff1A;
|
||||
--fc-breezy-muted-border: #ffffff0D;
|
||||
--fc-breezy-strong-border: #ffffff26;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,30 @@
|
|||
console.log("Youpi Calendar !");
|
||||
|
||||
mw.hook('wikipage.content').add(function() {
|
||||
console.log("Content loaded ?");
|
||||
console.log(FullCalendar);
|
||||
var elems = document.getElementsByClassName('mw-crans-fullcalendar');
|
||||
for (let el of elems) {
|
||||
try {
|
||||
const url = new URL(el.dataset.ics);
|
||||
let calendar = new FullCalendar.Calendar(el, {
|
||||
headerToolbar: {
|
||||
start: 'prev,today,next',
|
||||
center: 'title',
|
||||
end: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek,multiMonthYear',
|
||||
},
|
||||
locale: 'fr',
|
||||
events: {
|
||||
url: url.href,
|
||||
format: 'ics'
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
console.log(elems);
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
diff --git a/sql/postgres/table_wsoauth_multiauth_mappings.sql b/sql/postgres/table_wsoauth_multiauth_mappings.sql
|
||||
index d2917db..b903ba7 100644
|
||||
--- a/sql/postgres/table_wsoauth_multiauth_mappings.sql
|
||||
+++ b/sql/postgres/table_wsoauth_multiauth_mappings.sql
|
||||
@@ -1,5 +1,5 @@
|
||||
CREATE TABLE /*_*/wsoauth_multiauth_mappings (
|
||||
- wsoauth_user int unsigned NOT NULL,
|
||||
+ wsoauth_user int NOT NULL,
|
||||
wsoauth_remote_name varchar(512) NOT NULL,
|
||||
wsoauth_provider_id varchar(255) NOT NULL,
|
||||
PRIMARY KEY (wsoauth_remote_name, wsoauth_provider_id)
|
||||
diff --git a/src/WSOAuth.php b/src/WSOAuth.php
|
||||
index 3a94c87..e077b9e 100644
|
||||
--- a/src/WSOAuth.php
|
||||
+++ b/src/WSOAuth.php
|
||||
@@ -308,11 +308,12 @@ class WSOAuth extends PluggableAuth {
|
||||
// Set $realname and $email to the values returned from the authentication provider, if they are available
|
||||
$realname = $remoteUserInfo['realname'] ?? null;
|
||||
$email = $remoteUserInfo['email'] ?? null;
|
||||
+ $username = ucfirst( $remoteUserInfo['name'] );
|
||||
|
||||
- $remoteUsername = ucfirst( $remoteUserInfo['name'] );
|
||||
- $localUserId = $this->getLocalAccountID( $remoteUsername );
|
||||
+ $remoteUserId = $remoteUserInfo['remoteUserId'] ?? $username;
|
||||
+ $localUserId = $this->getLocalAccountID( $remoteUserId );
|
||||
|
||||
- $this->session->set( self::WSOAUTH_REMOTE_USERNAME_SESSION_KEY, $remoteUsername );
|
||||
+ $this->session->set( self::WSOAUTH_REMOTE_USERNAME_SESSION_KEY, $remoteUserId );
|
||||
$this->session->save();
|
||||
|
||||
if ( $localUserId !== 0 ) {
|
||||
@@ -326,7 +327,7 @@ class WSOAuth extends PluggableAuth {
|
||||
$currentUser = RequestContext::getMain()->getUser();
|
||||
$currentUserId = $currentUser->getId();
|
||||
|
||||
- $this->createMapping( $currentUserId, $remoteUsername );
|
||||
+ $this->createMapping( $currentUserId, $remoteUserId );
|
||||
|
||||
// Log the account in like normal
|
||||
$username = $currentUser->getName();
|
||||
@@ -339,7 +340,7 @@ class WSOAuth extends PluggableAuth {
|
||||
throw new ContinuationException( wfMessage( "wsoauth-remote-only-accounts-disabled" )->parse() );
|
||||
}
|
||||
|
||||
- $desiredLocalUsername = $this->useRealNameAsUsername && $realname !== null ? $realname : $remoteUsername;
|
||||
+ $desiredLocalUsername = $this->useRealNameAsUsername && $realname !== null ? $realname : $username;
|
||||
$userId = User::newFromName( $desiredLocalUsername )->idForName();
|
||||
|
||||
if ( $userId > 0 && $this->migrateUsersByUsername ) {
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# NoteKfetAuth
|
||||
|
||||
Extension pour médiawiki pour ajouter le support de l'autentification par Note
|
||||
via l'extension WSOAuth.
|
||||
|
||||
## Installation
|
||||
|
||||
Il faut enregistrer l'extension comme provider
|
||||
|
||||
```
|
||||
$wgOAuthCustomAuthProviders = [
|
||||
'note' => WSOAuth\AuthenticationProvider\NoteKfetAuth::class
|
||||
];
|
||||
```
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "WSONoteKfetAuth",
|
||||
"author": [
|
||||
"Pyjacpp"
|
||||
],
|
||||
"url": "https://gitlab.crans.org/nounous/nixos/-/tree/main/hosts/vm/mediakiwi/WSONoteKfetAuth",
|
||||
"description": "Implementation of the NoteKfet OAuth2 for the WSOAuth extension",
|
||||
"type": "other",
|
||||
"requires": {
|
||||
"MediaWiki": ">= 1.35.0",
|
||||
"extensions": {
|
||||
"WSOAuth": ">= 9.0"
|
||||
}
|
||||
},
|
||||
"AutoloadNamespaces": {
|
||||
"WSOAuth\\AuthenticationProvider\\": "src/"
|
||||
},
|
||||
"config": {
|
||||
"NoteKfetUrl": {
|
||||
"description": "The url of the NoteKfet.",
|
||||
"value": "https://note.crans.org/"
|
||||
}
|
||||
},
|
||||
"manifest_version": 2
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2020 Marijn van Wezel
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
||||
* Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace WSOAuth\AuthenticationProvider;
|
||||
|
||||
use MediaWiki\User\UserIdentity;
|
||||
|
||||
class NoteKfetAuth extends AuthProvider {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $clientId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $clientSecret;
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct(
|
||||
string $clientId,
|
||||
string $clientSecret,
|
||||
?string $authUri,
|
||||
?string $redirectUri,
|
||||
array $extensionData = []
|
||||
) {
|
||||
$this->clientId = $clientId;
|
||||
$this->clientSecret = $clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function login( ?string &$key, ?string &$secret, ?string &$authUrl ): bool {
|
||||
|
||||
// This state is used to prevent CSRF, i.e., ensuring that authentification request
|
||||
// were initiated on our website.
|
||||
$state = random_int(PHP_INT_MIN, PHP_INT_MAX);
|
||||
$secret = "$state";
|
||||
$authUrl = $GLOBALS['wgNoteKfetUrl'] . "o/authorize/?" . http_build_query([
|
||||
'client_id' => $this->clientId,
|
||||
'response_type' => 'code',
|
||||
'scope' => '1_1',
|
||||
'state' => $secret,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function logout( UserIdentity &$user ): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getUser( string $key, string $secret, &$errorMessage ) {
|
||||
if ( !isset( $_GET['code'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !isset( $_GET['state'] ) || empty( $_GET['state'] ) || ( $_GET['state'] !== $secret ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$token = $this->getAccessTokens( $_GET['code'] );
|
||||
$userInfos = $this->getUserInfos( $token );
|
||||
|
||||
return [
|
||||
'name' => $this->sanitizeName( "$userInfos->normalized_name (note)" ),
|
||||
'realname' => $userInfos->username,
|
||||
'email' => $userInfos->email,
|
||||
'remoteUserId' => $userInfos->id,
|
||||
];
|
||||
} catch ( \Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function sanitizeName( string $name ) {
|
||||
// We replace forbidden chars.
|
||||
$res = preg_replace('/[#\/:<>=@\|]/', '-', $name);
|
||||
$res = preg_replace(['/[\[{]/', '/[\]}]/'], ['(', ')'], $res);
|
||||
$res = str_replace('_', ' ', $res);
|
||||
// We remove the last controls chars possibly remaining.
|
||||
return preg_replace('/[^a-zA-Z0-9 !\"$%&\'()*+,\-.;?\\\^`~]/', '', $res);
|
||||
}
|
||||
|
||||
private function getAccessTokens( string $code ) {
|
||||
$data = [
|
||||
'grant_type' => 'authorization_code',
|
||||
'client_id' => $this->clientId,
|
||||
'client_secret' => $this->clientSecret,
|
||||
'code' => $code,
|
||||
];
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-type: application/x-www-form-urlencoded',
|
||||
'content' => http_build_query($data),
|
||||
],
|
||||
];
|
||||
|
||||
$context = stream_context_create($options);
|
||||
$response = file_get_contents($GLOBALS['wgNoteKfetUrl'] . 'o/token/', false, $context);
|
||||
$tokens = json_decode($response);
|
||||
return $tokens->access_token;
|
||||
}
|
||||
|
||||
private function getUserInfos( string $token ) {
|
||||
$options = [
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => "Authorization: Bearer $token",
|
||||
],
|
||||
];
|
||||
|
||||
$context = stream_context_create($options);
|
||||
$response = file_get_contents($GLOBALS['wgNoteKfetUrl'] . 'api/me/', false, $context);
|
||||
return json_decode($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function saveExtraAttributes( int $id ): void {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
diff --git a/CategoryLockdown.php b/CategoryLockdown.php
|
||||
index 1e17ec7..b884553 100644
|
||||
--- a/CategoryLockdown.php
|
||||
+++ b/CategoryLockdown.php
|
||||
@@ -16,16 +16,23 @@ class CategoryLockdown {
|
||||
*/
|
||||
public static function onGetUserPermissionsErrors( $title, $user, $action, &$result ) {
|
||||
global $wgCategoryLockdown;
|
||||
+ global $wgCategoryGroupLockdown;
|
||||
+ global $wgCategoryLockdownWhitelist;
|
||||
|
||||
$explicitGroups = MediaWikiServices::getInstance()->getUserGroupManager()->getUserGroups( $user );
|
||||
$implicitGroups = MediaWikiServices::getInstance()->getUserGroupManager()->getUserImplicitGroups( $user );
|
||||
- $userGroups = $explicitGroups + $implicitGroups;
|
||||
+ $userGroups = array_merge( $explicitGroups, $implicitGroups );
|
||||
|
||||
// Rules don't apply to admins
|
||||
if ( in_array( 'sysop', $userGroups ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
+ // Rules doesn’t apply to the whitelist
|
||||
+ if ( in_array( $title, $wgCategoryLockdownWhitelist ) ) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
$categories = array_keys( $title->getParentCategories() );
|
||||
|
||||
// Apply rules to the category page itself
|
||||
@@ -33,16 +40,11 @@ class CategoryLockdown {
|
||||
$categories[] = $title->getFullText();
|
||||
}
|
||||
|
||||
+ // Support "Category:Top_secret", "Category:Top secret", "Top_secret" and "Top secret"
|
||||
+ $categories = array_map( fn($c) => str_replace( '_', ' ', substr( $c, strpos( $c, ':' ) + 1 ) ), $categories );
|
||||
$combinedGroups = [];
|
||||
foreach ( $categories as $category ) {
|
||||
- // Support "Category:Top_secret", "Category:Top secret", "Top_secret" and "Top secret"
|
||||
- $category = substr( $category, strpos( $category, ':' ) + 1 );
|
||||
- $category = str_replace( '_', ' ', $category );
|
||||
$permissions = $wgCategoryLockdown[ $category ] ?? null;
|
||||
- if ( !$permissions ) {
|
||||
- $category = str_replace( ' ', '_', $category );
|
||||
- $permissions = $wgCategoryLockdown[ $category ] ?? null;
|
||||
- }
|
||||
if ( !$permissions ) {
|
||||
continue;
|
||||
}
|
||||
@@ -57,15 +59,44 @@ class CategoryLockdown {
|
||||
$combinedGroups[] = $allowedGroup;
|
||||
}
|
||||
}
|
||||
- if ( $combinedGroups ) {
|
||||
- foreach ( $userGroups as $userGroup ) {
|
||||
- if ( in_array( $userGroup, $combinedGroups ) ) {
|
||||
- return;
|
||||
- }
|
||||
+
|
||||
+ $allow = false;
|
||||
+ foreach ( $userGroups as $userGroup ) {
|
||||
+ if ( in_array( $userGroup, $combinedGroups ) ) {
|
||||
+ $allow = true;
|
||||
+ break;
|
||||
}
|
||||
+ }
|
||||
+ if ( $combinedGroups && !$allow ) {
|
||||
$result = [ 'categorylockdown-error', implode( ', ', $combinedGroups ) ];
|
||||
return false;
|
||||
}
|
||||
+
|
||||
+ $allow = true;
|
||||
+ foreach ( $wgCategoryGroupLockdown as $group => $groupCategories ) {
|
||||
+ if ( str_starts_with( $group, "!") ?
|
||||
+ in_array( substr($group, 1), $userGroups ) :
|
||||
+ !in_array( $group, $userGroups ) ) {
|
||||
+ continue; # Skip if this group rule doesn’t match the user
|
||||
+ }
|
||||
+
|
||||
+ $requiredCat = $groupCategories[ $action ] ?? null;
|
||||
+ if ( $requiredCat == null ) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ $groupLocked = true;
|
||||
+ foreach ( $requiredCat as $c ) {
|
||||
+ if ( in_array( $c, $categories ) ) {
|
||||
+ $groupLocked = false;
|
||||
+ break; # One of the category is present, we can grant this action.
|
||||
+ }
|
||||
+ }
|
||||
+ if ( $groupLocked ) {
|
||||
+ $result = [ 'categorylockdown-error', implode( ', ', $requiredCat ) ];
|
||||
+ return false; # This group of users need a category from groupCategories on this page to perform $action
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./hardware-configuration.nix
|
||||
./mediawiki.nix
|
||||
|
||||
../../../modules
|
||||
../../../modules/crans/nullmailer.nix
|
||||
];
|
||||
|
||||
networking.hostName = "mediakiwi";
|
||||
boot.loader.grub.devices = [ "/dev/sda" ];
|
||||
|
||||
crans = {
|
||||
enable = true;
|
||||
|
||||
networking = {
|
||||
id = 144;
|
||||
srvNat.enable = true;
|
||||
};
|
||||
|
||||
# Enable when deploying the real mediakiwi
|
||||
resticClient.when = "06:47";
|
||||
};
|
||||
|
||||
system.stateVersion = "25.05";
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Do not modify this file! It was generated by ‘nixos-generate-config’
|
||||
# and may be overwritten by future invocations. Please make changes
|
||||
# to /etc/nixos/configuration.nix instead.
|
||||
{ config, lib, pkgs, modulesPath, ... }:
|
||||
|
||||
{
|
||||
imports =
|
||||
[ (modulesPath + "/profiles/qemu-guest.nix")
|
||||
];
|
||||
|
||||
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
|
||||
boot.initrd.kernelModules = [ ];
|
||||
boot.kernelModules = [ ];
|
||||
boot.extraModulePackages = [ ];
|
||||
|
||||
fileSystems."/" =
|
||||
{ device = "/dev/disk/by-uuid/45e1f37b-5bf2-47fc-86ef-e79b062c2b3c";
|
||||
fsType = "ext4";
|
||||
};
|
||||
|
||||
swapDevices = [ ];
|
||||
|
||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||
}
|
||||
|
|
@ -0,0 +1,852 @@
|
|||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
|
||||
inherit (lib)
|
||||
mkDefault
|
||||
mkEnableOption
|
||||
mkPackageOption
|
||||
mkForce
|
||||
mkIf
|
||||
mkMerge
|
||||
mkOption
|
||||
;
|
||||
inherit (lib)
|
||||
concatStringsSep
|
||||
literalExpression
|
||||
mapAttrsToList
|
||||
optional
|
||||
optionals
|
||||
optionalString
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.mediawiki;
|
||||
fpm = config.services.phpfpm.pools.mediawiki;
|
||||
user = "mediawiki";
|
||||
group =
|
||||
if cfg.webserver == "apache" then
|
||||
config.services.httpd.group
|
||||
else if cfg.webserver == "nginx" then
|
||||
config.services.nginx.group
|
||||
else
|
||||
"mediawiki";
|
||||
|
||||
cacheDir = "/var/cache/mediawiki";
|
||||
stateDir = "/var/lib/mediawiki";
|
||||
|
||||
toolsPath = pkgs.symlinkJoin {
|
||||
name = "mediawiki-path";
|
||||
paths = cfg.path;
|
||||
};
|
||||
|
||||
pkg = pkgs.stdenv.mkDerivation rec {
|
||||
pname = "mediawiki-full";
|
||||
inherit (src) version;
|
||||
src = cfg.package;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r * $out/
|
||||
|
||||
substituteInPlace $out/share/mediawiki/includes/config-schema.php \
|
||||
--replace-fail "/usr/bin/" "${toolsPath}/bin/" \
|
||||
--replace-fail "\$path/" "${toolsPath}/bin/"
|
||||
|
||||
# try removing directories before symlinking to allow overwriting any builtin extension or skin
|
||||
${concatStringsSep "\n" (
|
||||
mapAttrsToList (k: v: ''
|
||||
rm -rf $out/share/mediawiki/skins/${k}
|
||||
ln -s ${v} $out/share/mediawiki/skins/${k}
|
||||
'') cfg.skins
|
||||
)}
|
||||
|
||||
${concatStringsSep "\n" (
|
||||
mapAttrsToList (k: v: ''
|
||||
rm -rf $out/share/mediawiki/extensions/${k}
|
||||
ln -s ${
|
||||
if v != null then v else "$src/share/mediawiki/extensions/${k}"
|
||||
} $out/share/mediawiki/extensions/${k}
|
||||
'') cfg.extensions
|
||||
)}
|
||||
'';
|
||||
};
|
||||
|
||||
mediawikiScripts =
|
||||
pkgs.runCommand "mediawiki-scripts"
|
||||
{
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
preferLocalBuild = true;
|
||||
}
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
makeWrapper ${cfg.phpPackage}/bin/php $out/bin/mediawiki-maintenance \
|
||||
--set MEDIAWIKI_CONFIG ${mediawikiConfig} \
|
||||
--add-flags ${pkg}/share/mediawiki/maintenance/run.php
|
||||
|
||||
for i in changePassword createAndPromote deleteUserEmail renameUser resetUserEmail userOptions edit nukePage update importDump run; do
|
||||
script="$out/bin/mediawiki-$i"
|
||||
cat <<'EOF' >"$script"
|
||||
#!${pkgs.runtimeShell}
|
||||
become=(exec)
|
||||
if [[ "$(id -u)" != ${user} ]]; then
|
||||
become=(exec /run/wrappers/bin/sudo -u ${user} --)
|
||||
fi
|
||||
"${"$"}{become[@]}" ${placeholder "out"}/bin/mediawiki-maintenance \
|
||||
EOF
|
||||
if [[ "$i" != "run" ]]; then
|
||||
echo " ${pkg}/share/mediawiki/maintenance/$i.php \"\$@\"" >>"$script"
|
||||
else
|
||||
echo " ${pkg}/share/mediawiki/maintenance/\$1.php \"\''${@:2}\"" >>"$script"
|
||||
fi
|
||||
chmod +x "$script"
|
||||
done
|
||||
'';
|
||||
|
||||
dbAddr =
|
||||
if cfg.database.type == "postgres" then
|
||||
"${if cfg.database.socket == null then cfg.database.host else cfg.database.socket}"
|
||||
else if cfg.database.socket == null then
|
||||
"${cfg.database.host}:${toString cfg.database.port}"
|
||||
else if cfg.database.type == "mysql" then
|
||||
"${cfg.database.host}:${cfg.database.socket}"
|
||||
else
|
||||
throw "Unsupported database type: ${cfg.database.type} for socket: ${cfg.database.socket}";
|
||||
|
||||
mediawikiConfig = pkgs.writeTextFile {
|
||||
name = "LocalSettings.php";
|
||||
checkPhase = ''
|
||||
${cfg.phpPackage}/bin/php --syntax-check "$target"
|
||||
'';
|
||||
text =
|
||||
let
|
||||
dbSettings =
|
||||
if cfg.database.type == "sqlite" then
|
||||
''
|
||||
$wgSQLiteDataDir = "${cfg.database.path}";
|
||||
''
|
||||
else
|
||||
''
|
||||
$wgDBserver = "${dbAddr}";
|
||||
$wgDBport = "${toString cfg.database.port}";
|
||||
$wgDBuser = "${cfg.database.user}";
|
||||
${optionalString (
|
||||
cfg.database.passwordFile != null
|
||||
) "$wgDBpassword = file_get_contents(\"${cfg.database.passwordFile}\");"}
|
||||
|
||||
${optionalString (cfg.database.type == "mysql" && cfg.database.tablePrefix != null) ''
|
||||
# MySQL specific settings
|
||||
$wgDBprefix = "${cfg.database.tablePrefix}";
|
||||
''}
|
||||
|
||||
${optionalString (cfg.database.type == "mysql") ''
|
||||
# MySQL table options to use during installation or update
|
||||
$wgDBTableOptions = "ENGINE=InnoDB, DEFAULT CHARSET=binary";
|
||||
''}
|
||||
'';
|
||||
in
|
||||
''
|
||||
<?php
|
||||
# Protect against web entry
|
||||
if ( !defined( 'MEDIAWIKI' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$wgSitename = "${cfg.name}";
|
||||
$wgMetaNamespace = false;
|
||||
|
||||
## The URL base path to the directory containing the wiki;
|
||||
## defaults for all runtime URL paths are based off of this.
|
||||
## For more information on customizing the URLs
|
||||
## (like /w/index.php/Page_title to /wiki/Page_title) please see:
|
||||
## https://www.mediawiki.org/wiki/Manual:Short_URL
|
||||
$wgScriptPath = "${lib.optionalString (cfg.webserver == "nginx") "/w"}";
|
||||
|
||||
## The protocol and server name to use in fully-qualified URLs
|
||||
$wgServer = "${cfg.url}";
|
||||
|
||||
## The URL path to static resources (images, scripts, etc.)
|
||||
$wgResourceBasePath = $wgScriptPath;
|
||||
|
||||
${lib.optionalString (cfg.webserver == "nginx") ''
|
||||
$wgArticlePath = "/wiki/$1";
|
||||
$wgUsePathInfo = true;
|
||||
''}
|
||||
|
||||
## The URL path to the logo. Make sure you change this from the default,
|
||||
## or else you'll overwrite your logo when you upgrade!
|
||||
$wgLogo = "$wgResourceBasePath/resources/assets/wiki.png";
|
||||
|
||||
## UPO means: this is also a user preference option
|
||||
|
||||
$wgEnableEmail = true;
|
||||
$wgEnableUserEmail = true; # UPO
|
||||
|
||||
$wgPasswordSender = "${cfg.passwordSender}";
|
||||
|
||||
$wgEnotifUserTalk = false; # UPO
|
||||
$wgEnotifWatchlist = false; # UPO
|
||||
$wgEmailAuthentication = true;
|
||||
|
||||
## Database settings
|
||||
$wgDBtype = "${cfg.database.type}";
|
||||
$wgDBname = "${cfg.database.name}";
|
||||
${dbSettings}
|
||||
|
||||
## Shared memory settings
|
||||
$wgMainCacheType = CACHE_NONE;
|
||||
$wgMemCachedServers = [];
|
||||
|
||||
${optionalString (cfg.uploadsDir != null) ''
|
||||
$wgEnableUploads = true;
|
||||
$wgUploadDirectory = "${cfg.uploadsDir}";
|
||||
''}
|
||||
|
||||
$wgUseImageMagick = true;
|
||||
|
||||
# InstantCommons allows wiki to use images from https://commons.wikimedia.org
|
||||
$wgUseInstantCommons = false;
|
||||
|
||||
# Periodically send a pingback to https://www.mediawiki.org/ with basic data
|
||||
# about this MediaWiki instance. The Wikimedia Foundation shares this data
|
||||
# with MediaWiki developers to help guide future development efforts.
|
||||
$wgPingback = true;
|
||||
|
||||
## If you use ImageMagick (or any other shell command) on a
|
||||
## Linux server, this will need to be set to the name of an
|
||||
## available UTF-8 locale
|
||||
$wgShellLocale = "C.UTF-8";
|
||||
|
||||
## Set $wgCacheDirectory to a writable directory on the web server
|
||||
## to make your wiki go slightly faster. The directory should not
|
||||
## be publicly accessible from the web.
|
||||
$wgCacheDirectory = "${cacheDir}";
|
||||
|
||||
# Site language code, should be one of the list in ./languages/data/Names.php
|
||||
$wgLanguageCode = "en";
|
||||
|
||||
$wgSecretKey = file_get_contents("${stateDir}/secret.key");
|
||||
|
||||
# Changing this will log out all existing sessions.
|
||||
$wgAuthenticationTokenVersion = "";
|
||||
|
||||
## For attaching licensing metadata to pages, and displaying an
|
||||
## appropriate copyright notice / icon. GNU Free Documentation
|
||||
## License and Creative Commons licenses are supported so far.
|
||||
$wgRightsPage = ""; # Set to the title of a wiki page that describes your license/copyright
|
||||
$wgRightsUrl = "";
|
||||
$wgRightsText = "";
|
||||
$wgRightsIcon = "";
|
||||
|
||||
# Enable APCU caching
|
||||
$wgMainCacheType = CACHE_ACCEL;
|
||||
|
||||
# Enabled skins.
|
||||
${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadSkin('${k}');") cfg.skins)}
|
||||
|
||||
# Enabled extensions.
|
||||
${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadExtension('${k}');") cfg.extensions)}
|
||||
|
||||
|
||||
# End of automatically generated settings.
|
||||
# Add more configuration options below.
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
};
|
||||
|
||||
withTrailingSlash = str: if lib.hasSuffix "/" str then str else "${str}/";
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.mediawiki = {
|
||||
|
||||
enable = mkEnableOption "MediaWiki";
|
||||
|
||||
package = mkPackageOption pkgs "mediawiki" { };
|
||||
|
||||
# https://www.mediawiki.org/wiki/Compatibility#PHP
|
||||
phpPackage = mkPackageOption pkgs "php" { } // {
|
||||
default = pkgs.php83.buildEnv {
|
||||
extensions = { all, enabled }: enabled ++ (with all; [ apcu ]);
|
||||
};
|
||||
};
|
||||
|
||||
finalPackage = mkOption {
|
||||
type = types.package;
|
||||
readOnly = true;
|
||||
default = pkg;
|
||||
defaultText = literalExpression "pkg";
|
||||
description = ''
|
||||
The final package used by the module. This is the package that will have extensions and skins installed.
|
||||
'';
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "MediaWiki";
|
||||
example = "Foobar Wiki";
|
||||
description = "Name of the wiki.";
|
||||
};
|
||||
|
||||
url = mkOption {
|
||||
type = types.str;
|
||||
default =
|
||||
if cfg.webserver == "apache" then
|
||||
"${
|
||||
if
|
||||
cfg.httpd.virtualHost.addSSL || cfg.httpd.virtualHost.forceSSL || cfg.httpd.virtualHost.onlySSL
|
||||
then
|
||||
"https"
|
||||
else
|
||||
"http"
|
||||
}://${cfg.httpd.virtualHost.hostName}"
|
||||
else if cfg.webserver == "nginx" then
|
||||
let
|
||||
hasSSL = host: host.forceSSL || host.addSSL;
|
||||
in
|
||||
"${
|
||||
if hasSSL config.services.nginx.virtualHosts.${cfg.nginx.hostName} then "https" else "http"
|
||||
}://${cfg.nginx.hostName}"
|
||||
else
|
||||
"http://localhost";
|
||||
defaultText = ''
|
||||
if "mediawiki uses ssl" then "{"https" else "http"}://''${cfg.hostName}" else "http://localhost";
|
||||
'';
|
||||
example = "https://wiki.example.org";
|
||||
description = "URL of the wiki.";
|
||||
};
|
||||
|
||||
uploadsDir = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = "${stateDir}/uploads";
|
||||
description = ''
|
||||
This directory is used for uploads of pictures. The directory passed here is automatically
|
||||
created and permissions adjusted as required.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
A file containing the initial password for the administrator account "admin".
|
||||
'';
|
||||
example = "/run/keys/mediawiki-password";
|
||||
};
|
||||
|
||||
passwordSender = mkOption {
|
||||
type = types.str;
|
||||
default =
|
||||
if cfg.webserver == "apache" then
|
||||
if cfg.httpd.virtualHost.adminAddr != null then
|
||||
cfg.httpd.virtualHost.adminAddr
|
||||
else
|
||||
config.services.httpd.adminAddr
|
||||
else
|
||||
"root@localhost";
|
||||
defaultText = literalExpression ''
|
||||
if cfg.webserver == "apache" then
|
||||
if cfg.httpd.virtualHost.adminAddr != null then
|
||||
cfg.httpd.virtualHost.adminAddr
|
||||
else
|
||||
config.services.httpd.adminAddr else "root@localhost"
|
||||
'';
|
||||
description = "Contact address for password reset.";
|
||||
};
|
||||
|
||||
path = mkOption {
|
||||
type = types.listOf types.package;
|
||||
defaultText = lib.literalExpression "with pkgs; [ diffutils imagemagick ]";
|
||||
example = lib.literalExpression "with pkgs; [ librsvg ]";
|
||||
description = "Extra packages to add to the PATH of phpfpm-pool.";
|
||||
};
|
||||
|
||||
skins = mkOption {
|
||||
default = { };
|
||||
type = types.attrsOf types.path;
|
||||
description = ''
|
||||
Attribute set of paths whose content is copied to the {file}`skins`
|
||||
subdirectory of the MediaWiki installation in addition to the default skins.
|
||||
'';
|
||||
};
|
||||
|
||||
extensions = mkOption {
|
||||
default = { };
|
||||
type = types.attrsOf (types.nullOr types.path);
|
||||
description = ''
|
||||
Attribute set of paths whose content is copied to the {file}`extensions`
|
||||
subdirectory of the MediaWiki installation and enabled in configuration.
|
||||
|
||||
Use `null` instead of path to enable extensions that are part of MediaWiki.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
Matomo = pkgs.fetchzip {
|
||||
url = "https://github.com/DaSchTour/matomo-mediawiki-extension/archive/v4.0.1.tar.gz";
|
||||
sha256 = "0g5rd3zp0avwlmqagc59cg9bbkn3r7wx7p6yr80s644mj6dlvs1b";
|
||||
};
|
||||
ParserFunctions = null;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
webserver = mkOption {
|
||||
type = types.enum [
|
||||
"apache"
|
||||
"none"
|
||||
"nginx"
|
||||
];
|
||||
default = "apache";
|
||||
description = "Webserver to use.";
|
||||
};
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum [
|
||||
"mysql"
|
||||
"postgres"
|
||||
"mssql"
|
||||
"oracle"
|
||||
"sqlite"
|
||||
];
|
||||
default = "mysql";
|
||||
description = "Database engine to use. MySQL/MariaDB is the database of choice by MediaWiki developers.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = if cfg.database.type == "sqlite" then null else "localhost";
|
||||
defaultText = ''"localhost"'';
|
||||
description = "Database host address. Used only if database type is not SQLite.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default =
|
||||
if cfg.database.type == "mysql" then
|
||||
3306
|
||||
else if cfg.database.type != "sqlite" then
|
||||
5432
|
||||
else
|
||||
null;
|
||||
defaultText = literalExpression "3306";
|
||||
description = "Database host port. Used only if database type is not SQLite.";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "mediawiki";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = if cfg.database.type != "sqlite" then "mediawiki" else null;
|
||||
defaultText = literalExpression ''"mediawiki"'';
|
||||
description = "Database user. Used only if database type is not SQLite.";
|
||||
};
|
||||
|
||||
path = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = if cfg.database.type == "sqlite" then "${stateDir}/data" else null;
|
||||
defaultText = literalExpression ''"${stateDir}/data"'';
|
||||
description = "Path to store the MediaWiki database in if using SQLite.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/run/keys/mediawiki-dbpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
{option}`database.user`. Used only if database type is not SQLite.
|
||||
'';
|
||||
};
|
||||
|
||||
tablePrefix = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
If you only have access to a single database and wish to install more than
|
||||
one version of MediaWiki, or have other applications that also use the
|
||||
database, you can give the table names a unique prefix to stop any naming
|
||||
conflicts or confusion. Only used if database type is MySQL.
|
||||
See <https://www.mediawiki.org/wiki/Manual:$wgDBprefix>.
|
||||
'';
|
||||
};
|
||||
|
||||
socket = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default =
|
||||
if (cfg.database.type == "mysql" && cfg.database.createLocally) then
|
||||
"/run/mysqld/mysqld.sock"
|
||||
else if (cfg.database.type == "postgres" && cfg.database.createLocally) then
|
||||
"/run/postgresql"
|
||||
else
|
||||
null;
|
||||
defaultText = literalExpression "/run/mysqld/mysqld.sock";
|
||||
description = "Path to the unix socket file to use for authentication. Used only if database type is not SQLite.";
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = cfg.database.type == "mysql" || cfg.database.type == "postgres";
|
||||
defaultText = literalExpression "true";
|
||||
description = ''
|
||||
Create the database and database user locally.
|
||||
This currently only applies if database type "mysql" or "postgres" is selected.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
nginx.hostName = mkOption {
|
||||
type = types.str;
|
||||
example = literalExpression "wiki.example.com";
|
||||
default = "localhost";
|
||||
description = ''
|
||||
The hostname to use for the nginx virtual host.
|
||||
This is used to generate the nginx configuration.
|
||||
'';
|
||||
};
|
||||
|
||||
httpd.virtualHost = mkOption {
|
||||
type = types.submodule { }; # (import ../web-servers/apache-httpd/vhost-options.nix);
|
||||
example = literalExpression ''
|
||||
{
|
||||
hostName = "mediawiki.example.org";
|
||||
adminAddr = "webmaster@example.org";
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`.
|
||||
See [](#opt-services.httpd.virtualHosts) for further information.
|
||||
'';
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type =
|
||||
with types;
|
||||
attrsOf (oneOf [
|
||||
str
|
||||
int
|
||||
bool
|
||||
]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the MediaWiki PHP pool. See the documentation on `php-fpm.conf`
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
description = ''
|
||||
Any additional text to be appended to MediaWiki's
|
||||
LocalSettings.php configuration file. For configuration
|
||||
settings, see <https://www.mediawiki.org/wiki/Manual:Configuration_settings>.
|
||||
'';
|
||||
default = "";
|
||||
example = ''
|
||||
$wgEnableEmail = false;
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
imports = [
|
||||
(lib.mkRenamedOptionModule
|
||||
[ "services" "mediawiki" "virtualHost" ]
|
||||
[ "services" "mediawiki" "httpd" "virtualHost" ]
|
||||
)
|
||||
];
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion =
|
||||
cfg.database.createLocally -> (cfg.database.type == "mysql" || cfg.database.type == "postgres");
|
||||
message = "services.mediawiki.createLocally is currently only supported for database type 'mysql' and 'postgres'";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
cfg.database.createLocally -> cfg.database.user == user && cfg.database.name == cfg.database.user;
|
||||
message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true";
|
||||
}
|
||||
{
|
||||
assertion = cfg.database.createLocally -> cfg.database.socket != null;
|
||||
message = "services.mediawiki.database.socket must be set if services.mediawiki.database.createLocally is set to true";
|
||||
}
|
||||
{
|
||||
assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
|
||||
message = "a password cannot be specified if services.mediawiki.database.createLocally is set to true";
|
||||
}
|
||||
];
|
||||
|
||||
warnings =
|
||||
lib.optional
|
||||
(
|
||||
cfg.database.type == "sqlite"
|
||||
&& (
|
||||
cfg.database.host != null
|
||||
|| cfg.database.port != null
|
||||
|| cfg.database.user != null
|
||||
|| cfg.database.passwordFile != null
|
||||
|| cfg.database.socket != null
|
||||
)
|
||||
)
|
||||
''
|
||||
The services.mediawiki.database options host, port, user, passwordFile, and socket will be ignored because services.mediawiki.database.type is "sqlite".
|
||||
''
|
||||
++ lib.optional (cfg.database.type != "sqlite" && cfg.database.path != null) ''
|
||||
The services.mediawiki.database.path option will be ignored because services.mediawiki.database.type is not "sqlite".
|
||||
''
|
||||
++ lib.optional (cfg.database.type != "mysql" && cfg.database.tablePrefix != null) ''
|
||||
The services.mediawiki.database.tablePrefix option has no effect when the services.mediawiki.database.type is not "mysql".
|
||||
'';
|
||||
|
||||
services.mediawiki = {
|
||||
path = with pkgs; [
|
||||
diffutils
|
||||
imagemagick
|
||||
];
|
||||
skins = {
|
||||
MonoBook = "${cfg.package}/share/mediawiki/skins/MonoBook";
|
||||
Timeless = "${cfg.package}/share/mediawiki/skins/Timeless";
|
||||
Vector = "${cfg.package}/share/mediawiki/skins/Vector";
|
||||
};
|
||||
};
|
||||
|
||||
services.mysql = mkIf (cfg.database.type == "mysql" && cfg.database.createLocally) {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = cfg.database.user;
|
||||
ensurePermissions = {
|
||||
"${cfg.database.name}.*" = "ALL PRIVILEGES";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.postgresql = mkIf (cfg.database.type == "postgres" && cfg.database.createLocally) {
|
||||
enable = true;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = cfg.database.user;
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.phpfpm.pools.mediawiki = {
|
||||
inherit user group;
|
||||
phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}";
|
||||
phpPackage = cfg.phpPackage;
|
||||
settings =
|
||||
(
|
||||
if (cfg.webserver == "apache") then
|
||||
{
|
||||
"listen.owner" = config.services.httpd.user;
|
||||
"listen.group" = config.services.httpd.group;
|
||||
}
|
||||
else if (cfg.webserver == "nginx") then
|
||||
{
|
||||
"listen.owner" = config.services.nginx.user;
|
||||
"listen.group" = config.services.nginx.group;
|
||||
}
|
||||
else
|
||||
{
|
||||
"listen.owner" = user;
|
||||
"listen.group" = group;
|
||||
}
|
||||
)
|
||||
// cfg.poolConfig;
|
||||
};
|
||||
|
||||
services.httpd = lib.mkIf (cfg.webserver == "apache") {
|
||||
enable = true;
|
||||
extraModules = [ "proxy_fcgi" ];
|
||||
virtualHosts.${cfg.httpd.virtualHost.hostName} = mkMerge [
|
||||
cfg.httpd.virtualHost
|
||||
{
|
||||
documentRoot = mkForce "${pkg}/share/mediawiki";
|
||||
extraConfig = ''
|
||||
<Directory "${pkg}/share/mediawiki">
|
||||
<FilesMatch "\.php$">
|
||||
<If "-f %{REQUEST_FILENAME}">
|
||||
SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
|
||||
</If>
|
||||
</FilesMatch>
|
||||
|
||||
Require all granted
|
||||
DirectoryIndex index.php
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
''
|
||||
+ optionalString (cfg.uploadsDir != null) ''
|
||||
Alias "/images" "${cfg.uploadsDir}"
|
||||
<Directory "${cfg.uploadsDir}">
|
||||
Require all granted
|
||||
</Directory>
|
||||
'';
|
||||
}
|
||||
];
|
||||
};
|
||||
# inspired by https://www.mediawiki.org/wiki/Manual:Short_URL/Nginx
|
||||
services.nginx = lib.mkIf (cfg.webserver == "nginx") {
|
||||
enable = true;
|
||||
virtualHosts.${config.services.mediawiki.nginx.hostName} = {
|
||||
root = "${pkg}/share/mediawiki";
|
||||
locations = {
|
||||
"~ ^/w/(index|load|api|thumb|opensearch_desc|rest|img_auth)\\.php$".extraConfig = ''
|
||||
rewrite ^/w/(.*) /$1 break;
|
||||
include ${config.services.nginx.package}/conf/fastcgi.conf;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools.mediawiki.socket};
|
||||
'';
|
||||
"/w/images/".alias = withTrailingSlash cfg.uploadsDir;
|
||||
# Deny access to deleted images folder
|
||||
"/w/images/deleted".extraConfig = ''
|
||||
deny all;
|
||||
'';
|
||||
# MediaWiki assets (usually images)
|
||||
"~ ^/w/resources/(assets|lib|src)".extraConfig = ''
|
||||
rewrite ^/w(/.*) $1 break;
|
||||
add_header Cache-Control "public";
|
||||
expires 7d;
|
||||
'';
|
||||
# Assets, scripts and styles from skins and extensions
|
||||
"~ ^/w/(skins|extensions)/.+\\.(css|js|gif|jpg|jpeg|png|svg|wasm|ttf|woff|woff2)$".extraConfig = ''
|
||||
rewrite ^/w(/.*) $1 break;
|
||||
add_header Cache-Control "public";
|
||||
expires 7d;
|
||||
'';
|
||||
|
||||
# Handling for Mediawiki REST API, see [[mw:API:REST_API]]
|
||||
"/w/rest.php/".tryFiles = "$uri $uri/ /w/rest.php?$query_string";
|
||||
|
||||
# Handling for the article path (pretty URLs)
|
||||
"/wiki/".extraConfig = ''
|
||||
rewrite ^/wiki/(?<pagename>.*)$ /w/index.php;
|
||||
'';
|
||||
|
||||
# Explicit access to the root website, redirect to main page (adapt as needed)
|
||||
"= /".extraConfig = ''
|
||||
return 301 /wiki/;
|
||||
'';
|
||||
|
||||
# Every other entry point will be disallowed.
|
||||
# Add specific rules for other entry points/images as needed above this
|
||||
"/".extraConfig = ''
|
||||
return 404;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${stateDir}' 0750 ${user} ${group} - -"
|
||||
"d '${cacheDir}' 0750 ${user} ${group} - -"
|
||||
]
|
||||
++ optionals (cfg.uploadsDir != null) [
|
||||
"d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
|
||||
"Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
|
||||
];
|
||||
|
||||
systemd.services.mediawiki-init = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "phpfpm-mediawiki.service" ];
|
||||
after =
|
||||
optional (cfg.database.type == "mysql" && cfg.database.createLocally) "mysql.service"
|
||||
++ optional (cfg.database.type == "postgres" && cfg.database.createLocally) "postgresql.target";
|
||||
script =
|
||||
let
|
||||
dbOptions =
|
||||
if cfg.database.type == "sqlite" then
|
||||
''
|
||||
--dbpath ${lib.escapeShellArg cfg.database.path} \
|
||||
--dbname ${lib.escapeShellArg cfg.database.name} \
|
||||
''
|
||||
else
|
||||
''
|
||||
--dbserver ${lib.escapeShellArg dbAddr} \
|
||||
--dbport ${toString cfg.database.port} \
|
||||
--dbname ${lib.escapeShellArg cfg.database.name} \
|
||||
${
|
||||
optionalString (
|
||||
cfg.database.tablePrefix != null
|
||||
) "--dbprefix ${lib.escapeShellArg cfg.database.tablePrefix}"
|
||||
} \
|
||||
--dbuser ${lib.escapeShellArg cfg.database.user} \
|
||||
${
|
||||
optionalString (
|
||||
cfg.database.passwordFile != null
|
||||
) "--dbpassfile ${lib.escapeShellArg cfg.database.passwordFile}"
|
||||
} \
|
||||
'';
|
||||
in
|
||||
''
|
||||
if ! test -e "${stateDir}/secret.key"; then
|
||||
tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c 64 > ${stateDir}/secret.key
|
||||
fi
|
||||
|
||||
${optionalString (cfg.database.type == "sqlite") "mkdir -p ${cfg.database.path}"}
|
||||
|
||||
echo "exit( \$this->getPrimaryDB()->tableExists( 'user' ) ? 1 : 0 );" | \
|
||||
${cfg.phpPackage}/bin/php ${pkg}/share/mediawiki/maintenance/run.php eval --conf ${mediawikiConfig} && \
|
||||
${cfg.phpPackage}/bin/php ${pkg}/share/mediawiki/maintenance/run.php ${pkg}/share/mediawiki/maintenance/install.php \
|
||||
--confpath /tmp \
|
||||
--scriptpath / \
|
||||
${dbOptions} \
|
||||
--dbtype ${cfg.database.type} \
|
||||
--passfile ${lib.escapeShellArg cfg.passwordFile} \
|
||||
${lib.escapeShellArg cfg.name} \
|
||||
admin
|
||||
|
||||
${cfg.phpPackage}/bin/php ${pkg}/share/mediawiki/maintenance/update.php --conf ${mediawikiConfig} --quick --skip-external-dependencies
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = user;
|
||||
Group = group;
|
||||
PrivateTmp = true;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.httpd.after =
|
||||
optional (
|
||||
cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "mysql"
|
||||
) "mysql.service"
|
||||
++ optional (
|
||||
cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "postgres"
|
||||
) "postgresql.target";
|
||||
|
||||
users.users.${user} = {
|
||||
inherit group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
users.groups.${group} = { };
|
||||
|
||||
environment.systemPackages = [ mediawikiScripts ];
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
{
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
phpExtensions = config.services.mediawiki.phpPackage.extensions;
|
||||
in
|
||||
{
|
||||
# Mauvaise config posgresql, on fixe à la mano temporairement
|
||||
disabledModules = [ "services/web-apps/mediawiki.nix" ];
|
||||
imports = [ ./mediawiki-patch.nix ];
|
||||
|
||||
age.secrets.mediawiki-admin-passwd = {
|
||||
file = ../../../secrets/mediakiwi/mediawiki-admin-passwd.age;
|
||||
owner = "mediawiki";
|
||||
};
|
||||
|
||||
age.secrets.mediawiki-ldap = {
|
||||
file = ../../../secrets/mediakiwi/mediawiki-ldap.age;
|
||||
owner = "mediawiki";
|
||||
};
|
||||
|
||||
age.secrets.mediawiki-oauth = {
|
||||
file = ../../../secrets/mediakiwi/mediawiki-oauth.age;
|
||||
owner = "mediawiki";
|
||||
};
|
||||
|
||||
# Attention : il ne faut pas mettre de retour à la ligne à la fin du mot de passe !
|
||||
age.secrets.mediawiki-db = {
|
||||
file = ../../../secrets/mediakiwi/mediawiki-db-pass.age;
|
||||
owner = "mediawiki";
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
imagemagick
|
||||
|
||||
# For the PdfHandler extension
|
||||
ghostscript
|
||||
poppler-utils
|
||||
|
||||
# For the SyntaxHighlighting extension
|
||||
python3Packages.pygments
|
||||
];
|
||||
|
||||
services.phpfpm.pools.mediawiki.phpOptions = ''
|
||||
upload_max_filesize = 512M
|
||||
post_max_size = 512M
|
||||
max_execution_time = 1000
|
||||
max_input_time = 2000
|
||||
default_socket_timeout = 2000
|
||||
extension = ${phpExtensions.mbstring}/lib/php/extensions/mbstring.so
|
||||
extension = ${phpExtensions.luasandbox}/lib/php/extensions/luasandbox.so
|
||||
'';
|
||||
|
||||
services.nginx.clientMaxBodySize = "512M";
|
||||
|
||||
services.syslogd.enable = true;
|
||||
|
||||
# Tâches lourdes à gérer au fond https://www.mediawiki.org/wiki/Manual:Job_queue
|
||||
systemd.services.mediawiki-jobs =
|
||||
let
|
||||
# Hack pour retrouver la conf
|
||||
conf = config.services.phpfpm.pools.mediawiki.phpEnv.MEDIAWIKI_CONFIG;
|
||||
php = "${config.services.mediawiki.phpPackage}/bin/php";
|
||||
maintenanceScript = "${config.services.mediawiki.finalPackage}/share/mediawiki/maintenance/run.php";
|
||||
runJobs = "MEDIAWIKI_CONFIG='${conf}' ${php} ${maintenanceScript} runJobs --maxtime=3600";
|
||||
in
|
||||
{
|
||||
description = "Tâches lourdes de Mediawiki";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "phpfpm-mediawiki.service" ];
|
||||
path = [ pkgs.mediawiki ];
|
||||
# Inspiré de https://www.mediawiki.org/wiki/Manual:Job_queue
|
||||
script = ''
|
||||
while true; do
|
||||
${runJobs} --type="enotifNotify"
|
||||
# --wait pour ne pas boucler pour rien
|
||||
${runJobs} --wait --maxjobs=20
|
||||
# Au bout de 20 jobs on attend pour éviter de surcharger le serveur
|
||||
# avec des tâches de fond
|
||||
sleep 5
|
||||
done
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Restart = "always";
|
||||
RestartSec = "60s";
|
||||
User = "mediawiki";
|
||||
Nice = 10;
|
||||
ProtectSystem = "full";
|
||||
OOMScoreAdjust = 200;
|
||||
};
|
||||
};
|
||||
|
||||
services.mediawiki = {
|
||||
enable = true;
|
||||
|
||||
name = "Wiki Crans";
|
||||
|
||||
nginx.hostName = "mediawiki.crans.org";
|
||||
|
||||
webserver = "nginx";
|
||||
|
||||
passwordFile = config.age.secrets.mediawiki-admin-passwd.path;
|
||||
|
||||
database = {
|
||||
createLocally = false;
|
||||
host = "tealc.adm.crans.org";
|
||||
name = "mediawiki";
|
||||
user = "mediawiki";
|
||||
passwordFile = config.age.secrets.mediawiki-db.path;
|
||||
type = "postgres";
|
||||
};
|
||||
|
||||
extraConfig = ''
|
||||
# TODO remove log error in test
|
||||
error_reporting( -1 );
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
|
||||
# Server settings
|
||||
$wgFavicon = 'https://www.crans.org/images/favicon.ico';
|
||||
$wgLogo = 'https://www.crans.org/images/crans.svg';
|
||||
$wgJobRunRate = 0; # On fait les jobs manuellement dans un systemd.
|
||||
$wgEnableEditRecovery = true; # Pour pouvoir récuppérer des changements non sauvegardées (1 mois par défaut en local)
|
||||
$wgRestrictDisplayTitle = false; # Pour changer l’affichage des noms de pages.
|
||||
|
||||
# Files and Uploads
|
||||
$wgMaxUploadSize = 512 * 1024 * 1024;
|
||||
$wgUseImageMagick = true;
|
||||
$wgImageMagickConvertCommand = '${pkgs.imagemagick}/bin/convert';
|
||||
$wgStrictFileExtensions = false; # On autorise toutes les extensions sauf https://www.mediawiki.org/wiki/Manual:$wgProhibitedFileExtensions
|
||||
$wgFileExtensions[] = 'pdf';
|
||||
$wgFileExtensions[] = 'svg';
|
||||
|
||||
# Limite mémoire, quand on fait de gros importi (9M de xml), le parser
|
||||
# prends plus que 50M
|
||||
$wgMemoryLimit = 512 * 1024 * 1024;
|
||||
|
||||
# E-mail settings
|
||||
$wgEnableEmail = true;
|
||||
$wgSMTP = [
|
||||
'host' => 'smtp.adm.crans.org',
|
||||
'IDHost' => 'crans.org',
|
||||
'localhost' => 'crans.org',
|
||||
'port' => 25,
|
||||
'auth' => false,
|
||||
];
|
||||
$wgPasswordSender = 'wiki@crans.org'; # C’est un alias de root (pas de réponse)
|
||||
$wgEmergencyContact = 'contact@crans.org';
|
||||
$wgNoReplyAddress = 'contact@crans.org'; # On reçoit les réponses aux mails
|
||||
$wgEnableUserEmail = true; # On active les emails aux utilisateurices mais restreint via les ACLs
|
||||
$wgAllowHTMLEmail = true;
|
||||
|
||||
$wgEnotifUseRealName = true; # On active les noms réels pour plus de lisibilité avec les comptes note
|
||||
$wgEnotifFromEditor = false; # On ne se fait pas passer par l’utilisateurice
|
||||
$wgEnotifRevealEditorAddress = false; # Pas de champ répondre avec l’adresse mail de l’utilisateurice
|
||||
$wgEnotifUserTalk = true;
|
||||
$wgEnotifMinorEdits = true;
|
||||
$wgEnotifWatchlist = true;
|
||||
# Peut-être utilisé pour les Wikistes
|
||||
$wgUsersNotifiedOnAllChanges = [ 'Ninja_wiki_notifs' ];
|
||||
|
||||
# Auth
|
||||
$wgPluggableAuth_EnableLocalLogin = false;
|
||||
$LDAPAuthentication2AllowLocalLogin = true;
|
||||
$LDAPProviderDomainConfigs = "${config.age.secrets.mediawiki-ldap.path}";
|
||||
$wgOAuthCustomAuthProviders = [
|
||||
'note' => WSOAuth\AuthenticationProvider\NoteKfetAuth::class
|
||||
];
|
||||
$wgPluggableAuth_Config = [
|
||||
"Compte Crans" => [
|
||||
'plugin' => 'LDAPAuthentication2',
|
||||
'data' => [
|
||||
'domain' => 'crans'
|
||||
],
|
||||
'buttonLabelMessage' => 'crans-connexion-crans'
|
||||
],
|
||||
"Note BDE" => [
|
||||
'plugin' => 'WSOAuth',
|
||||
'data' => require('${config.age.secrets.mediawiki-oauth.path}'),
|
||||
'buttonLabelMessage' => 'crans-connexion-note-bde'
|
||||
]
|
||||
];
|
||||
|
||||
# Theme
|
||||
$wgDefaultSkin = 'citizen';
|
||||
$wgCitizenThemeColor = '#AD1F1F';
|
||||
$wgCitizenEnableARFonts = true;
|
||||
$wgCitizenEnableCJKFonts = true;
|
||||
|
||||
$wgLanguageCode = 'fr';
|
||||
$wgLocaltimezone = 'Europe/Paris';
|
||||
$wgDefaultUserOptions['timecorrection'] = 'ZoneInfo|0|' . $wgLocaltimezone;
|
||||
|
||||
# Access Control
|
||||
$wgGroupPermissions['*']['edit'] = false; # Restrict edition for anonymous user
|
||||
$wgGroupPermissions['*']['createaccount'] = false; # Restrict the creation of account to sysop only
|
||||
$wgGroupPermissions['*']['autocreateaccount'] = true; # Création de comptes depuis LDAP et Note mais pas local
|
||||
$wgGroupPermissions['*']['delete'] = true; # Création de comptes depuis LDAP et Note mais pas local
|
||||
$wgGroupPermissions['user']['sendemail'] = false;
|
||||
$wgGroupPermissions['sysop']['sendemail'] = true; # On réserve l’envoie de mail aux admins
|
||||
# Relecture des pages, envoie de mails autorisés
|
||||
$wgGroupPermissions['patrol'] = [ 'autopatrol' => true, 'patrol' => true, 'sendemail' => true ];
|
||||
$wgCategoryLockdownWhitelist = [
|
||||
"Spécial:Connexion",
|
||||
"Spécial:Déconnexion",
|
||||
"Spécial:Connexion/return",
|
||||
"Spécial:PluggableAuthLogin",
|
||||
"Spécial:Recherche",
|
||||
"MediaWiki:Common.css",
|
||||
"MediaWiki:Common.js"
|
||||
];
|
||||
$wgCategoryGroupLockdown["!user"]["read"] = [ "Page Publique" ]; # Restrict read for non-user (i.e. anonymous) on execpt for Page Publique
|
||||
$wgFeed = false; # Pas de flux car ils ne sont pas authentifiés :(.
|
||||
|
||||
# Extensions
|
||||
$wgWikiEditorRealtimePreview = true;
|
||||
$wgCiteBookReferencing = true;
|
||||
$wgPdfProcessor = '${pkgs.ghostscript}/bin/gs';
|
||||
$wgPdfPostProcessor = $wgImageMagickConvertCommand;
|
||||
$wgPdfInfo = '${pkgs.poppler-utils}/bin/pdfinfo';
|
||||
$wgPdftoText = '${pkgs.poppler-utils}/bin/pdftotext';
|
||||
$wgScribuntoDefaultEngine = 'luasandbox';
|
||||
# 4 pour les catégories, 3 pour le reste https://www.mediawiki.org/wiki/Extension:CategoryTree
|
||||
$wgCategoryTreeMaxDepth = [10 => 3, 20 => 3, 0 => 4, 100 => 3];
|
||||
# SyntaxHighlight extension
|
||||
$wgPygmentizePath = '${pkgs.python3Packages.pygments}/bin/pygmentize';
|
||||
|
||||
# Custom Namespaces
|
||||
define("NS_ARCHIVE", 3000);
|
||||
define("NS_ARCHIVE_TALK", 3001);
|
||||
$wgExtraNamespaces = [
|
||||
NS_ARCHIVE => "Archive",
|
||||
NS_ARCHIVE_TALK => "Discussion_archive",
|
||||
];
|
||||
# Sous pages
|
||||
$wgNamespacesWithSubpages[NS_MAIN] = true;
|
||||
$wgNamespacesWithSubpages[NS_ARCHIVE] = true;
|
||||
$wgNamespacesWithSubpages[NS_CATEGORY] = true;
|
||||
$wgVisualEditorAvailableNamespaces[NS_ARCHIVE] = true;
|
||||
# Pour les Popups
|
||||
$wgContentNamespaces[] = NS_ARCHIVE;
|
||||
$wgContentNamespaces[] = NS_USER;
|
||||
# Pour la recherche
|
||||
$wgNamespacesToBeSearchedDefault[NS_ARCHIVE] = true; # Temporairement pour la migration ?
|
||||
$wgNamespacesToBeSearchedDefault[NS_USER] = true;
|
||||
'';
|
||||
|
||||
skins = {
|
||||
Citizen = pkgs.fetchFromGitHub {
|
||||
name = "Citizen";
|
||||
owner = "StarCitizenTools";
|
||||
repo = "mediawiki-skins-Citizen";
|
||||
tag = "v3.19.0";
|
||||
sha256 = "sha256-iQS0lyvOYH45gI6fMKYV4ERK9JO35J8ktLNrQi6UsH0=";
|
||||
};
|
||||
};
|
||||
|
||||
extensions = {
|
||||
# Enables embedded extensions
|
||||
AbuseFilter = null; # pour faire de la modération
|
||||
CategoryTree = null; # pour faire des arbres de catégories
|
||||
Cite = null; # pour faire des références/footnotes
|
||||
CiteThisPage = null; # pour avoir la citation (à la bibtex) d'une page
|
||||
CodeEditor = null; # pour éditer des macros/scripts wiki
|
||||
DiscussionTools = null; # pour des pages de discussion intéractives
|
||||
Echo = null; # pour le système de notification du wiki
|
||||
Gadgets = null; # pour avoir un système d'outils activable par les users
|
||||
ImageMap = null; # pour mettre des widgets sur des images
|
||||
Linter = null; # requis pour DiscussionTools
|
||||
Math = null; # pour avoir des maths LaTeX
|
||||
MultimediaViewer = null; # pour avoir un affichage sympa des images
|
||||
Nuke = null; # pour purger des pages
|
||||
PageImages = null; # pour set la bonne image représentant un article
|
||||
ParserFunctions = null; # pour la logique et les fonctions de base du templating
|
||||
PdfHandler = null; # pour afficher les pdfs
|
||||
Poem = null; # pour afficher des blocks de texte respectant l'espacement (poèmes, écrits)
|
||||
ReplaceText = null; # pour du méga-renommage à travers le wiki
|
||||
Scribunto = null; # pour faire du scripting et des templates pour le wiki
|
||||
SyntaxHighlight_GeSHi = null; # pour avoir de la coloration syntaxique
|
||||
TemplateData = null; # pour faire de la doc sur les templates
|
||||
TemplateStyles = null; # pour styliser les templates (bundlé pour la prochaine version de mediawiki)
|
||||
TextExtracts = null; # pour set le bon text représentant un article
|
||||
Thanks = null; # pour remercier des gens en notif
|
||||
TitleBlacklist = null; # pour faire de la modération
|
||||
VisualEditor = null; # pour éditer visuellement les pages
|
||||
WikiEditor = null; # pour éditer le code wiki des pages
|
||||
|
||||
CategoryLockdown = pkgs.applyPatches {
|
||||
src = pkgs.fetchFromGitHub {
|
||||
name = "CategoryLockdown";
|
||||
owner = "wikimedia";
|
||||
repo = "mediawiki-extensions-CategoryLockdown";
|
||||
rev = "d29e734c87697590af80f91d0cdbb4275bc3f072";
|
||||
# Le SHA doit être changé à chaque nouveau commit de traduction.
|
||||
# Pas de meilleure solution à ma connaissance pour suivre les releases.
|
||||
sha256 = "sha256-iYRzCzWu3hndPoMIkFZOaYJkbVoAnVv/ZMZTSAcQdKo=";
|
||||
};
|
||||
patches = [
|
||||
# Cette extension s’occupe des du contrôle d’accès du Wiki
|
||||
# et a été beaucoup patché. Pensez à vérifier les changements
|
||||
# et révisez le patch le cas échéant.
|
||||
"${./category-lockdown.patch}"
|
||||
];
|
||||
};
|
||||
|
||||
# FullCalendar
|
||||
FullCalendar = ./FullCalendar;
|
||||
|
||||
# Popups
|
||||
Popups = pkgs.fetchFromGitHub {
|
||||
name = "Popups";
|
||||
owner = "wikimedia";
|
||||
repo = "mediawiki-extensions-Popups";
|
||||
rev = "82742d6e42750bf5b8000c90fc894b304aa95235";
|
||||
# Le SHA doit être changé à chaque nouveau commit de traduction.
|
||||
# Pas de meilleure solution à ma connaissance pour suivre les releases.
|
||||
sha256 = "sha256-mqiDU7w3B3imMLqoTvwgnyk/vCDWeAiTEX1y4dWmNDk=";
|
||||
};
|
||||
|
||||
# Auth
|
||||
PluggableAuth = pkgs.fetchFromGitHub {
|
||||
name = "PluggableAuth";
|
||||
owner = "wikimedia";
|
||||
repo = "mediawiki-extensions-PluggableAuth";
|
||||
rev = "3b9428bd8832aeb8ef8ff1ea61b7a4afca7f0ecb";
|
||||
# Le SHA doit être changé à chaque nouveau commit de traduction.
|
||||
# Pas de meilleure solution à ma connaissance pour suivre les releases.
|
||||
sha256 = "sha256-9PUf7KC0oz4XeN/m/5KbefNpsgRwV2NswMQZl7a/+I0=";
|
||||
};
|
||||
LDAPProvider = pkgs.fetchFromGitHub {
|
||||
name = "LDAPProvider";
|
||||
owner = "wikimedia";
|
||||
repo = "mediawiki-extensions-LDAPProvider";
|
||||
tag = "3.1.0";
|
||||
# Le SHA doit être changé à chaque nouveau commit de traduction.
|
||||
# Pas de meilleure solution à ma connaissance pour suivre les releases.
|
||||
sha256 = "sha256-9/5Uph5C6VPWT7NzrPsYpM9pxZPIQneKZDPFCaYm1e8=";
|
||||
};
|
||||
LDAPAuthentication2 = pkgs.fetchFromGitHub {
|
||||
name = "LDAPAuthentication2";
|
||||
owner = "wikimedia";
|
||||
repo = "mediawiki-extensions-LDAPAuthentication2";
|
||||
tag = "3.1.0";
|
||||
# Le SHA doit être changé à chaque nouveau commit de traduction.
|
||||
# Pas de meilleure solution à ma connaissance pour suivre les releases.
|
||||
sha256 = "sha256-/9VISvpvhaVq/sAHE10INlTju7+PTirFO/6U68WBOgM=";
|
||||
};
|
||||
LDAPGroup = pkgs.fetchFromGitHub {
|
||||
name = "LDAPGroup";
|
||||
owner = "wikimedia";
|
||||
repo = "mediawiki-extensions-LDAPGroups";
|
||||
tag = "3.1.0";
|
||||
# Le SHA doit être changé à chaque nouveau commit de traduction.
|
||||
# Pas de meilleure solution à ma connaissance pour suivre les releases.
|
||||
sha256 = "sha256-aW1nVechxy4CfyT953LfNgeUOXZGeIkV3KwPJJwlyug=";
|
||||
};
|
||||
WSOAuth = pkgs.applyPatches {
|
||||
src = pkgs.fetchFromGitHub {
|
||||
name = "WSOAuth";
|
||||
owner = "wikimedia";
|
||||
repo = "mediawiki-extensions-WSOAuth";
|
||||
rev = "4b8a12d26196b323be5da93480b6a75fda25b6d0";
|
||||
# Le SHA doit être changé à chaque nouveau commit de traduction.
|
||||
# Pas de meilleure solution à ma connaissance pour suivre les releases.
|
||||
sha256 = "sha256-W2SL14U5nVUcf/aTkWuVk9+vQGd3RCtuR3bAhBMN5iY=";
|
||||
};
|
||||
patches = [ "${./WSOAuth.patch}" ];
|
||||
};
|
||||
WSONoteKfetAuth = "${./WSONoteKfetAuth}";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{ pkgs, ... }:
|
||||
{ pkgs, config, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
|
|
@ -9,11 +9,77 @@
|
|||
networking.hostName = "nextcloud";
|
||||
boot.loader.grub.devices = [ "/dev/sda" ];
|
||||
|
||||
services.autofs = {
|
||||
services.autofs =
|
||||
let
|
||||
autoMaster = pkgs.writeScript "home-nextcloud" ''
|
||||
#!/usr/bin/env bash
|
||||
USER=$(echo $1 | sed "s/_[1-9]*$//")
|
||||
UHOME=/home-adh/$USER
|
||||
|
||||
USERID=$(ldapsearch -LLL -b "dc=crans,dc=org" -H ldap://172.16.10.157 -D "cn=admin,dc=crans,dc=org" -y ${config.age.secrets.nextcloud_ldap_pass.path} "uid=$USER" uidNumber | grep uidNumber | awk '{print $2}')
|
||||
UGROUP=$(ldapsearch -LLL -b "dc=crans,dc=org" -H ldap://172.16.10.157 -D "cn=admin,dc=crans,dc=org" -y ${config.age.secrets.nextcloud_ldap_pass.path} "uid=$USER" gidNumber | grep gidNumber | awk '{print $2}')
|
||||
|
||||
# On quitte si l'utilisateur $USER n'existe pas
|
||||
if [ -z "$USERID" ]; then
|
||||
logger -p local0.error -t autofs "user $USER n'existe pas"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Rafraîchi les stats du dossier
|
||||
ls ''\${UHOME}/NextCloud/ &>/dev/null || /bin/ls ''\${UHOME} &>/dev/null
|
||||
|
||||
# Création du lien symbolique Nextcloud s'il n'existe pas
|
||||
if [ ! -e "''\${UHOME}/NextCloud" ] || ! (
|
||||
find ''\${UHOME}/NextCloud/ -maxdepth 0 -user $USERID -group ''\${UGROUP} |
|
||||
grep -q ''\${UHOME}/NextCloud/
|
||||
); then
|
||||
if [ ! -e "''\${UHOME}/NextCloud" ]; then
|
||||
ln -fs OwnCloud ''\${UHOME}/NextCloud
|
||||
fi
|
||||
|
||||
if [ ! -e "''\${UHOME}/NextCloud" ] || ! (
|
||||
find ''\${UHOME}/NextCloud/ -maxdepth 0 -user $USERID -group ''\${UGROUP} |
|
||||
grep -q ''\${UHOME}/NextCloud/
|
||||
); then
|
||||
logger -p local0.error -t autofs "impossible de créer le lien symbolique ''\${UHOME}/NextCloud"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
logger -p local0.info -t autofs "Lien symbolique ''\${UHOME}/NextCloud -> ''\${UHOME}/OwnCloud créé"
|
||||
fi
|
||||
|
||||
# Création du dossier OwnCloud s'il n'existe pas ou n'appartient pas a l'utilisateur
|
||||
if [ ! -d "''\${UHOME}/OwnCloud" ] || ! (
|
||||
find ''\${UHOME}/OwnCloud/ -maxdepth 0 -user $USERID -group ''\${UGROUP} |
|
||||
grep -q ''\${UHOME}/OwnCloud/
|
||||
); then
|
||||
if [ ! -d "''\${UHOME}/OwnCloud" ]; then
|
||||
mkdir ''\${UHOME}/OwnCloud
|
||||
fi
|
||||
chmod 700 ''\${UHOME}/OwnCloud &&
|
||||
chown $USERID:''\${UGROUP} ''\${UHOME}/OwnCloud
|
||||
|
||||
if [ ! -d "''\${UHOME}/OwnCloud" ] || ! (
|
||||
find ''\${UHOME}/OwnCloud/ -maxdepth 0 -user $USERID -group ''\${UGROUP} |
|
||||
grep -q ''\${UHOME}/OwnCloud/
|
||||
); then
|
||||
logger -p local0.error -t autofs "impossible de créer le dossier ''\${UHOME}/OwnCloud"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
logger -p local0.info -t autofs "Dossier ''\${UHOME}/OwnCloud créé"
|
||||
fi
|
||||
|
||||
chmod 750 /home-nextcloud &&
|
||||
chown nextcloud:nextcloud /home-nextcloud &&
|
||||
echo "-fstype=fuse.bindfs,map=$USERID/nextcloud:@$UGROUP/@nextcloud,resolve-symlinks,allow_other :''\${UHOME}/NextCloud"
|
||||
'';
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
|
||||
autoMaster = ''
|
||||
/home-nextcloud /etc/auto.master.d/home-nextcloud.sh -t60
|
||||
/home-nextcloud ${autoMaster} -t60
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,15 +12,19 @@
|
|||
owner = "nextcloud";
|
||||
group = "nextcloud";
|
||||
};
|
||||
# Attention : sensible au retour à la ligne final (il n’en faut pas) !
|
||||
nextcloud_ldap_pass = {
|
||||
file = ../../../secrets/nextcloud/nextcloud_ldap_pass.age;
|
||||
};
|
||||
};
|
||||
|
||||
services.nextcloud = {
|
||||
enable = true;
|
||||
package = pkgs.nextcloud33;
|
||||
package = pkgs.nextcloud34;
|
||||
|
||||
configureRedis = true;
|
||||
hostName = "nextcloud.crans.org";
|
||||
https = false;
|
||||
https = true;
|
||||
|
||||
maxUploadSize = "4G";
|
||||
|
||||
|
|
@ -41,11 +45,26 @@
|
|||
trusted_proxies = [
|
||||
# hodaur
|
||||
"172.16.10.145"
|
||||
# reverseproxy
|
||||
"172.16.10.151"
|
||||
];
|
||||
};
|
||||
|
||||
mail_domain = "crans.org";
|
||||
mail_from_address = "root";
|
||||
mail_smtphost = "smtp.crans.org";
|
||||
mail_smtpport = 25;
|
||||
};
|
||||
|
||||
appstoreEnable = true;
|
||||
extraAppsEnable = true;
|
||||
};
|
||||
|
||||
# Activation CORS pour récuppérer les agendas depuis le wiki
|
||||
# TODO: à mettre que sur /remote.php/dav/public-calendars/ pour éviter des requêtes malveillantes depuis le wiki.
|
||||
services.nginx.virtualHosts.${config.services.nextcloud.hostName}.extraConfig = ''
|
||||
add_header 'Access-Control-Allow-Origin' 'https://mediawiki.crans.org' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET' always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'false' always;
|
||||
add_header 'Vary' 'Origin' always;
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
{ pkgs, ... }:
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
formatJSON = pkgs.formats.json { };
|
||||
|
|
@ -84,6 +88,11 @@ let
|
|||
path_regex = "^/[a-zA-Z0-9_-]*/public/.*";
|
||||
action = "ALLOW";
|
||||
}
|
||||
{
|
||||
name = "allow-phpmyadmin";
|
||||
path_regex = "^/phpmyadmin/.*";
|
||||
action = "ALLOW";
|
||||
}
|
||||
{
|
||||
# geneau utilisait ses pages persos avant la mise en place d'Anubis.
|
||||
# Le déploiement d'Anubis l'a impacté (cf les mails de fin 2025).
|
||||
|
|
@ -99,7 +108,7 @@ let
|
|||
}
|
||||
{
|
||||
name = "challenge-other";
|
||||
path_regex = "^*";
|
||||
path_regex = "^.*";
|
||||
action = "CHALLENGE";
|
||||
}
|
||||
];
|
||||
|
|
@ -128,28 +137,118 @@ let
|
|||
];
|
||||
};
|
||||
|
||||
anubisWiki = formatJSON.generate "anubis_wiki.json" {
|
||||
"bots" = [
|
||||
{
|
||||
import = "${antiBot}";
|
||||
}
|
||||
{
|
||||
name = "allow-mediawiki-images";
|
||||
path_regex = "^/w/images/.*";
|
||||
action = "ALLOW";
|
||||
}
|
||||
{
|
||||
name = "allow-mediawiki-api";
|
||||
path_regex = "^/w/api.php.*";
|
||||
action = "ALLOW";
|
||||
}
|
||||
{
|
||||
name = "challenge-other";
|
||||
path_regex = ".*";
|
||||
action = "CHALLENGE";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
fetchFromCrans =
|
||||
opts:
|
||||
pkgs.fetchFromGitLab (
|
||||
{
|
||||
domain = "gitlab.adm.crans.org";
|
||||
owner = "nounous";
|
||||
}
|
||||
// opts
|
||||
);
|
||||
|
||||
installPartySite = pkgs.python3Packages.buildPythonApplication {
|
||||
name = "site-install-party";
|
||||
pyproject = false;
|
||||
|
||||
src = fetchFromCrans {
|
||||
repo = "site-install-party";
|
||||
rev = "4dd5fdfe8e588f3ed4dd6966fc9b72234ecdba17";
|
||||
hash = "sha256-kiuiC99TMpLbefByUAm5hBskqKuTOa2YumKDB+jO7UY=";
|
||||
};
|
||||
|
||||
build-system = with pkgs.python3Packages; [
|
||||
mkdocs
|
||||
mkdocs-material
|
||||
];
|
||||
# On ne met pas directement dans $out pour éviter la fixupPhase
|
||||
buildPhase = ''
|
||||
python3 -m mkdocs -- build -d $out/site
|
||||
'';
|
||||
};
|
||||
|
||||
homepagePkg = pkgs.stdenv.mkDerivation {
|
||||
name = "site-homepage";
|
||||
|
||||
src = fetchFromCrans {
|
||||
repo = "homepage";
|
||||
rev = "a71446ad7c125241835190a4035159bc7be751bc";
|
||||
hash = "sha256-dAKJpBnGeDtv0UM8gXsdqUCzbliskRMYUEQ1KGCW1Cw=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
hugo
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
hugo build -d $out
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
crans = {
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
"autoconfig".serveLocalFiles = ./staticsites/autoconfig;
|
||||
"cas".proxyPass = "172.16.10.120";
|
||||
"collabora" = {
|
||||
target = "172.16.10.149";
|
||||
proxyPass = "172.16.10.149";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
"eclat" = {
|
||||
anubisConfig = "${anubisMirrors}";
|
||||
httpOnly = true;
|
||||
target = "172.16.10.104";
|
||||
proxyPass = "172.16.10.104";
|
||||
};
|
||||
"eclats" = {
|
||||
anubisConfig = "${anubisMirrors}";
|
||||
target = "172.16.10.104";
|
||||
proxyPass = "172.16.10.104";
|
||||
};
|
||||
"element".proxyPass = "172.16.10.118";
|
||||
"ethercalc" = {
|
||||
proxyPass = "172.16.10.133:8000";
|
||||
serverAliases = [ "excel" ];
|
||||
};
|
||||
"framadate".proxyPass = "172.16.10.109";
|
||||
"grafana" = {
|
||||
proxyPass = "172.16.10.121:3000";
|
||||
serverAliases = [ "fyre" ];
|
||||
};
|
||||
"imprimante" = {
|
||||
proxyPass = "172.16.10.131";
|
||||
serverAliases = [ "helloworld" ];
|
||||
};
|
||||
"intranet" = {
|
||||
proxyPass = "172.16.10.156";
|
||||
serverAliases = [ "re2o" ];
|
||||
};
|
||||
"install-party" = {
|
||||
anubisConfig = "${anubisChallenge}";
|
||||
target = "/var/www/install-party.crans.org";
|
||||
serveLocalFiles = "${installPartySite}/site";
|
||||
serverAliases = [
|
||||
"i-p"
|
||||
"adopteunmanchot"
|
||||
|
|
@ -158,38 +257,68 @@ in
|
|||
};
|
||||
"lists" = {
|
||||
anubisConfig = "${anubisChallenge}";
|
||||
target = "172.16.10.110";
|
||||
proxyPass = "172.16.10.110";
|
||||
};
|
||||
"mediawiki" = {
|
||||
anubisConfig = "${anubisChallenge}";
|
||||
target = "172.16.10.144";
|
||||
serverAliases = [
|
||||
"mediakiwi"
|
||||
];
|
||||
anubisConfig = "${anubisWiki}";
|
||||
proxyPass = "172.16.10.144";
|
||||
serverAliases = [ "mediakiwi" ];
|
||||
};
|
||||
"mirrors" = {
|
||||
anubisConfig = "${anubisMirrors}";
|
||||
target = "172.16.10.104";
|
||||
proxyPass = "172.16.10.104";
|
||||
};
|
||||
"mirror" = {
|
||||
anubisConfig = "${anubisMirrors}";
|
||||
httpOnly = true;
|
||||
target = "172.16.10.104";
|
||||
proxyPass = "172.16.10.104";
|
||||
};
|
||||
"nekorale".globalRedirect = "perso.crans.org/club-nekorale";
|
||||
"nextcloud".proxyPass = "172.16.10.146";
|
||||
"owncloud".proxyPass = "172.16.10.136";
|
||||
"pad".proxyPass = "172.16.10.130:9001";
|
||||
"pdf" = {
|
||||
proxyPass = "172.16.10.140";
|
||||
serverAliases = [
|
||||
"livre"
|
||||
"stirling"
|
||||
"stirling-pdf"
|
||||
];
|
||||
};
|
||||
"perso" = {
|
||||
anubisConfig = "${anubisPerso}";
|
||||
target = "172.16.10.31";
|
||||
serverAliases = [
|
||||
"clubs"
|
||||
];
|
||||
proxyPass = "172.16.10.31";
|
||||
serverAliases = [ "clubs" ];
|
||||
};
|
||||
"pot-vieux".globalRedirect = "perso.crans.org/club-vieux";
|
||||
"re2o-dev".proxyPass = "172.16.10.166";
|
||||
"services".serveLocalFiles = fetchFromCrans {
|
||||
repo = "services-page";
|
||||
rev = "e32ac21513d88e5123eb4138835453df64aa21d0";
|
||||
hash = "sha256-nc4IjA/wtj7WXnt0wZqGymGz/NR45STlMLbsTdnSzJ0=";
|
||||
};
|
||||
"snl".globalRedirect = "perso.crans.org/sonetlumens";
|
||||
"test-melon".proxyPass = "172.16.10.150:8080";
|
||||
"tmpad".proxyPass = "172.16.10.130:9002";
|
||||
"vaultwarden" = {
|
||||
proxyPass = "172.16.10.159";
|
||||
serverAliases = [ "pass" ];
|
||||
};
|
||||
"webirc".proxyPass = "172.16.10.31:9000";
|
||||
"webmail" = {
|
||||
proxyPass = "172.16.10.107";
|
||||
serverAliases = [ "roundcube" ];
|
||||
};
|
||||
"wiki" = {
|
||||
anubisConfig = "${anubisChallenge}";
|
||||
target = "172.16.10.161";
|
||||
serverAliases = [
|
||||
"wikipedia"
|
||||
];
|
||||
proxyPass = "172.16.10.161";
|
||||
serverAliases = [ "wikipedia" ];
|
||||
};
|
||||
"_crans-org" = {
|
||||
serveLocalFiles = homepagePkg;
|
||||
serverAliases = [ "www" ];
|
||||
};
|
||||
"zero".proxyPass = "172.16.10.130";
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -197,4 +326,14 @@ in
|
|||
acme.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Règle particulières
|
||||
services.nginx.virtualHosts = {
|
||||
"www-alias-crans.org".default = true;
|
||||
|
||||
"_crans-org".locations = {
|
||||
"/_matrix/".proxyPass = "https://matrix.crans.org/_matrix/";
|
||||
"/_synapse/".proxyPass = "https://matrix.crans.org/_synapse/";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
<clientConfig version="1.0">
|
||||
<emailProvider id="crans.org">
|
||||
<domain>crans.org</domain>
|
||||
<domain>crans.eu</domain>
|
||||
<domain>crans.fr</domain>
|
||||
<displayName>Cr@ns Mail</displayName>
|
||||
<displayShortName>Cr@ns</displayShortName>
|
||||
<incomingServer type="imap">
|
||||
<hostname>imap.crans.org</hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<username>%EMAILLOCALPART%</username>
|
||||
<authentication>plain</authentication>
|
||||
</incomingServer>
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>smtp.crans.org</hostname>
|
||||
<port>465</port>
|
||||
<socketType>SSL</socketType>
|
||||
<username>%EMAILLOCALPART%</username>
|
||||
<authentication>plain</authentication>
|
||||
</outgoingServer>
|
||||
</emailProvider>
|
||||
</clientConfig>
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
{ config, lib, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
self,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.crans.users;
|
||||
|
|
@ -34,6 +39,20 @@ in
|
|||
users = {
|
||||
mutableUsers = false;
|
||||
|
||||
motd =
|
||||
let
|
||||
shortRev = self.shortRev or self.dirtyShortRev or "inconnue";
|
||||
rev = self.rev or (lib.strings.removeSuffix "-dirty" (self.dirtyRev or "inconnue"));
|
||||
isDirty = !(self ? rev);
|
||||
in
|
||||
''
|
||||
${
|
||||
if isDirty then "!! DIRTY !! " else ""
|
||||
}Version déployée ${shortRev}, modifiée le ${self.lastModifiedDate or "??"}.
|
||||
|
||||
https://gitlab.crans.org/nounous/nixos/-/commit/${rev}
|
||||
'';
|
||||
|
||||
users.root = {
|
||||
hashedPasswordFile = config.age.secrets.root-passwd-hash.path;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,5 +2,4 @@
|
|||
|
||||
{
|
||||
services.qemuGuest.enable = true;
|
||||
boot.kernelParams = [ "console=ttyS0,115200" ];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{ lib, ... }:
|
||||
{ lib, self, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
|
|
@ -25,6 +25,9 @@
|
|||
cleanOnBoot = true;
|
||||
};
|
||||
|
||||
# Version de la flake pour nixos-version --configuration-revision
|
||||
system.configurationRevision = self.rev or self.dirtyRev or "inconnue";
|
||||
|
||||
# Recommandé depuis le passage à 26.05
|
||||
boot.zfs.forceImportAll = lib.mkDefault false;
|
||||
boot.zfs.forceImportRoot = lib.mkDefault false;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ in
|
|||
dnsPropagationCheck = false;
|
||||
};
|
||||
|
||||
# Certificat wildcard par défaut pour les services utilisant acme
|
||||
# TODO: À déplacer sur les VMs qui en ont besoin (e.g. coturn…).
|
||||
certs."crans.org" = {
|
||||
domain = "*.crans.org";
|
||||
dnsProvider = "rfc2136";
|
||||
|
|
|
|||
|
|
@ -6,5 +6,13 @@
|
|||
recommendedOptimisation = true;
|
||||
|
||||
clientMaxBodySize = "512M";
|
||||
|
||||
# On trust nos ip internes pour bien indiquer le client au bout
|
||||
# i.e. on considère pas le reverseproxy comme notre client
|
||||
appendHttpConfig = ''
|
||||
set_real_ip_from 172.16.0.0/16;
|
||||
set_real_ip_from fd00::/56;
|
||||
real_ip_header X-Real-Ip;
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
|
|
@ -8,28 +7,6 @@
|
|||
let
|
||||
cfg = config.crans.reverseProxy;
|
||||
|
||||
formatJSON = pkgs.formats.json { };
|
||||
|
||||
allowAll = formatJSON.generate "allow_all.json" {
|
||||
bots = [
|
||||
{
|
||||
name = "allow_all";
|
||||
path_regex = ".*";
|
||||
action = "ALLOW";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
open_graph = formatJSON.generate "opengraph.json" {
|
||||
openGraph = [
|
||||
{
|
||||
enabled = true;
|
||||
considerHost = true;
|
||||
ttl = "24h";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
mainTld = "org";
|
||||
otherTld = [
|
||||
"fr"
|
||||
|
|
@ -43,6 +20,8 @@ let
|
|||
mkOption
|
||||
types
|
||||
;
|
||||
|
||||
mkHostName = als: tld: if als == "_crans-org" then "crans.${tld}" else "${als}.crans.${tld}";
|
||||
in
|
||||
|
||||
{
|
||||
|
|
@ -65,18 +44,36 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
target = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
proxyPass = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Indique la destination. Il peut s'agir du chemin vers des fichiers statiques.
|
||||
Indique la destination à proxy.
|
||||
'';
|
||||
example = "172.16.10.128:8000";
|
||||
};
|
||||
|
||||
serveLocalFiles = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
Chemin vers un dossier à exposer statiquement.
|
||||
'';
|
||||
example = "/var/local/adopter-un-manchot";
|
||||
};
|
||||
|
||||
globalRedirect = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Si définie, toutes les requêtes sont redirigées (via 301) sur cet hôte.
|
||||
'';
|
||||
example = "perso.crans.org/club";
|
||||
};
|
||||
|
||||
anubisConfig = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Chemin du fichier de configuration
|
||||
'';
|
||||
|
|
@ -95,7 +92,7 @@ in
|
|||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Interdit les connexions en ssh
|
||||
Interdit les connexions en https
|
||||
'';
|
||||
example = "true";
|
||||
};
|
||||
|
|
@ -134,107 +131,206 @@ in
|
|||
wantedBy = [ "multi-user.target" ];
|
||||
}) cfg.virtualHosts;
|
||||
|
||||
# On fait un certificat commun pour tous les hosts
|
||||
security.acme.certs."all.crans.all" = mkIf cfg.enable {
|
||||
domain = "*.crans.${mainTld}";
|
||||
dnsProvider = "rfc2136";
|
||||
# Contient le serveur à contacter avec le protocole
|
||||
# et le mot de passe
|
||||
environmentFile = config.age.secrets.acme-env.path;
|
||||
extraDomainNames =
|
||||
lib.concatMap (tld: [
|
||||
"*.crans.${tld}"
|
||||
"crans.${tld}"
|
||||
]) otherTld
|
||||
++ [
|
||||
"crans.${mainTld}"
|
||||
];
|
||||
group = config.services.nginx.group;
|
||||
};
|
||||
|
||||
# Création des dossiers de logs
|
||||
system.activationScripts = {
|
||||
nginx-logs = {
|
||||
text =
|
||||
lib.concatMapStrings (f: "mkdir -p /var/log/nginx/${f}/\n") (
|
||||
[ "anubis" ] ++ (lib.map (hn: "host/${hn}") (builtins.attrNames cfg.virtualHosts))
|
||||
)
|
||||
+ "chown -R ${config.services.nginx.user}:${config.services.nginx.group} /var/log/nginx/\n";
|
||||
};
|
||||
};
|
||||
|
||||
age.secrets.anubis-secret = mkIf cfg.enable {
|
||||
file = ../../secrets/reverseproxy/anubis-secret.age;
|
||||
owner = "anubis";
|
||||
};
|
||||
|
||||
services = mkIf cfg.enable {
|
||||
anubis = {
|
||||
defaultOptions.group = "nginx";
|
||||
instances = lib.mapAttrs (vhostName: vhostConfig: {
|
||||
instances = lib.mapAttrs (
|
||||
vhostName: vhostConfig:
|
||||
let
|
||||
hostname = mkHostName vhostName mainTld;
|
||||
in
|
||||
mkIf (vhostConfig.anubisConfig != null) {
|
||||
enable = true;
|
||||
settings = {
|
||||
BIND = "/run/anubis/anubis-${vhostName}/socket.sock";
|
||||
BIND_NETWORK = "unix";
|
||||
METRICS_BIND = "/run/anubis/anubis-${vhostName}/anubis-${vhostName}-metrics.sock";
|
||||
TARGET = "unix:///run/nginx/nginx-${vhostName}.sock";
|
||||
COOKIE_DOMAIN = "crans.org";
|
||||
REDIRECT_DOMAINS = "${vhostName}.crans.org";
|
||||
COOKIE_DOMAIN = hostname;
|
||||
# TODO: db pour mettre en commun les token.
|
||||
# Transitoirement on les sépare
|
||||
COOKIE_PREFIX = "anubis-${vhostName}";
|
||||
REDIRECT_DOMAINS = hostname;
|
||||
SOCKET_MODE = "0660";
|
||||
# Secret partagé entre les instances
|
||||
ED25519_PRIVATE_KEY_HEX_FILE = config.age.secrets.anubis-secret.path;
|
||||
# OpenGraph config
|
||||
OG_PASSTHROUGH = vhostConfig.anubisOpenGraph;
|
||||
OG_EXPIRY_TIME = "24h";
|
||||
OG_CACHE_CONSIDER_HOST = true;
|
||||
# Policy config
|
||||
POLICY_FNAME = if (vhostConfig.anubisConfig == "") then "${allowAll}" else vhostConfig.anubisConfig;
|
||||
POLICY_FNAME = vhostConfig.anubisConfig;
|
||||
};
|
||||
}
|
||||
) cfg.virtualHosts;
|
||||
};
|
||||
|
||||
logrotate.settings =
|
||||
let
|
||||
# https://github.com/NixOS/nixpkgs/blob/nixos-26.05/nixos/modules/services/web-servers/nginx/default.nix
|
||||
base = {
|
||||
su = "${config.services.nginx.user} ${config.services.nginx.group}";
|
||||
postrotate = "[ ! -f /var/run/nginx/nginx.pid ] || kill -USR1 `cat /var/run/nginx/nginx.pid`";
|
||||
frequency = "daily";
|
||||
ifempty = true;
|
||||
compress = true;
|
||||
delaycompress = true;
|
||||
dateyesterday = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
nginx-anubis = base // {
|
||||
files = [ "/var/log/nginx/anubis/*.log" ];
|
||||
rotate = 7; # une semaine de log pour débugguer
|
||||
};
|
||||
nginx-hosts = base // {
|
||||
files = [ "/var/log/nginx/host/*/*.log" ];
|
||||
rotate = 173; # 6 mois de logs
|
||||
dateext = true;
|
||||
dateformat = "%Y-%m-%d";
|
||||
};
|
||||
}) cfg.virtualHosts;
|
||||
};
|
||||
|
||||
nginx =
|
||||
let
|
||||
# Configuration du serveur principal.
|
||||
mainConfig = lib.mapAttrs' (
|
||||
configVhost =
|
||||
vhostName: vhostConfig:
|
||||
lib.nameValuePair (vhostName + "-anubis") {
|
||||
enableACME = !vhostConfig.httpOnly;
|
||||
let
|
||||
hostname = mkHostName vhostName mainTld;
|
||||
|
||||
sslConf = {
|
||||
forceSSL = !vhostConfig.httpOnly;
|
||||
rejectSSL = vhostConfig.httpOnly;
|
||||
useACMEHost = mkIf (!vhostConfig.httpOnly) "all.crans.all";
|
||||
acmeRoot = null;
|
||||
};
|
||||
|
||||
# Conf pour l’hôte en entrée
|
||||
entryExtraConf = {
|
||||
extraConfig = ''
|
||||
set_real_ip_from 172.16.0.0/16;
|
||||
set_real_ip_from fd00::/56;
|
||||
real_ip_header X-Real-Ip;
|
||||
'';
|
||||
}
|
||||
// sslConf;
|
||||
# Conf supplémentaire pour le proxy principal
|
||||
vhostExtraConf =
|
||||
if vhostConfig.anubisConfig != null then
|
||||
# Il reçoit les requêtes d’Anubis
|
||||
{
|
||||
listen = [
|
||||
{ addr = "unix:/run/nginx/nginx-${vhostName}.sock"; }
|
||||
];
|
||||
extraConfig = ''
|
||||
set_real_ip_from unix:;
|
||||
real_ip_header X-Real-IP;
|
||||
'';
|
||||
}
|
||||
else
|
||||
# Il est en entrée
|
||||
entryExtraConf;
|
||||
|
||||
logHostConf = ''
|
||||
access_log /var/log/nginx/host/${vhostName}/access.log;
|
||||
error_log /var/log/nginx/host/${vhostName}/error.log;
|
||||
'';
|
||||
|
||||
# Les alias : vhostName × otherTld U serverAliases × allTld
|
||||
aliases =
|
||||
lib.foldr
|
||||
(
|
||||
tld: acc:
|
||||
acc
|
||||
++ (lib.foldr (alias: acc: acc ++ [ (mkHostName alias tld) ]) [
|
||||
(mkHostName vhostName tld)
|
||||
] vhostConfig.serverAliases)
|
||||
)
|
||||
(lib.foldr (alias: acc: acc ++ [ (mkHostName alias mainTld) ]) [ ] vhostConfig.serverAliases)
|
||||
otherTld;
|
||||
in
|
||||
{
|
||||
# Configuration du service à proxy.
|
||||
"${vhostName}" = vhostExtraConf // {
|
||||
serverName = hostname;
|
||||
locations."/" = mkIf (vhostConfig.proxyPass != null) {
|
||||
proxyPass = "http://${vhostConfig.proxyPass}";
|
||||
proxyWebsockets = vhostConfig.proxyWebsockets;
|
||||
};
|
||||
root = vhostConfig.serveLocalFiles;
|
||||
globalRedirect = vhostConfig.globalRedirect;
|
||||
extraConfig = vhostExtraConf.extraConfig + logHostConf;
|
||||
};
|
||||
|
||||
# Entrée d’Anubis
|
||||
"${vhostName}-anubis" = mkIf (vhostConfig.anubisConfig != null) (
|
||||
entryExtraConf
|
||||
// rec {
|
||||
serverName = hostname;
|
||||
locations."/" = {
|
||||
proxyPass = "http://unix:/run/anubis/anubis-${vhostName}/socket.sock";
|
||||
proxyWebsockets = vhostConfig.proxyWebsockets;
|
||||
};
|
||||
serverName = "${vhostName}.crans.${mainTld}";
|
||||
extraConfig = "
|
||||
set_real_ip_from 172.16.0.0/16;
|
||||
set_real_ip_from fd00::/56;
|
||||
real_ip_header X-Real-Ip;
|
||||
";
|
||||
locations."/.within.website/x/cmd/anubis/api/honeypot/" = locations."/" // {
|
||||
extraConfig = ''
|
||||
add_header 'X-Robots-Tag' 'noindex,nofollow' always;
|
||||
'';
|
||||
};
|
||||
extraConfig = entryExtraConf.extraConfig + ''
|
||||
access_log /var/log/nginx/anubis/access.log;
|
||||
error_log /var/log/nginx/anubis/error.log;
|
||||
'';
|
||||
}
|
||||
) cfg.virtualHosts;
|
||||
|
||||
# Redirections
|
||||
redirectConfig = lib.mapAttrs (vhostName: vhostConfig: {
|
||||
# Redirection vers d'autres machines
|
||||
locations = mkIf (!lib.strings.hasPrefix "/" vhostConfig.target) {
|
||||
"/favicon.ico".root = "/var/www/logo/";
|
||||
"/" = {
|
||||
proxyPass = "http://${vhostConfig.target}";
|
||||
proxyWebsockets = vhostConfig.proxyWebsockets;
|
||||
};
|
||||
};
|
||||
# Redirection vers des fichiers locaux
|
||||
root = mkIf (lib.strings.hasPrefix "/" vhostConfig.target) vhostConfig.target;
|
||||
listen = [
|
||||
{ addr = "unix:/run/nginx/nginx-${vhostName}.sock"; }
|
||||
];
|
||||
serverName = "${vhostName}.crans.${mainTld}";
|
||||
extraConfig = "
|
||||
set_real_ip_from unix:;
|
||||
real_ip_header X-Real-IP;
|
||||
";
|
||||
}) cfg.virtualHosts;
|
||||
|
||||
# Génération des alias
|
||||
getAliases = name: config: lib.foldr (
|
||||
tld: acc:
|
||||
acc
|
||||
++
|
||||
(lib.foldr (alias: acc: acc ++ ["${alias}.crans.${tld}"]) ["${name}.crans.${tld}"] config.serverAliases)
|
||||
)
|
||||
(lib.foldr (alias: acc: acc ++ ["${alias}.crans.${mainTld}"]) [] config.serverAliases)
|
||||
otherTld;
|
||||
|
||||
# Configuration des alias
|
||||
aliasConfig = lib.foldr (
|
||||
vhost: acc:
|
||||
acc
|
||||
//
|
||||
lib.foldr (
|
||||
alias: acc:
|
||||
acc
|
||||
//
|
||||
{
|
||||
"${vhost.name}-alias-${alias}" = rec {
|
||||
rejectSSL = vhost.value.httpOnly;
|
||||
forceSSL = !rejectSSL;
|
||||
enableACME = !rejectSSL;
|
||||
);
|
||||
}
|
||||
# Les alias
|
||||
// lib.mergeAttrsList (
|
||||
lib.map (alias: {
|
||||
"${vhostName}-alias-${alias}" = sslConf // {
|
||||
serverName = alias;
|
||||
globalRedirect = "${vhost.name}.crans.${mainTld}";
|
||||
globalRedirect = lib.defaultTo hostname vhostConfig.globalRedirect;
|
||||
extraConfig = logHostConf;
|
||||
};
|
||||
}
|
||||
) { } (getAliases vhost.name vhost.value)
|
||||
) { } (lib.attrsToList cfg.virtualHosts);
|
||||
}) aliases
|
||||
);
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
virtualHosts = redirectConfig // aliasConfig // mainConfig;
|
||||
virtualHosts = lib.concatMapAttrs configVhost cfg.virtualHosts;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ let
|
|||
collabora = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFa2D9fREtO2r2oIx6q9JAKFUHtxGbgEPMjkx09DQSU8 root@collabora";
|
||||
jitsi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB6jVMIZ5y2oXX9HOkw7r5UUjw95MlFaFuu7FnEC0Q8z root@jitsi";
|
||||
livre = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEVfKNokHG6ig32hhQxTep+fKFmKahlDClPrX/dP4/gb root@livre";
|
||||
mediakiwi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFfcKtim7Aj+strHdg847D7KCCdoOAuasxx/2yi2TY1n root@mediakiwi";
|
||||
neo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMGfSvxqC2PJYRrxJaivVDujwlwCZ6AwH8hOSA9ktZ1V root@neo";
|
||||
nextcloud = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMgSP9UmuJw8Bi2ML07WHsWvxN8akkc9XZxXyOgdjXkq root@nextcloud";
|
||||
periodique = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHTdfSIL3AWIv0mjRDam6E/qsjoqwJ8QSm1Cb0xqs1s1 root@periodique";
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,33 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 ZpDcxw Z/XNMwkMr7AXaMpDc/Q7ASWWwv/ID5pRpSUyx66R+2Y
|
||||
8IWRR7cQVrrBFwAg0zHnbliWZWDKsa2PmdxHZsCFQLM
|
||||
-> piv-p256 ewCc3w AkDTktwBj1dnAA63otR9BhIAyjcPoSww4G8/cP1zSX+j
|
||||
i+ftraOs9yZ51gUumFA+zJZzgAPRtVyuPfUsA0hlXnI
|
||||
-> piv-p256 6CL/Pw AsD6ZBVGl8HU3FQKnuskPk8qFHPgxK+9adJGuuzik88c
|
||||
11sTCEgccvVjS29h2j3d7PwNZsdPqf4Y/9tagHbE2HM
|
||||
-> ssh-ed25519 eOAUSg wdE8dGVe35YXCdpXfko+lLsJTgJUmal7OSII3r4Somw
|
||||
nXbdTQ6KrGbb/FMSiELLuQexNut8qefOIcGyocbYbiA
|
||||
-> ssh-rsa REaZBA
|
||||
S8twRuHh+Oety2nRkiOyugu5ZgCaaoapLe4UF9CNCy0ldX87t+Y2va/WdAB2q/UU
|
||||
evMdN8MOplM8UxkQy69I2BlT26M9QktFuo90ktXK35DrFwiaP8a4aKOyiKzXFdcZ
|
||||
h4F7Af//lMlcwLYFARbAuXTIj5w8/Hd9IvhAbFoqtXo5+a2n6MgHdc5LvlLSjf9v
|
||||
aP7+01jGcT2lSbmw8YS8+caYE3LVlUh2ThTeFQKYWm+nE78IP19/TOabmNnx8eBX
|
||||
LtJggwnoNPrwmn53fn1tADI1+6QoIpTIrZOZLhAKnzDc5oasVNyjAY9NGC/U6A8e
|
||||
9eaPn3KmIg6QBg8hXUCLH4JeGHFU0hIxCeLotoh1F1HEi+CY3tPuylpny08/J3AJ
|
||||
i3+RvguyuJ3M5zwoMDvmavPw5mcV9tvWYhUSAWd6iZ6DIN1AHFsXQFXQuRn4P+3h
|
||||
v9faikTZrt/sv4ZQgWKNoTJ6CDxcAnhDkTtlgG4afjreFvRuB7esQz8v+G6BhUcE
|
||||
|
||||
-> ssh-ed25519 J/iReg aY/7+a5eakXhTJcsTR6ES7dTNBbmgh5riO0fhHzN4V8
|
||||
g4c9HRqdnw8oW3XZaDzUGl8FiVRe/Zwf/uewt8Henlk
|
||||
-> ssh-ed25519 GNhSGw pBleL7cbEZQhQRPP6wSLIJ9SmU7xCjdanDzQvzVBuUg
|
||||
d7QKEOygw4M8ne+iFiU9DxOwBGTC9JNS/2UPx9zjLXs
|
||||
-> ssh-ed25519 eXMAtA PRMzXld8kf3oLoqORVVeO0THG2g6as9vTwGhznQPZBo
|
||||
i+Ea7CfolHGjvoDuIeHl4bCF20718pV7WrlES+Sf2bE
|
||||
-> ssh-ed25519 5hXocQ 0e9eAjPAQee/VZvL2PYm9RgoVBnkOgI/y0pblLvoRW4
|
||||
PBfnrfOakv2ACKrDYs/to1zVaYldLiLldaUxn3GY/5k
|
||||
-> ssh-ed25519 bRHVVA gX62eG+wJesfSBd4lXzTqNMI+FjPl6fnztXygR8HDx0
|
||||
mML+owf1iLKGT9ZZkMUu2/l10eSPZEl5TwyAftOOyDA
|
||||
-> ssh-ed25519 HgW9eA 3Qp1NEa89r/JsT4GKnqDPaTmJaEI1on9D5n5I84DzhE
|
||||
hH9Kf6l1s1R3zT69iIXmKEB0qPE/i850WMpfb2lk3hE
|
||||
--- AAABc4YYLcIgv6zjThJ9BhOkR24VZ4wMV+LPR9yTtOM
|
||||
¢Bá.áZñ§—3PÀ5¸ÇwÔ!™)Ñ<>}Ú[UèB™ö±ÛråNØÚI^
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 ZpDcxw EYeeHlk7G0Ce8Zjz7DOA6umVE58ZNeRb1ao86/romSA
|
||||
iCLH6+fsNCeM5heK2QFh9kEY44BiDOQ8um4MCNUDeuI
|
||||
-> piv-p256 ewCc3w A+QtLSWeA4j0756rlETtyvJRiZR3y70+UMs/X1/u0fqq
|
||||
FslsT1yQ+NNHK8m/eBLWeNcsPu2BnubNO1h/AO95UxY
|
||||
-> piv-p256 6CL/Pw AxHghSnLJmHJz9rXlP1IQDoigFTkFUQiE7h3c0EMeLsc
|
||||
bmI1TbflKxJ6Axg/TgDEtCV0pw7tY6V/pMl4CJGjvL8
|
||||
-> ssh-ed25519 eOAUSg TPhT5qmh2Y9b0eBa41uTdIDdZEcDZtR6tEV8Jo1MuCk
|
||||
gBD5CQlnxMYohS13RvzoDDQeGDhYOEcS18Xv1S/5f+M
|
||||
-> ssh-rsa REaZBA
|
||||
NtWXPRN/qPJ5XUZNXt64Rt3brMfbXxP2ARvXo3wpnB0ed0QPnX29OTyVm/O2VNrY
|
||||
5jPouFaxWwDBuwyJ/HyJJB5SdrE188u7du/M0Z8l1/H8i2lpkp98XS64AEXkvrHo
|
||||
oWFsRloCWyeIU3m1MtUdqu/c3pnqyiE1SlCnajb6MUUrhDe2CdEEgJxFsIyMwXpW
|
||||
mkwtHxe9ngVebRLY3vw9OVhtqrKeKrNvSGGCE+IFn6kWuNoGD/rDI52kx76sNa0D
|
||||
531FFAdsiPt1VKZi0YBaUfRP407sc6rw8R258jFCeYpa58Zud62BWsT0d464gPkD
|
||||
tJQUZecCPRW0jflpVWzqZHIVurtp3Dqjp/oovmMMJ303/ywL0VdZR6PeMndGOkf+
|
||||
ame4zqTwOyQoxSkU+62D/KeSzM0QlVqAZhL+lMfYkFCYizZYQZSlLAHi7nF3704W
|
||||
S+nrv+FNNxLYT6zlq0hHokP9fIfUct/eFfMmV2ff9E77Tow/79TpgQnfxxdtJWJG
|
||||
|
||||
-> ssh-ed25519 J/iReg ZAxHEQjYmqQkMFth5Lp344f0JqwfGh4CO6DEG+uMmys
|
||||
qlLlf9eLNNw3/o++6LVpy2Anj3TWkNbcJoJ/rP4JKsI
|
||||
-> ssh-ed25519 GNhSGw 44kEvp1o41BaNRwFnAjKB4K3otIYhN7QN9FgCd+eSQ4
|
||||
fqZM/JCIderSDDrxckv4VLZu64Dfx275UVmfKsozpKU
|
||||
-> ssh-ed25519 eXMAtA mPn1SPpt7RkhMBLqUZ4mWGvqNeGtUN+pcA9jjKKGPGU
|
||||
bGyck1ZRHRlzk+HVmL3e99MiVFxjTuC7v/6WHjRjH8A
|
||||
-> ssh-ed25519 5hXocQ bT0MsqVcYEcB/ckkuAC0zbH5+pfs4iPso9MHhVUyx2U
|
||||
jZQOFmGWnZMtdTS5ePKIvnQFiOVsZTC/A09ZmXzveho
|
||||
-> ssh-ed25519 bRHVVA NSnH4xuAWnDgUIpi+csZOvq0K6u99Dt1IUGDrzZGcgQ
|
||||
L9WOetohzpDPAcF/1jGPmWnBhazSDyIU/0or+il2/ug
|
||||
-> ssh-ed25519 HgW9eA LdB9v8WGmExJffyQa8KuudNr97nyxshWj9j0YMDJKlI
|
||||
0BCRYHWmulVjwtlYUR2Xi4rdnLSRQTLL2jn8N5rubSo
|
||||
--- y4i6KM8sEq07Sx594lC7WX9hEtq9ce4Vm9tTU8loJtc
|
||||
2Õ‹r};‰„Ï,"¾èÃvß&êÀÂd¬¼}™Ž×8Uê_¼¨VY¸µÅn§h€ZúħÈgê`Nf`
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,33 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 APVFfA Z+0daNofon4xgikMa0TUQjUgp9jPPrQkOZJuEJDUmzM
|
||||
l/QeEQvggQtfgyrDcs9lRpvuFMEgn9i/wQFpuaVoAZ0
|
||||
-> piv-p256 ewCc3w AzbC2uqqhtetX1z1XlyVxexejzk8tvPNi/3rcGgOyMSZ
|
||||
cpl1dYYvqgkXbpvy0JcVabP7Ftb7cGY+IahV/yyDbhs
|
||||
-> piv-p256 6CL/Pw AixE5HedgCUVyin52g5YHUQFKaSr7hlF9LfsmbQPMf1l
|
||||
loUpO6pz/Qv+F0mSefpCP3IeDeJ/pq5hYlzacdAtlo4
|
||||
-> ssh-ed25519 eOAUSg b49cZWhT7+NLJlrH9EyWu6GPLkST9/T5BGU1azdQoBI
|
||||
dhzYN+L3k8CZRMbVgff1ofqlImyuD19PUGhhsxTHaMY
|
||||
-> ssh-rsa REaZBA
|
||||
CWiPTj/40DTjN2GWUsR92frMYiBP7tfSVADQ/+GCapOMNJD91MGxaiK0IocbJr0P
|
||||
KpOjfdjRDG8KIYszNgaS8XkHn/CUj2KsnRV2WBg3HWSrMdD4quoOxKWbFaU3b38C
|
||||
4mUK8gIkjbJFWFMSTOhAexHGYSRLl7fG/0VmRvc2ibs5CahbPsMbFDKuto7ApYhY
|
||||
e/t5SsYgIkbrNpeTLolO943YCIlU/JVP5XOuEDk1ZlKPRc+igpgqFjsD3LsV56Rr
|
||||
LFrjp4i5wJeA72HeH/S7nP96sAwxyLDHQrWhw3ntM0n2SRRVCVmLqNNOebUL59gy
|
||||
HRHHoZosepmHzMEOHQ+CrIxuYMiUQ/AyMkZmMTjxQsLniakJWI06G54f1+aaA0F9
|
||||
BhwFateWkLBNdkSdwqJpBAzh427yetzp/bac8dcTv45ylgsvCVSDpoew5NLoIuh5
|
||||
I/54NC6Pr5BF0LOW7b2fR9pzk7Rv6hnSxTbeyATeMhh7nTjdcFalUI3Bd1P3RL+i
|
||||
|
||||
-> ssh-ed25519 J/iReg wbkKYkdu/cxnZNEGXH0a5R5jV6cQlMpWFMdQiUpWTiQ
|
||||
cW3rZQMYrzhx93lsxoPhA8AVQLygKytSue3eEF/yxcc
|
||||
-> ssh-ed25519 GNhSGw Hd4AOis8kPKDKrOgRZNj9pG0bHFHgLfJwo6sd1/dSFg
|
||||
07UA5cZbEpf3pA7i04CDX6m9FZF5ahAorfp5GzXJSS4
|
||||
-> ssh-ed25519 eXMAtA hZ278rlg0u1Bp8rkEQGFagLYg3lkXK3uSolnUbIy0Bo
|
||||
k2sPaUhiCCaBUogPxrUk3Fm386DKuR7UBrGMAteykps
|
||||
-> ssh-ed25519 5hXocQ sYQmrGm4eQlgE4Lkn+ZFE4uI6czxM46SMpQHFRedtQU
|
||||
cIFA9Vk091Q5hFUUrjf6PRr/WS7XN1BfgpSOZN1m0xI
|
||||
-> ssh-ed25519 bRHVVA 2n976nXhvJzoFjP+JFe6L1rmCEmTr4YwhTR1rqyA1y4
|
||||
/X27rYKrHUB0Jltxc190yPXCiHlOXX3QEXM52BnON60
|
||||
-> ssh-ed25519 HgW9eA nNf/0O1vh6+uv5i1+HEy6MgF3VUImMru8BFCcJY7R1s
|
||||
fPSK5LcFR6WP25nFvJRs9RatiJ4t/LvIFYqdoOYxu/k
|
||||
--- QKKNQqbCPvtps4QlI3ric0JdAcdJ9g3QkTPj+RUGAis
|
||||
[xqZ†ÿ¥ªSD’ˆcO
ÌfkŠÖý”N¹±›;<3B>H]IòÉ£ÖùÎA‘€A
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 ZpDcxw yQPK+1aqMhup98+6GN/2i/BvUc7pmZZD4Narkdlg2A8
|
||||
GTub9L+DeO3Jc/FF5RSwuSutXpirNHA/tozS4beSpEs
|
||||
-> piv-p256 ewCc3w A4Lc20pRdb/qBbrBOmz5kxxzaz0efzEKy8xIA8uS5KAu
|
||||
j7apD2Xca3EH8bWAz0uzKmic38+P1yje7FrLy80vb5Y
|
||||
-> piv-p256 6CL/Pw AootwQpQE58CZZqPbs1/MlyfG6tf/uVQSMz1QKRjJfbZ
|
||||
AlvdwLqgz+N3GkAMPL4WURg5QPL9i8W6ySiS+r4SjpU
|
||||
-> ssh-ed25519 eOAUSg LXj/21Dw32+b0mudgaJWzYywsjEwvITr5FNsIhCgaUc
|
||||
h8sjcz1vppK+Dg3vo2APZ0jd9xHrvdXQherpd0TSNdU
|
||||
-> ssh-rsa REaZBA
|
||||
am1cDikdZtSGm7FNtAwq/+hu4FtcVQTrMUPh4tRPlZ4iThra0BHgm4NLZny06Fuc
|
||||
2crbyM4b2TWBK6KNCBTyGDTViuavP3QgFAPW2IEX/g7sFWqEZfGk9I01QZnnpT6x
|
||||
phyNR+/3UsDEAt/6Bn0CmIiXZzPJ2uQ4CEsvK3HW9632GnnUZp6wMDgHCa7XNViL
|
||||
l6GkzBg3Kp7/eI53F5wlOQ0UYj8fXeug6ZUr2wc2KT3AnBnKpYNyhO5QN0a/iKha
|
||||
1uIpV0qi6Q3QctmItSUwiDi1G6LwV56ZStvrJHBJxTJKF0LmWonPNalfNORhH8wU
|
||||
um9YLSYOZd3n/5vPY+odUP1sZBypF5VfrEUWagu/Dosn2REpo2HwXauc4rkKOLu7
|
||||
ruZqcScgEnU3c2PI1ZYvSdpoqSh/eaMv6EFhk0qLfVMXHioIgBmdFV6j+3UUr/AK
|
||||
ZaCic7P6eIzVohnyuZrk9waOCb6xGmaC5tz+7hO6iWaFHXYBuiZ0G5VgZWWvsn+b
|
||||
|
||||
-> ssh-ed25519 J/iReg rDz6s3YT+m0tuqBQAnV7CfIjojJK4OGc/yr4Ku79m3M
|
||||
uukWbHyK5U0RNgtnUs/OB0eAUYsH0+gOuvEme056DfY
|
||||
-> ssh-ed25519 GNhSGw BYacYjqeKRGu4Ohx+6ud8f2fmnyJgKg8Now3jiSYSzY
|
||||
pqOaR6QGXHoS0FmNDNR+egeqvQDQvWoJzUh9kMm/ZyI
|
||||
-> ssh-ed25519 eXMAtA xAjxD71MvThN7+wn9oaBPNv7X7puYYOaRBXT4rWhn34
|
||||
s2WpYf3FCYmG04LvuOHTZ8Zl1UQxgDV5qK/CFJwmLhs
|
||||
-> ssh-ed25519 5hXocQ 55mFRg0FGkbOzuHl4CikB74sFHKzSeC9oxlPp6byr1k
|
||||
KwjBEvz8f/vHDadPg1uNAUuzbHaGIS23nXx9sltrOtk
|
||||
-> ssh-ed25519 bRHVVA CikC77O5cpo+Da1Jvn5d7NFmmlgoUwO1oIBHs2wKbi0
|
||||
Q1Szf2jgP2IxSmU+N5deixEsduD69ERrYFLTMdmxerc
|
||||
-> ssh-ed25519 HgW9eA lq09SPlqq5znIlAB5l88U5uz20mXbHk5wltSMhJteBw
|
||||
dJpOifpANfZdzNt25UKyEB2ua03LKJ4uwO62ue4mw2w
|
||||
--- U13fjXS4HiYErtXV3/nucPnvTCgToNFJJr7jEH8IOME
|
||||
øL[|ÏUÜY
|
||||
²r
|
||||
Cbº¤BY"ó %‚©[Ã\_ÿÑß÷9f7ABñ×c“3I¹ëöÅäB
|
||||
ýÌy8ìÕ#p“`Ï0ñ7‰ý¢×Já£{ð<±u'ôÁ{æGä4˜EÕ1<C395>þTÈÅŸÖT@Ÿ*¦\÷ƒÄ„í2¥QñÈõý#[…½KVÓVì,‘ý3Å2±ÒÉZ‚æÖ±ºÍJí‡ûÜEÔm˜à—3ÖPësª¸ëcG'wˆÃü±³–ŽcDôC®*Ÿq³Î4ûDGr‰¨‚(´ZõgnˆÅ*¥WxkŽlQÃ<51>{Áxz9ÕÛß Rʸpï0ëºZ7cÑž‚Wœmt<6D>º·pMb(OÂ
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 ZpDcxw VzJJsefMGzLbGcbVyZON0M590x1gM7P8wwbafDVN1hE
|
||||
isRye6kXQbR2LmJI9TM6PDgN+K6P+rmP0iviY7GwF8w
|
||||
-> piv-p256 ewCc3w AlLQPdPUU6Gn6S+ZilzoYqaj/YA3BaUp6YQNxED/rw+2
|
||||
D291xkVjKPDl12DckPiW/A+VuxZwJRLOwBWM8vIJsFs
|
||||
-> piv-p256 6CL/Pw AmWfIbAU1QtaSq2dWnwkr0wr2QIcoAruvJjP5cNWSc34
|
||||
dPPOOCeMJKLc39qcXj+de0/AEJH/wR/hTMHK9oOYIgQ
|
||||
-> ssh-ed25519 eOAUSg v17y+5tQDF0EYj7ciY/qO22saHBPYyHVvffyfcckSC0
|
||||
TZyAvWfn0YcUS9G6dZluOzORhwaNG++1+v1k5tACrQE
|
||||
-> ssh-rsa REaZBA
|
||||
EOyTVfvxTMordC3FoHOnlFQF5+7iLk+XntDZan6l8Cfo+Z+8+CdOVy3mNnQqrPI8
|
||||
RVL/QTQh6oWw5ujGVVZNa7JFGUpqMbhE6RCDcL/Ho5t7VRylnmOEjyWSG/HcYw25
|
||||
BfTJaaSTbjI6dphBjzniRHNCps/AplHuuTp5TsCl9h/wxQdQ5kF/62sme/iSqupy
|
||||
8ed5PZueaqgBm0bSOENcV6u4YWShDjsjAIo8kXLAUtGEsMUao6B3Igtpxz09kgb9
|
||||
P46Kyt135Dp0d1YHVgdCXFrux56+3sJ9qTPylD93I+cGn74K2tyrtf11Atiju72m
|
||||
CO89ovzQZXydgFCr5E+YAkypnEElDS4UVGqUKkMeezmM8kUdLQNlwIo2ikgMR+bi
|
||||
Ypj1jtk1EJWzRJbXTy7lS4VV8ZRZHuxEQVeiibWujMCP3TAD/yIsw9ifvr58uapk
|
||||
AgiW7kE6MChQ844dE6H67JPG5z5xcn2ONLJWyBPB/ACGSiuDLX9joxt/xLlMBwGD
|
||||
|
||||
-> ssh-ed25519 J/iReg HxEnTdqslHnj6GI59srB5XPsEkRUp9+y+R/BFVxp31k
|
||||
/Ol2xcDvvd1vVy8iO+gLwg6LrN664VJEwnMzjq9hOTI
|
||||
-> ssh-ed25519 GNhSGw J+/GbL5u5CP3+OTqxT8ZRmAjNfwg5q4RpaUkfrQK9SA
|
||||
ylJls9MxM7+4FSP0GIn6Ll8LQNBwLM6JyQovFqePJ5c
|
||||
-> ssh-ed25519 eXMAtA C/hYcW6GFh98Y/nfVXfSFXqedI990mdwYyYeyghoQRM
|
||||
G2wFbOsQu25NZPBc0Q3NkA6xQXrrEH6eeggnGfdKwwU
|
||||
-> ssh-ed25519 5hXocQ pdViAG8/pAB6ANXfIoTGxcmpxKM7Vc30/rTpPOkMS2U
|
||||
hIAikSvQW1TbQX/sZuh79wmz6HsPHEJ5aYKDGOgth3w
|
||||
-> ssh-ed25519 bRHVVA mFylt2nF7ZSaVreJY7vYRyN2+uTYgmBma5obySmKkFg
|
||||
WgSRPkHDDCEvH7xAK/sWVGXH5BJMVoVD4kR8gEBSE9M
|
||||
-> ssh-ed25519 HgW9eA 57LJVmiyiLaOxFxEqhk2YYzv43shHrSz3BkQJ5cm5WQ
|
||||
EGcAXlGAAzscqY8qpmDDros+KU8owNLRkkx5QVSUIwo
|
||||
--- kwagcoZytytUNvidfgNm2R7aQKiAA9HS8tN9fqhM0qY
|
||||
<µ•D²¸íüÝ–JåžA5ŽtJŸ¶Ä볆áöÙt*=ˆþK™¨ý|é<1B>þN‰@6‡[ë)¬cJ68µQyøÚC;~BcuøxÛj³õŽÅÅq™…ý*M½‡5EA»;
à°!ÜÌš?µêfr`Q¥c<C2A5>-Y’h<>ÈÞ
|
||||
¬çïª*1ÑôXyW§úrÄ
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 GCcVXA qD1f4YdP88JvuPLS6H416O0LF+rKN/gJ+gPf0lFWIRs
|
||||
B0LuHjXX0UiHHTwY7v1HqP4VF5/5yn0d/gNW4zUOC5w
|
||||
-> piv-p256 ewCc3w A8Wnjz7tLNpV/m5bUzuq/MBTHn+jQiaCSUb4vFvGDH7N
|
||||
gNBEsgdHogaz3OSJBelB39bGrOBt8+hkNXpAYcFcy28
|
||||
-> piv-p256 6CL/Pw AoJBcpyhK+hpZOfHOBUHh0Kivvn6YJ4xepo7IWppbMuJ
|
||||
f1fwsu8okH7F+6nSHowxHpTdtjU72lvd6FmwFIMmV8E
|
||||
-> ssh-ed25519 eOAUSg 0bH2dDdNzKapr54HKndKJpKO5iTQtbhY957NIJQcvBo
|
||||
LYVsTuKNsE4JlBxbf2e+Qo9ZjRxnz6saEHEjyfCcAjc
|
||||
-> ssh-rsa REaZBA
|
||||
Pho7O15lWd5/2Uy1OQmA/JzE0wmOji6IPKGj03nK9aYSQjsTTc30YhNCrkddglGX
|
||||
MCClDyGDlQ0mCwHKVfpFeeSC9VQWQDDYk90dM4MF//6hHPqU/zn8dR25NyxDgyyi
|
||||
DVrCFB+S09LsKI4isbDoCru6fWXfdN4Fj8I44c64dBpIVh4XhSzUtmlzmge//35C
|
||||
y6EvAjVIhVTbbgnQ5CJFgnRlSPaN5BHDq76aJ3c+8Ajkjowa2YcYu7w8asi7PsIs
|
||||
WH6J2qMIPvPdxOEKczGc401ysUfz9fNFKs0Xx4spduwv8I3Sv4ImURDkYmCl/k7J
|
||||
rwRXbUGr8JzZu093cmkjVxDrSfb9VhqlZVJD7uc2VSTk8ntwStLWafSuE03bIN2L
|
||||
on0t/4/wXtvFtwlGoDe9QGYAJpPYWM0ZUPMZfpddOz9vYvL1+6nUcDroLMvCIItI
|
||||
blwJZ0970K/Je99SmI3+6mUEAw8nJ/pBFaF/LmTJecypLEalLNsu2IALyCMIdTa0
|
||||
|
||||
-> ssh-ed25519 J/iReg fpgi4t31rVl+KvOYd0Ph0Nd2gInJAZXNhQC5ekpv0kI
|
||||
lznh3YwmqVr05vjWxf1HKRWAFZGXbypAYygjUe+ELlk
|
||||
-> ssh-ed25519 GNhSGw 2cVptD/h5NWcYsjPPwQcP/b9tjJPDbhjDKh2hKhA5GM
|
||||
JZvQvXkb1RRw+NHnJ+HMV5tlwiBnLf4+cwm6J2hZD9Y
|
||||
-> ssh-ed25519 eXMAtA axP3hHgfgWJScGLIGpYgu0iYW1qZRQzMK9XjIeszrnc
|
||||
rBzAqSVoRqj/drZSOlxSIZLgZZU/wqGML1gKCEHtcy4
|
||||
-> ssh-ed25519 5hXocQ b3kjoM+FfQ434V7X8FdpIHVFjTn4sc+psj+9jyRljxU
|
||||
l5rna7ap0ydVXPCSr4qrPnu2MV2n7XwoSh/T87OkuMU
|
||||
-> ssh-ed25519 bRHVVA 92Rj1gMmueSHWzxsKWVs+7nF4JzuekhfaB2u5AWnazk
|
||||
yg4qa9lyHsGCOXWOTm7/XH9F8JzvRLjzsxyXe88oXnE
|
||||
-> ssh-ed25519 HgW9eA mpIFRqMXj08JuyYLf6Hx4p9fOLbxXZquSMAjh0e/lSU
|
||||
h1kvwnu+mLOeRfVFUXgE9IDo/MQ+4gtkYwrF3JWQfwY
|
||||
--- MCDm2NH5NWgwnFYQ+zJ4obfdxWpiv3BGohnIJ1bRkco
|
||||
_Ö¹`ðãFËÉÑvF3[;œç197%»4¼90MÞ[ï¼Ip¢:Ÿs|ÈñC<C3B1>RÃ6,CÔè÷;á[³;<§Éû¤b(´fqÀg8îp÷p÷
÷Dk p/ÐwîÒ¿$9Ý\
|
||||
Loading…
Reference in New Issue