Add web challenges
1
web/glacier-exchange/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
We have launched a new revolutionary exchange tool, allowing you to trade on the market and hanging out with your rich friends in the Glacier Club. Only Billionaires can get in though. Can you help me hang out with lEon sMuk?
|
BIN
web/glacier-exchange/chall/assets/icons/ascoin.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
web/glacier-exchange/chall/assets/icons/cashout.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
web/glacier-exchange/chall/assets/icons/doge.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
web/glacier-exchange/chall/assets/icons/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
web/glacier-exchange/chall/assets/icons/gamestock.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
web/glacier-exchange/chall/assets/icons/glaciercoin.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
web/glacier-exchange/chall/assets/icons/smtl.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
web/glacier-exchange/chall/assets/icons/ycmi.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
web/glacier-exchange/chall/assets/images/bg.jpg
Normal file
After Width: | Height: | Size: 873 KiB |
BIN
web/glacier-exchange/chall/assets/images/convert-button.jpg
Normal file
After Width: | Height: | Size: 18 KiB |
372
web/glacier-exchange/chall/assets/scripts/chart.component.js
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
Vue.component('glacier-chart', {
|
||||||
|
props: [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
template: `<div class="box has-text-white">
|
||||||
|
<h1 class="title has-text-centered has-text-white">{{ name }}</h1>
|
||||||
|
<div id="chartcontrols"></div>
|
||||||
|
<div id="chartdiv"></div>
|
||||||
|
</div>`,
|
||||||
|
watch: {
|
||||||
|
name() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
data: _ => {
|
||||||
|
return {
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData() {
|
||||||
|
fetch("/api/data/fetch/" + encodeURIComponent(this.name)).then(res => res.json()).then(data => {
|
||||||
|
this.data = data;
|
||||||
|
this.render();
|
||||||
|
})
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
am5.ready(_ => {
|
||||||
|
|
||||||
|
// Create root element
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/getting-started/#Root_element
|
||||||
|
var root = am5.Root.new("chartdiv");
|
||||||
|
// Set themes
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/concepts/themes/
|
||||||
|
root.setThemes([
|
||||||
|
// am5themes_Animated.new(root)
|
||||||
|
am5themes_Dark.new(root)
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Create a stock chart
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/stock-chart/#Instantiating_the_chart
|
||||||
|
var stockChart = root.container.children.push(am5stock.StockChart.new(root, {
|
||||||
|
}));
|
||||||
|
this.chart = stockChart;
|
||||||
|
|
||||||
|
|
||||||
|
// Set global number format
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/concepts/formatters/formatting-numbers/
|
||||||
|
root.numberFormatter.set("numberFormat", "#,###.00");
|
||||||
|
|
||||||
|
|
||||||
|
// Create a main stock panel (chart)
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/stock-chart/#Adding_panels
|
||||||
|
var mainPanel = stockChart.panels.push(am5stock.StockPanel.new(root, {
|
||||||
|
wheelY: "zoomX",
|
||||||
|
panX: true,
|
||||||
|
panY: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
// Create value axis
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
|
||||||
|
var valueAxis = mainPanel.yAxes.push(am5xy.ValueAxis.new(root, {
|
||||||
|
renderer: am5xy.AxisRendererY.new(root, {
|
||||||
|
pan: "zoom"
|
||||||
|
}),
|
||||||
|
extraMin: 0.1, // adds some space for for main series
|
||||||
|
tooltip: am5.Tooltip.new(root, {}),
|
||||||
|
numberFormat: "#,###.00",
|
||||||
|
extraTooltipPrecision: 2
|
||||||
|
}));
|
||||||
|
|
||||||
|
var dateAxis = mainPanel.xAxes.push(am5xy.GaplessDateAxis.new(root, {
|
||||||
|
baseInterval: {
|
||||||
|
timeUnit: "day",
|
||||||
|
count: 1
|
||||||
|
},
|
||||||
|
renderer: am5xy.AxisRendererX.new(root, {}),
|
||||||
|
tooltip: am5.Tooltip.new(root, {})
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
// Add series
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
|
||||||
|
var valueSeries = mainPanel.series.push(am5xy.CandlestickSeries.new(root, {
|
||||||
|
name: this.name,
|
||||||
|
clustered: false,
|
||||||
|
valueXField: "Date",
|
||||||
|
valueYField: "Close",
|
||||||
|
highValueYField: "High",
|
||||||
|
lowValueYField: "Low",
|
||||||
|
openValueYField: "Open",
|
||||||
|
calculateAggregates: true,
|
||||||
|
xAxis: dateAxis,
|
||||||
|
yAxis: valueAxis,
|
||||||
|
legendValueText: "open: [bold]{openValueY}[/] high: [bold]{highValueY}[/] low: [bold]{lowValueY}[/] close: [bold]{valueY}[/]",
|
||||||
|
legendRangeValueText: ""
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
// Set main value series
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/stock-chart/#Setting_main_series
|
||||||
|
stockChart.set("stockSeries", valueSeries);
|
||||||
|
|
||||||
|
|
||||||
|
// Add a stock legend
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/stock-chart/stock-legend/
|
||||||
|
var valueLegend = mainPanel.plotContainer.children.push(am5stock.StockLegend.new(root, {
|
||||||
|
stockChart: stockChart
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
// Create volume axis
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
|
||||||
|
var volumeAxisRenderer = am5xy.AxisRendererY.new(root, {
|
||||||
|
inside: true
|
||||||
|
});
|
||||||
|
|
||||||
|
volumeAxisRenderer.labels.template.set("forceHidden", true);
|
||||||
|
volumeAxisRenderer.grid.template.set("forceHidden", true);
|
||||||
|
|
||||||
|
var volumeValueAxis = mainPanel.yAxes.push(am5xy.ValueAxis.new(root, {
|
||||||
|
numberFormat: "#.#a",
|
||||||
|
height: am5.percent(20),
|
||||||
|
y: am5.percent(100),
|
||||||
|
centerY: am5.percent(100),
|
||||||
|
renderer: volumeAxisRenderer
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add series
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
|
||||||
|
var volumeSeries = mainPanel.series.push(am5xy.ColumnSeries.new(root, {
|
||||||
|
name: "Volume",
|
||||||
|
clustered: false,
|
||||||
|
valueXField: "Date",
|
||||||
|
valueYField: "Volume",
|
||||||
|
xAxis: dateAxis,
|
||||||
|
yAxis: volumeValueAxis,
|
||||||
|
legendValueText: "[bold]{valueY.formatNumber('#,###.0a')}[/]"
|
||||||
|
}));
|
||||||
|
|
||||||
|
volumeSeries.columns.template.setAll({
|
||||||
|
strokeOpacity: 0,
|
||||||
|
fillOpacity: 0.5
|
||||||
|
});
|
||||||
|
|
||||||
|
// color columns by stock rules
|
||||||
|
volumeSeries.columns.template.adapters.add("fill", function (fill, target) {
|
||||||
|
var dataItem = target.dataItem;
|
||||||
|
if (dataItem) {
|
||||||
|
return stockChart.getVolumeColor(dataItem);
|
||||||
|
}
|
||||||
|
return fill;
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Set main series
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/stock-chart/#Setting_main_series
|
||||||
|
stockChart.set("volumeSeries", volumeSeries);
|
||||||
|
valueLegend.data.setAll([valueSeries, volumeSeries]);
|
||||||
|
|
||||||
|
|
||||||
|
// Add cursor(s)
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/
|
||||||
|
mainPanel.set("cursor", am5xy.XYCursor.new(root, {
|
||||||
|
yAxis: valueAxis,
|
||||||
|
xAxis: dateAxis,
|
||||||
|
snapToSeries: [valueSeries],
|
||||||
|
snapToSeriesBy: "y!"
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
// Add scrollbar
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/xy-chart/scrollbars/
|
||||||
|
var scrollbar = mainPanel.set("scrollbarX", am5xy.XYChartScrollbar.new(root, {
|
||||||
|
orientation: "horizontal",
|
||||||
|
height: 50
|
||||||
|
}));
|
||||||
|
stockChart.toolsContainer.children.push(scrollbar);
|
||||||
|
|
||||||
|
var sbDateAxis = scrollbar.chart.xAxes.push(am5xy.GaplessDateAxis.new(root, {
|
||||||
|
baseInterval: {
|
||||||
|
timeUnit: "day",
|
||||||
|
count: 1
|
||||||
|
},
|
||||||
|
renderer: am5xy.AxisRendererX.new(root, {})
|
||||||
|
}));
|
||||||
|
|
||||||
|
var sbValueAxis = scrollbar.chart.yAxes.push(am5xy.ValueAxis.new(root, {
|
||||||
|
renderer: am5xy.AxisRendererY.new(root, {})
|
||||||
|
}));
|
||||||
|
|
||||||
|
var sbSeries = scrollbar.chart.series.push(am5xy.LineSeries.new(root, {
|
||||||
|
valueYField: "Close",
|
||||||
|
valueXField: "Date",
|
||||||
|
xAxis: sbDateAxis,
|
||||||
|
yAxis: sbValueAxis
|
||||||
|
}));
|
||||||
|
|
||||||
|
sbSeries.fills.template.setAll({
|
||||||
|
visible: true,
|
||||||
|
fillOpacity: 0.3
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up series type switcher
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/stock/toolbar/series-type-control/
|
||||||
|
var seriesSwitcher = am5stock.SeriesTypeControl.new(root, {
|
||||||
|
stockChart: stockChart
|
||||||
|
});
|
||||||
|
|
||||||
|
seriesSwitcher.events.on("selected", function (ev) {
|
||||||
|
setSeriesType(ev.item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getNewSettings(series) {
|
||||||
|
var newSettings = [];
|
||||||
|
am5.array.each(["name", "valueYField", "highValueYField", "lowValueYField", "openValueYField", "calculateAggregates", "valueXField", "xAxis", "yAxis", "legendValueText", "stroke", "fill"], function (setting) {
|
||||||
|
newSettings[setting] = series.get(setting);
|
||||||
|
});
|
||||||
|
return newSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSeriesType(seriesType) {
|
||||||
|
// Get current series and its settings
|
||||||
|
var currentSeries = stockChart.get("stockSeries");
|
||||||
|
var newSettings = getNewSettings(currentSeries);
|
||||||
|
|
||||||
|
// Remove previous series
|
||||||
|
var data = currentSeries.data.values;
|
||||||
|
mainPanel.series.removeValue(currentSeries);
|
||||||
|
|
||||||
|
// Create new series
|
||||||
|
var series;
|
||||||
|
switch (seriesType) {
|
||||||
|
case "line":
|
||||||
|
series = mainPanel.series.push(am5xy.LineSeries.new(root, newSettings));
|
||||||
|
break;
|
||||||
|
case "candlestick":
|
||||||
|
case "procandlestick":
|
||||||
|
newSettings.clustered = false;
|
||||||
|
series = mainPanel.series.push(am5xy.CandlestickSeries.new(root, newSettings));
|
||||||
|
if (seriesType == "procandlestick") {
|
||||||
|
series.columns.template.get("themeTags").push("pro");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ohlc":
|
||||||
|
newSettings.clustered = false;
|
||||||
|
series = mainPanel.series.push(am5xy.OHLCSeries.new(root, newSettings));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new series as stockSeries
|
||||||
|
if (series) {
|
||||||
|
valueLegend.data.removeValue(currentSeries);
|
||||||
|
series.data.setAll(data);
|
||||||
|
stockChart.set("stockSeries", series);
|
||||||
|
var cursor = mainPanel.get("cursor");
|
||||||
|
if (cursor) {
|
||||||
|
cursor.set("snapToSeries", [series]);
|
||||||
|
}
|
||||||
|
valueLegend.data.insertIndex(0, series);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Stock toolbar
|
||||||
|
// -------------------------------------------------------------------------------
|
||||||
|
// https://www.amcharts.com/docs/v5/charts/stock/toolbar/
|
||||||
|
var toolbar = am5stock.StockToolbar.new(root, {
|
||||||
|
container: document.getElementById("chartcontrols"),
|
||||||
|
stockChart: stockChart,
|
||||||
|
controls: [
|
||||||
|
am5stock.IndicatorControl.new(root, {
|
||||||
|
stockChart: stockChart,
|
||||||
|
legend: valueLegend
|
||||||
|
}),
|
||||||
|
am5stock.DateRangeSelector.new(root, {
|
||||||
|
stockChart: stockChart
|
||||||
|
}),
|
||||||
|
am5stock.PeriodSelector.new(root, {
|
||||||
|
stockChart: stockChart
|
||||||
|
}),
|
||||||
|
seriesSwitcher,
|
||||||
|
am5stock.DrawingControl.new(root, {
|
||||||
|
stockChart: stockChart
|
||||||
|
}),
|
||||||
|
am5stock.ResetControl.new(root, {
|
||||||
|
stockChart: stockChart
|
||||||
|
}),
|
||||||
|
am5stock.SettingsControl.new(root, {
|
||||||
|
stockChart: stockChart
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// data
|
||||||
|
var data = this.data;
|
||||||
|
|
||||||
|
var tooltip = am5.Tooltip.new(root, {
|
||||||
|
getStrokeFromSprite: false,
|
||||||
|
getFillFromSprite: false
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltip.get("background").setAll({
|
||||||
|
strokeOpacity: 1,
|
||||||
|
stroke: am5.color(0x000000),
|
||||||
|
fillOpacity: 1,
|
||||||
|
fill: am5.color(0xffffff)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function makeEvent(date, letter, color, description) {
|
||||||
|
var dataItem = dateAxis.createAxisRange(dateAxis.makeDataItem({ value: date }))
|
||||||
|
var grid = dataItem.get("grid");
|
||||||
|
if (grid) {
|
||||||
|
grid.setAll({ visible: true, strokeOpacity: 0.2, strokeDasharray: [3, 3] })
|
||||||
|
}
|
||||||
|
|
||||||
|
var bullet = am5.Container.new(root, {
|
||||||
|
dy: -100
|
||||||
|
});
|
||||||
|
|
||||||
|
var circle = bullet.children.push(am5.Circle.new(root, {
|
||||||
|
radius: 10,
|
||||||
|
stroke: color,
|
||||||
|
fill: am5.color(0xffffff),
|
||||||
|
tooltipText: description,
|
||||||
|
tooltip: tooltip,
|
||||||
|
tooltipY: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
var label = bullet.children.push(am5.Label.new(root, {
|
||||||
|
text: letter,
|
||||||
|
centerX: am5.p50,
|
||||||
|
centerY: am5.p50
|
||||||
|
}));
|
||||||
|
|
||||||
|
dataItem.set("bullet", am5xy.AxisBullet.new(root, {
|
||||||
|
location: 0,
|
||||||
|
stacked: true,
|
||||||
|
sprite: bullet
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// set data to all series
|
||||||
|
valueSeries.data.setAll(data);
|
||||||
|
volumeSeries.data.setAll(data);
|
||||||
|
sbSeries.data.setAll(data);
|
||||||
|
|
||||||
|
}); // end am5.ready()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
103
web/glacier-exchange/chall/assets/scripts/index.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
|
||||||
|
(function() {
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
dropdownVisibility: {
|
||||||
|
source: false,
|
||||||
|
target: false
|
||||||
|
},
|
||||||
|
sourceCoinAmount: 0,
|
||||||
|
sourceCoinFilter: '',
|
||||||
|
targetCoinFilter: '',
|
||||||
|
sourceCoinValue: 'cashout',
|
||||||
|
targetCoinValue: 'ascoin',
|
||||||
|
balances: [],
|
||||||
|
coins: [],
|
||||||
|
club: false
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeSourceCoin(name) {
|
||||||
|
this.sourceCoinValue = name;
|
||||||
|
this.dropdownVisibility.source = false;
|
||||||
|
this.sourceCoinFilter = '';
|
||||||
|
},
|
||||||
|
changeTargetCoin(name) {
|
||||||
|
this.targetCoinValue = name;
|
||||||
|
this.dropdownVisibility.target = false;
|
||||||
|
this.targetCoinFilter = '';
|
||||||
|
},
|
||||||
|
swapCoins() {
|
||||||
|
const tmp = this.sourceCoinValue;
|
||||||
|
this.sourceCoinValue = this.targetCoinValue;
|
||||||
|
this.targetCoinValue = tmp;
|
||||||
|
},
|
||||||
|
fetchCoins() {
|
||||||
|
fetch("/api/fetch_coins").then(res => res.json()).then(coins => {
|
||||||
|
this.coins = coins;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fetchBalances() {
|
||||||
|
fetch("/api/wallet/balances").then(res => res.json()).then(balances => {
|
||||||
|
this.balances = balances;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
convert() {
|
||||||
|
fetch("/api/wallet/transaction", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
sourceCoin: this.sourceCoinValue,
|
||||||
|
targetCoin: this.targetCoinValue,
|
||||||
|
balance: this.sourceCoinAmount
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}).then(_ => {
|
||||||
|
this.fetchBalances();
|
||||||
|
this.sourceCoinAmount = 0;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
joinGlacierClub() {
|
||||||
|
fetch("/api/wallet/join_glacier_club", {method: "POST"}).then(res => res.json()).then(club => {
|
||||||
|
this.club = club;
|
||||||
|
this.$refs.modalGlacierclub.classList.add('is-active')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeClubModal() {
|
||||||
|
this.$refs.modalGlacierclub.classList.remove('is-active')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredSourceCoins() {
|
||||||
|
return this.coins.filter(coin => coin.value.includes(this.sourceCoinFilter) && coin.name !== this.targetCoinValue);
|
||||||
|
},
|
||||||
|
filteredTargetCoins() {
|
||||||
|
return this.coins.filter(coin => coin.value.includes(this.targetCoinFilter) && coin.name !== this.sourceCoinValue);
|
||||||
|
},
|
||||||
|
sourceCoin() {
|
||||||
|
return this.coins.filter(coin => coin.name === this.sourceCoinValue)[0];
|
||||||
|
},
|
||||||
|
targetCoin() {
|
||||||
|
return this.coins.filter(coin => coin.name === this.targetCoinValue)[0];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchCoins();
|
||||||
|
this.fetchBalances();
|
||||||
|
},
|
||||||
|
delimiters: ['$$', '$$'],
|
||||||
|
})
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
setInterval(_ => {
|
||||||
|
document.querySelectorAll("[data-adjust-width-to]").forEach(element => {
|
||||||
|
const referenceElementId = element.dataset.adjustWidthTo;
|
||||||
|
if(!referenceElementId) return;
|
||||||
|
const referenceElement = document.getElementById(referenceElementId);
|
||||||
|
if(!referenceElement) return;
|
||||||
|
element.style.width = `${referenceElement.offsetWidth}px`;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})();
|
141
web/glacier-exchange/chall/assets/styles/main.css
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
html {
|
||||||
|
background: url(/assets/images/bg.jpg) no-repeat center center fixed;
|
||||||
|
-webkit-background-size: cover;
|
||||||
|
-moz-background-size: cover;
|
||||||
|
-o-background-size: cover;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
background-color: #27273e;
|
||||||
|
opacity: 0.9;
|
||||||
|
/* min-height: 500px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.textfield-area {
|
||||||
|
border: 1px solid #72727b;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textfield-area .button {
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textfield-area input:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textfield-area input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textfield-area label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textfield-area input:focus,
|
||||||
|
.textfield-area .button:focus,
|
||||||
|
.textfield-area .button:focus-visible,
|
||||||
|
.select-area .input:focus,
|
||||||
|
.select-area .input:active {
|
||||||
|
outline: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-area {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-area .input,
|
||||||
|
.select-area .input:focus,
|
||||||
|
.select-area .input::placeholder {
|
||||||
|
background: #3b3b4f;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-area .control.has-icons-right .input:focus~.icon {
|
||||||
|
color: #dbdbdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-area .dropdown-item {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-area .dropdown-menu {
|
||||||
|
padding-top: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-area a.dropdown-item {
|
||||||
|
padding-right: 1rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-area .dropdown-content {
|
||||||
|
background: #27273e;
|
||||||
|
border: 1px solid #353544;
|
||||||
|
border-radius: 0px 0px 2px 2px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-area .dropdown-item:hover {
|
||||||
|
background: #1e1e31;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-area i {
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #3a3a57;
|
||||||
|
font-size: 30px;
|
||||||
|
color: #15b1d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-area i:hover {
|
||||||
|
background-color: #151522;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Chart library style **/
|
||||||
|
|
||||||
|
#chartcontrols {
|
||||||
|
height: auto;
|
||||||
|
padding: 5px 5px 0 16px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chartdiv {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance .table {
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance .table th {
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancy-button button,
|
||||||
|
.fancy-button button:focus,
|
||||||
|
.fancy-button button:hover,
|
||||||
|
.fancy-button button::placeholder {
|
||||||
|
background-color: #90b4ce;
|
||||||
|
border-color: #02375e;
|
||||||
|
border-width: 2px;
|
||||||
|
color: #111160;
|
||||||
|
}
|
||||||
|
|
2
web/glacier-exchange/chall/requirements.txt
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
flask
|
||||||
|
flask_restful
|
123
web/glacier-exchange/chall/server.py
Executable file
@ -0,0 +1,123 @@
|
|||||||
|
from flask import Flask, render_template, request, send_from_directory, jsonify, session
|
||||||
|
from flask_restful import Api
|
||||||
|
from src.coin_api import get_coin_price_from_api
|
||||||
|
from src.wallet import Wallet
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
api = Api(app)
|
||||||
|
|
||||||
|
app.secret_key = os.urandom(64)
|
||||||
|
|
||||||
|
wallets = {}
|
||||||
|
def get_wallet_from_session():
|
||||||
|
if "id" not in session:
|
||||||
|
session["id"] = make_token()
|
||||||
|
if session["id"] not in wallets:
|
||||||
|
wallets[session["id"]] = Wallet()
|
||||||
|
return wallets[session["id"]]
|
||||||
|
|
||||||
|
def make_token():
|
||||||
|
return secrets.token_urlsafe(16)
|
||||||
|
|
||||||
|
@app.route("/", methods=["GET", "POST"])
|
||||||
|
def index():
|
||||||
|
return render_template(
|
||||||
|
"index.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route('/assets/<path:path>')
|
||||||
|
def assets(path):
|
||||||
|
return send_from_directory('assets', path)
|
||||||
|
|
||||||
|
@app.route('/api/data/fetch/<path:coin>')
|
||||||
|
def fetch(coin: str):
|
||||||
|
data = get_coin_price_from_api(coin)
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
@app.route('/api/wallet/transaction', methods=['POST'])
|
||||||
|
def transaction():
|
||||||
|
payload = request.json
|
||||||
|
status = 0
|
||||||
|
if "sourceCoin" in payload and "targetCoin" in payload and "balance" in payload:
|
||||||
|
wallet = get_wallet_from_session()
|
||||||
|
status = wallet.transaction(payload["sourceCoin"], payload["targetCoin"], float(payload["balance"]))
|
||||||
|
return jsonify({
|
||||||
|
"result": status
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route("/api/wallet/join_glacier_club", methods=["POST"])
|
||||||
|
def join_glacier_club():
|
||||||
|
wallet = get_wallet_from_session()
|
||||||
|
clubToken = False
|
||||||
|
inClub = wallet.inGlacierClub()
|
||||||
|
if inClub:
|
||||||
|
f = open("/flag.txt")
|
||||||
|
clubToken = f.read()
|
||||||
|
f.close()
|
||||||
|
return {
|
||||||
|
"inClub": inClub,
|
||||||
|
"clubToken": clubToken
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.route('/api/wallet/balances')
|
||||||
|
def get_balance():
|
||||||
|
wallet = get_wallet_from_session()
|
||||||
|
balances = wallet.getBalances()
|
||||||
|
user_balances = []
|
||||||
|
for name in balances:
|
||||||
|
user_balances.append({
|
||||||
|
"name": name,
|
||||||
|
"value": balances[name]
|
||||||
|
})
|
||||||
|
return user_balances
|
||||||
|
|
||||||
|
@app.route('/api/fetch_coins')
|
||||||
|
def fetch_coins():
|
||||||
|
return jsonify([
|
||||||
|
{
|
||||||
|
"name": 'cashout',
|
||||||
|
"value": 'Cashout Account',
|
||||||
|
"short": 'CA'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": 'glaciercoin',
|
||||||
|
"value": 'GlacierCoin',
|
||||||
|
"short": 'GC'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": 'ascoin',
|
||||||
|
"value": 'AsCoin',
|
||||||
|
"short": 'AC'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": 'doge',
|
||||||
|
"value": 'Doge',
|
||||||
|
"short": 'DO'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": 'gamestock',
|
||||||
|
"value": 'Gamestock',
|
||||||
|
"short": 'GS'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": 'ycmi',
|
||||||
|
"value": 'Yeti Clubs Manufacturing Inc.',
|
||||||
|
"short": 'YC'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": 'smtl',
|
||||||
|
"value": 'Synthetic Mammoth Tusks LLC',
|
||||||
|
"short": 'ST'
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8080,
|
||||||
|
debug=True,
|
||||||
|
)
|
48
web/glacier-exchange/chall/src/coin_api.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import time
|
||||||
|
import random
|
||||||
|
|
||||||
|
def get_coin_price_from_api(coin: str):
|
||||||
|
coins = coin.split('/')
|
||||||
|
if(len(coins) != 2):
|
||||||
|
return []
|
||||||
|
seed = coins[0] + coins[1] if coins[0] < coins[1] else coins[1] + coins[0]
|
||||||
|
is_reverse = coins[0] < coins[1]
|
||||||
|
random.seed(seed)
|
||||||
|
end_timestamp = int(time.time()) * 1000
|
||||||
|
|
||||||
|
new_open = 15.67
|
||||||
|
new_high = 15.83
|
||||||
|
new_low = 15.24
|
||||||
|
new_close = 15.36
|
||||||
|
|
||||||
|
new_volume = 3503100
|
||||||
|
movement = 0.7
|
||||||
|
|
||||||
|
data = []
|
||||||
|
max_ticks = 200
|
||||||
|
for ts in range(0, max_ticks):
|
||||||
|
|
||||||
|
display_new_open = 1. / new_open if is_reverse else new_open
|
||||||
|
display_new_high = 1. / new_high if is_reverse else new_high
|
||||||
|
display_new_low = 1. / new_low if is_reverse else new_low
|
||||||
|
display_new_close = 1. / new_close if is_reverse else new_close
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
"Date": end_timestamp - (max_ticks - ts) * (1000 * 86400),
|
||||||
|
"Open": display_new_open,
|
||||||
|
"High": display_new_high,
|
||||||
|
"Low": display_new_low,
|
||||||
|
"Close": display_new_close,
|
||||||
|
"Volume": new_volume
|
||||||
|
})
|
||||||
|
|
||||||
|
# New Open => Downwards Trend
|
||||||
|
# New Close => Upwards Trend
|
||||||
|
indicator = new_open if random.random() > 0.5 else new_close
|
||||||
|
|
||||||
|
new_open = indicator + movement * (random.random() - 0.5)
|
||||||
|
new_high = indicator + movement * (random.random() - 0.5)
|
||||||
|
new_low = indicator + movement * (random.random() - 0.5)
|
||||||
|
new_close = indicator + movement * (random.random() - 0.5)
|
||||||
|
new_volume = new_volume + movement * (random.random() - 0.5)
|
||||||
|
return data
|
39
web/glacier-exchange/chall/src/wallet.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
class Wallet():
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.balances = {
|
||||||
|
"cashout": 1000,
|
||||||
|
"glaciercoin": 0,
|
||||||
|
"ascoin": 0,
|
||||||
|
"doge": 0,
|
||||||
|
"gamestock": 0,
|
||||||
|
"ycmi": 0,
|
||||||
|
"smtl": 0
|
||||||
|
}
|
||||||
|
self.lock = threading.Lock();
|
||||||
|
|
||||||
|
|
||||||
|
def getBalances(self):
|
||||||
|
return self.balances
|
||||||
|
|
||||||
|
def transaction(self, source, dest, amount):
|
||||||
|
if source in self.balances and dest in self.balances:
|
||||||
|
with self.lock:
|
||||||
|
if self.balances[source] >= amount:
|
||||||
|
self.balances[source] -= amount
|
||||||
|
self.balances[dest] += amount
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def inGlacierClub(self):
|
||||||
|
with self.lock:
|
||||||
|
for balance_name in self.balances:
|
||||||
|
if balance_name == "cashout":
|
||||||
|
if self.balances[balance_name] < 1000000000:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if self.balances[balance_name] != 0.0:
|
||||||
|
return False
|
||||||
|
return True
|
201
web/glacier-exchange/chall/templates/index.html
Executable file
@ -0,0 +1,201 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<title>GlacierExchange</title>
|
||||||
|
<link rel="shortcut icon" href="/assets/icons/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||||
|
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
|
<link rel="stylesheet" href="assets/styles/main.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<section class="hero is-fullheight" v-if="coins.length > 0 && balances.length > 0" id="app">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<glacier-chart :key="sourceCoinValue + '/' + targetCoinValue" :name="sourceCoinValue + '/' + targetCoinValue"></glacier-chart>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div class="balance box has-text-white">
|
||||||
|
<h1 class="title has-text-centered has-text-white">Your Balance</h1>
|
||||||
|
<table class="table has-text-centered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="has-text-centered">Coin</th>
|
||||||
|
<th class="has-text-centered">Balance</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="balance in balances">
|
||||||
|
<th class="has-text-centered">$$ balance.name $$</th>
|
||||||
|
<td>$$ balance.value $$</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="fancy-button buttons are-medium">
|
||||||
|
<button class="button is-fullwidth" @click="joinGlacierClub">Join GlacierClub</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="box has-text-white">
|
||||||
|
<h1 class="title has-text-centered has-text-white">GlacierExchange</h1>
|
||||||
|
|
||||||
|
<p style="text-align: justify;" class="mb-5">
|
||||||
|
The GlacierExchange is a revolutionary tool for converting glacierchain coins from one
|
||||||
|
currency to another.
|
||||||
|
The conversion rate is always guaranteed to be 1:1 without fees.
|
||||||
|
</p>
|
||||||
|
<!-- Input field -->
|
||||||
|
<div class="textfield-area">
|
||||||
|
<div class="columns is-flex is-vcentered">
|
||||||
|
<div class="column">
|
||||||
|
<label for="from">From</label>
|
||||||
|
<input type="text" v-model="sourceCoinAmount" />
|
||||||
|
</div>
|
||||||
|
<div class="column is-two-fifths has-text-right">
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button @click="dropdownVisibility.source ^= 1" class="button"
|
||||||
|
aria-haspopup="true" aria-controls="dropdown-menu">
|
||||||
|
<img class="mr-3" style="width: 32px" :src="'/assets/icons/' + sourceCoin.name + '.png'" alt="">
|
||||||
|
<span>$$ sourceCoin.short $$</span>
|
||||||
|
<span class="ml-2 icon is-small">
|
||||||
|
<i class="fa fa-angle-down" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Dropdown -->
|
||||||
|
<div class="select-area" id="select-area-from">
|
||||||
|
<div :class="['dropdown', dropdownVisibility.source ? 'is-active' : '']">
|
||||||
|
<div class="dropdown-menu" role="menu" data-adjust-width-to="select-area-from">
|
||||||
|
<div class="control has-icons-right">
|
||||||
|
<input v-model="sourceCoinFilter" type="text"
|
||||||
|
placeholder="Search for symbol" class="input is-transparent is-small">
|
||||||
|
<span class="icon is-right">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
|
||||||
|
<a @click="changeSourceCoin(coin.name)" href="javascript:void(0)"
|
||||||
|
v-for="coin in filteredSourceCoins" class="dropdown-item">
|
||||||
|
<img class="mr-3" style="width: 16px; vertical-align: middle;" :src="'/assets/icons/' + coin.name + '.png'" alt="">
|
||||||
|
$$ coin.value $$
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Swap Coins -->
|
||||||
|
<div class="has-text-centered rotate-area mt-5 mb-5">
|
||||||
|
<i @click="swapCoins" class="fa-solid fa-rotate"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input field -->
|
||||||
|
<div class="textfield-area">
|
||||||
|
<div class="columns is-flex is-vcentered">
|
||||||
|
<div class="column">
|
||||||
|
<label for="from">To</label>
|
||||||
|
<input type="text" disabled :value="sourceCoinAmount" />
|
||||||
|
</div>
|
||||||
|
<div class="column is-two-fifths has-text-right">
|
||||||
|
<div class="dropdown-trigger">
|
||||||
|
<button @click="dropdownVisibility.target ^= 1" class="button"
|
||||||
|
aria-haspopup="true" aria-controls="dropdown-menu">
|
||||||
|
<img class="mr-3" style="width: 32px" :src="'/assets/icons/' + targetCoin.name + '.png'" alt="">
|
||||||
|
<span>$$ targetCoin.short $$</span>
|
||||||
|
<span class="ml-2 icon is-small">
|
||||||
|
<i class="fa fa-angle-down" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Dropdown -->
|
||||||
|
<div class="select-area" id="select-area-from">
|
||||||
|
<div :class="['dropdown', dropdownVisibility.target ? 'is-active' : '']">
|
||||||
|
<div class="dropdown-menu" role="menu" data-adjust-width-to="select-area-from">
|
||||||
|
<div class="control has-icons-right">
|
||||||
|
<input v-model="targetCoinFilter" type="text"
|
||||||
|
placeholder="Search for symbol" class="input is-transparent is-small">
|
||||||
|
<span class="icon is-right">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a @click="changeTargetCoin(coin.name)" href="javascript:void(0)"
|
||||||
|
v-for="coin in filteredTargetCoins" class="dropdown-item">
|
||||||
|
<img class="mr-3" style="width: 16px; vertical-align: middle;" :src="'/assets/icons/' + coin.name + '.png'" alt="">
|
||||||
|
$$ coin.value $$
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="fancy-button buttons are-medium mt-4">
|
||||||
|
<button class="button is-fullwidth" @click="convert">Convert</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="modalGlacierclub" class="modal">
|
||||||
|
<div class="modal-background" @click="closeClubModal"></div>
|
||||||
|
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="box has-text-white">
|
||||||
|
<div v-if="club && club.inClub === true">
|
||||||
|
<h1 class="title has-text-centered has-text-white">Welcome my lord</h1>
|
||||||
|
<p>
|
||||||
|
Your royality is sufficient to join glacier club.<br />
|
||||||
|
A member of the glacier club will contact you.<br />
|
||||||
|
Please hold ready your club token when they contact you<br />
|
||||||
|
Club-Token: $$ club.clubToken $$
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h1 class="title has-text-centered has-text-white">Insufficient royality</h1>
|
||||||
|
<p>
|
||||||
|
You are not eligible to join the royal club of the glaciers.<br />
|
||||||
|
Earn more money to get a member!<br />
|
||||||
|
Make sure to empty all coins except the cashout wallet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="closeClubModal" class="modal-close is-large" aria-label="close"></button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.amcharts.com/lib/5/index.js"></script>
|
||||||
|
<script src="https://cdn.amcharts.com/lib/5/xy.js"></script>
|
||||||
|
<script src="https://cdn.amcharts.com/lib/5/stock.js"></script>
|
||||||
|
<script src="https://cdn.amcharts.com/lib/5/themes/Dark.js"></script>
|
||||||
|
|
||||||
|
<script src="/assets/scripts/chart.component.js"></script>
|
||||||
|
<script src="/assets/scripts/index.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
BIN
web/glacier-exchange/glacier_exchange.zip
Normal file
1
web/peak/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Within the heart of Austria's alpine mystery lies your next conquest. Ascend the highest peak, shrouded in whispers of past explorers, to uncover the flag.txt awaiting atop. Beware the silent guards that stand sentinel along the treacherous path, obstructing your ascent.
|
BIN
web/peak/challenge.zip
Normal file
53
web/peak/dist/.docker/Dockerfile-web
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
FROM tobi312/php:8.1-apache
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
COPY ./web/ /var/www/html/
|
||||||
|
COPY ./flag/flag.txt /
|
||||||
|
|
||||||
|
RUN mkdir -p /var/sqlite/
|
||||||
|
COPY ./sqlite.db /var/sqlite/
|
||||||
|
|
||||||
|
RUN chown -R 33:33 /var/sqlite/
|
||||||
|
RUN chmod 750 /var/sqlite/sqlite.db
|
||||||
|
RUN mkdir -p /var/www/html/uploads/
|
||||||
|
RUN chown 33:33 /var/www/html/uploads/
|
||||||
|
RUN chmod -R 777 /var/www/html/uploads/
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
RUN ln -s /dev/null /root/.bash_history
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 python3-pip curl unzip wget cron util-linux \
|
||||||
|
fonts-liberation libasound2 libatk-bridge2.0-0 procps \
|
||||||
|
libnss3 lsb-release xdg-utils libxss1 libdbus-glib-1-2 \
|
||||||
|
libcairo2 libcups2 libgbm1 libgtk-3-0 libpango-1.0-0 \
|
||||||
|
libu2f-udev libvulkan1 libxkbcommon-x11-0 xvfb
|
||||||
|
|
||||||
|
RUN CHROMEDRIVER_VERSION=`curl -sS https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE` && \
|
||||||
|
wget -q -O chromedriver_linux64.zip https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/$CHROMEDRIVER_VERSION/linux64/chromedriver-linux64.zip && \
|
||||||
|
unzip chromedriver_linux64.zip && mv chromedriver-linux64/chromedriver /usr/bin/ && \
|
||||||
|
chmod +x /usr/bin/chromedriver && \
|
||||||
|
rm chromedriver_linux64.zip && rm -r chromedriver-linux64
|
||||||
|
|
||||||
|
RUN CHROME_SETUP=google-chrome.deb && \
|
||||||
|
wget -q -O $CHROME_SETUP "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" && \
|
||||||
|
dpkg -i $CHROME_SETUP && \
|
||||||
|
apt-get install -y -f && \
|
||||||
|
rm $CHROME_SETUP
|
||||||
|
|
||||||
|
RUN rm /usr/lib/python3.11/EXTERNALLY-MANAGED
|
||||||
|
|
||||||
|
RUN python3 -m pip install selenium urllib3 python-decouple requests bs4 pyvirtualdisplay
|
||||||
|
|
||||||
|
COPY ./admin-simulation/ /root/admin_simulation
|
||||||
|
|
||||||
|
RUN echo '#!/bin/bash' > /entrypoint.d/simulation.sh
|
||||||
|
RUN echo 'echo "$(env | grep "HOST=.*")" >> /etc/environment' >> /entrypoint.d/simulation.sh
|
||||||
|
RUN echo 'echo "$(env | grep "ADMIN_PW=.*")" >> /etc/environment' >> /entrypoint.d/simulation.sh
|
||||||
|
RUN echo 'service cron start' >> /entrypoint.d/simulation.sh
|
||||||
|
RUN chmod +x /entrypoint.d/simulation.sh
|
||||||
|
|
||||||
|
RUN echo '* * * * * root /usr/bin/flock -w 0 /var/cron.lock python3 /root/admin_simulation/admin.py "$ADMIN_PW" > /var/log/admin_simulation.log 2> /var/log/admin_simulation.error' >> /etc/crontab
|
153
web/peak/dist/admin-simulation/admin.py
vendored
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys, requests
|
||||||
|
import os
|
||||||
|
from time import sleep
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from pyvirtualdisplay import Display
|
||||||
|
|
||||||
|
|
||||||
|
import urllib3
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
class AdminAutomation:
|
||||||
|
host = os.environ.get("HOST", "http://web")
|
||||||
|
timeout = int(os.environ.get("TIMEOUT", "5"))
|
||||||
|
driver = None
|
||||||
|
_username = 'admin'
|
||||||
|
_password = ''
|
||||||
|
|
||||||
|
display = Display(visible=0, size=(800, 600))
|
||||||
|
display.start()
|
||||||
|
|
||||||
|
def __init__(self, password:str=''):
|
||||||
|
|
||||||
|
chrome_options = self._set_chrome_options()
|
||||||
|
service = Service(executable_path=r'/usr/bin/chromedriver')
|
||||||
|
self.driver = webdriver.Chrome(service=service, options=chrome_options)
|
||||||
|
self.driver.set_page_load_timeout(self.timeout)
|
||||||
|
|
||||||
|
self._password = password
|
||||||
|
if self._password == '':
|
||||||
|
raise Exception('No password for admin configured!')
|
||||||
|
|
||||||
|
def _set_chrome_options(self):
|
||||||
|
'''
|
||||||
|
Sets chrome options for Selenium:
|
||||||
|
- headless browser is enabled
|
||||||
|
- sandbox is disbaled
|
||||||
|
- dev-shm usage is disabled
|
||||||
|
- SSL certificate errors are ignored
|
||||||
|
'''
|
||||||
|
chrome_options = webdriver.ChromeOptions()
|
||||||
|
|
||||||
|
options = [
|
||||||
|
'--headless',
|
||||||
|
'--no-sandbox', '--disable-dev-shm-usage', '--ignore-certificate-errors',
|
||||||
|
'--disable-extensions', '--no-first-run', '--disable-logging',
|
||||||
|
'--disable-notifications', '--disable-permissions-api', '--hide-scrollbars',
|
||||||
|
'--disable-gpu', '--window-size=800,600', '--disable-xss-auditor'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
for option in options:
|
||||||
|
chrome_options.add_argument(option)
|
||||||
|
return chrome_options
|
||||||
|
|
||||||
|
def login(self) -> bool:
|
||||||
|
'''
|
||||||
|
Login as admin
|
||||||
|
- Returns: `True` if successful and `False` of unsuccessful
|
||||||
|
'''
|
||||||
|
self.driver.get(f'{self.host}/login.php')
|
||||||
|
|
||||||
|
|
||||||
|
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.NAME, 'username')))
|
||||||
|
self.driver.find_element('name', 'username').send_keys('admin')
|
||||||
|
|
||||||
|
|
||||||
|
WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.NAME, 'password')))
|
||||||
|
self.driver.find_element('name', 'password').send_keys(self._password)
|
||||||
|
|
||||||
|
|
||||||
|
WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, 'button')))
|
||||||
|
self.driver.find_element('name', 'button').click()
|
||||||
|
|
||||||
|
|
||||||
|
if self.driver.current_url != f'{self.host}/':
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f'[{datetime.now()}] Successfully logged in!\r\n')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def read_messages(self):
|
||||||
|
|
||||||
|
print(f'[{datetime.now()}] Checking messages...')
|
||||||
|
self.driver.get(f'{self.host}/admin/support.php')
|
||||||
|
|
||||||
|
if self.driver.current_url != f'{self.host}/admin/support.php':
|
||||||
|
raise Exception("Cannot access support.php! Session probably expired!")
|
||||||
|
|
||||||
|
links = [element.get_attribute('href') for element in self.driver.find_elements('name', 'inbox-header')]
|
||||||
|
if len(links) > 0:
|
||||||
|
for link in links:
|
||||||
|
if link:
|
||||||
|
try:
|
||||||
|
self.driver.get(link)
|
||||||
|
|
||||||
|
if self.driver.current_url == link:
|
||||||
|
print(f'[{datetime.now()}] Visiting: {self.driver.current_url}\r\n')
|
||||||
|
else:
|
||||||
|
print(f'[{datetime.now()}] After visiting {link}, got redirect to: {self.driver.current_url}\r\n')
|
||||||
|
except Exception as ex:
|
||||||
|
'''Timeout or other exception occurred on url.
|
||||||
|
'''
|
||||||
|
print(f'[{datetime.now()}] Error after visiting: {link} (Current URL: {self.driver.current_url}). Error: {ex}\r\n')
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
|
||||||
|
if self.driver:
|
||||||
|
self.driver.close()
|
||||||
|
self.driver.quit()
|
||||||
|
self.driver = None
|
||||||
|
if self.display:
|
||||||
|
self.display.stop()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
os.system('pkill -f chrome')
|
||||||
|
os.system('pkill -f Xvfb')
|
||||||
|
|
||||||
|
admin = None
|
||||||
|
try:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
raise Exception('Specify a password!')
|
||||||
|
admin = AdminAutomation(sys.argv[1])
|
||||||
|
|
||||||
|
tries = 0
|
||||||
|
while not admin.login():
|
||||||
|
if tries > 5:
|
||||||
|
raise Exception('Could not login!')
|
||||||
|
tries += 1
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
admin.read_messages()
|
||||||
|
sleep(5)
|
||||||
|
|
||||||
|
admin.close()
|
||||||
|
quit()
|
||||||
|
except Exception as ex:
|
||||||
|
print(f'[-] Error: {ex}')
|
||||||
|
|
||||||
|
if admin is not None:
|
||||||
|
admin.close()
|
||||||
|
quit()
|
16
web/peak/dist/docker-compose.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: .docker/Dockerfile-web
|
||||||
|
image: webserver
|
||||||
|
container_name: webserver
|
||||||
|
restart: always
|
||||||
|
hostname: webserver
|
||||||
|
environment:
|
||||||
|
ADMIN_PW: example-password
|
||||||
|
HOST: http://localhost
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
1
web/peak/dist/flag/flag.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
gctf{SOME_FLAG}
|
0
web/peak/dist/sqlite.db
vendored
Normal file
100
web/peak/dist/web/actions/contact.php
vendored
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
include_once "../includes/session.php";
|
||||||
|
|
||||||
|
function cleanup_old_files()
|
||||||
|
{
|
||||||
|
$currentTimestamp = time();
|
||||||
|
$uploadsDirectory = "../uploads";
|
||||||
|
$files = scandir($uploadsDirectory);
|
||||||
|
if(sizeof($files) > 0)
|
||||||
|
{
|
||||||
|
foreach ($files as $file)
|
||||||
|
{
|
||||||
|
if ($file !== '.' && $file !== '..' && $file !== '.htaccess')
|
||||||
|
{
|
||||||
|
$filePath = $uploadsDirectory . '/' . $file;
|
||||||
|
if (is_file($filePath))
|
||||||
|
{
|
||||||
|
$fileTimestamp = filemtime($filePath);
|
||||||
|
$timeDifference = $currentTimestamp - $fileTimestamp;
|
||||||
|
// Check if the file is older than 5 minutes (300 seconds)
|
||||||
|
if ($timeDifference > 300)
|
||||||
|
unlink($filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SESSION['user']) && $_SESSION['user']['role'] !== "admin")
|
||||||
|
{
|
||||||
|
if(isset($_POST['title']) && isset($_POST['content']))
|
||||||
|
{
|
||||||
|
cleanup_old_files();
|
||||||
|
|
||||||
|
$target_file = "";
|
||||||
|
if(isset($_FILES['image']) && $_FILES['image']['name'] !== "")
|
||||||
|
{
|
||||||
|
$targetDirectory = '/uploads/';
|
||||||
|
|
||||||
|
$timestamp = microtime(true);
|
||||||
|
$timestampStr = str_replace('.', '', sprintf('%0.6f', $timestamp));
|
||||||
|
|
||||||
|
$randomFilename = uniqid() . $timestampStr;
|
||||||
|
$targetFile = ".." . $targetDirectory . $randomFilename;
|
||||||
|
$imageFileType = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
|
||||||
|
$allowedExtensions = ['jpg', 'jpeg', 'png'];
|
||||||
|
|
||||||
|
$check = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$check = @getimagesize($_FILES['image']['tmp_name']);
|
||||||
|
}
|
||||||
|
catch(Exception $exx)
|
||||||
|
{
|
||||||
|
throw new Exception("File is not a valid image!");
|
||||||
|
}
|
||||||
|
if ($check === false)
|
||||||
|
{
|
||||||
|
throw new Exception("File is not a valid image!");
|
||||||
|
}
|
||||||
|
if (!in_array($imageFileType, $allowedExtensions))
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid image file type. Allowed types: jpg, jpeg, png");
|
||||||
|
}
|
||||||
|
if (!move_uploaded_file($_FILES['image']['tmp_name'], $targetFile))
|
||||||
|
{
|
||||||
|
throw new Exception("Error uploading the image! Try again! If this issue persists, contact a CTF admin!");
|
||||||
|
}
|
||||||
|
$target_file = $targetDirectory . $randomFilename;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = $_POST['title'];
|
||||||
|
$content = $_POST['content'];
|
||||||
|
$user_id = $_SESSION['user']['id'];
|
||||||
|
|
||||||
|
$sql = $pdo->prepare("INSERT INTO messages (title, content, file, user_id) VALUES (:title, :content, :file, :user_id)");
|
||||||
|
$sql->bindParam(':title', $title, PDO::PARAM_STR);
|
||||||
|
$sql->bindParam(':content', $content, PDO::PARAM_STR);
|
||||||
|
$sql->bindParam(':user_id', $user_id, PDO::PARAM_INT);
|
||||||
|
$sql->bindParam(':file', $target_file, PDO::PARAM_STR);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$sql->execute();
|
||||||
|
}
|
||||||
|
catch (PDOException $e)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not create request. Please try again! If this issue persists, contact a CTF admin!");
|
||||||
|
}
|
||||||
|
$_SESSION['success'] = "Message received! An admin will handle your request shortly. You can view your request <a name='message' href='/pages/view_message.php?id=" . $pdo->lastInsertId() ."'>here</a>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception $ex)
|
||||||
|
{
|
||||||
|
$_SESSION['error'] = htmlentities($ex->getMessage());
|
||||||
|
}
|
||||||
|
header('Location: /pages/contact.php');
|
53
web/peak/dist/web/actions/login.php
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
include_once "../includes/session.php";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST['username']) && isset($_POST['password']))
|
||||||
|
{
|
||||||
|
$username = $_POST['username'];
|
||||||
|
$password = $_POST['password'];
|
||||||
|
|
||||||
|
if ($username !== "" && $password !== "")
|
||||||
|
{
|
||||||
|
$sql = $pdo->prepare("SELECT * FROM users WHERE username=:name");
|
||||||
|
$sql->bindValue(':name', $username);
|
||||||
|
$sql->execute();
|
||||||
|
$user = $sql->fetch();
|
||||||
|
|
||||||
|
if ($user)
|
||||||
|
{
|
||||||
|
$id = $user["id"];
|
||||||
|
$username = $user["username"];
|
||||||
|
$role = $user["role"];
|
||||||
|
|
||||||
|
if(password_verify($password, $user["password"]))
|
||||||
|
{
|
||||||
|
$_SESSION['user'] = array();
|
||||||
|
$_SESSION['user']['id'] = $id;
|
||||||
|
$_SESSION['user']['username'] = $username;
|
||||||
|
$_SESSION['user']['role'] = $role;
|
||||||
|
header('Location: /');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid username or password!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid username or password!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Username and Password required!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception $ex)
|
||||||
|
{
|
||||||
|
$_SESSION['error'] = htmlentities($ex->getMessage());
|
||||||
|
header('Location: /login.php');
|
||||||
|
}
|
||||||
|
?>
|
5
web/peak/dist/web/actions/logout.php
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
include_once "../includes/session.php";
|
||||||
|
session_destroy();
|
||||||
|
header('Location: /');
|
||||||
|
?>
|
53
web/peak/dist/web/actions/register.php
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
include_once "../includes/session.php";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST['username']) && isset($_POST['password']))
|
||||||
|
{
|
||||||
|
$username = $_POST['username'];
|
||||||
|
$password = $_POST['password'];
|
||||||
|
|
||||||
|
if ($username !== "" && $password !== "")
|
||||||
|
{
|
||||||
|
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
$sql = $pdo->prepare("INSERT INTO users (username, password) VALUES (:username, :password)");
|
||||||
|
$sql->bindParam(':username', $username, PDO::PARAM_STR);
|
||||||
|
$sql->bindParam(':password', $hashedPassword, PDO::PARAM_STR);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$sql->execute();
|
||||||
|
|
||||||
|
$sql = $pdo->prepare("SELECT * FROM users WHERE username=:name");
|
||||||
|
$sql->bindValue(':name', $username);
|
||||||
|
$sql->execute();
|
||||||
|
$user = $sql->fetch();
|
||||||
|
|
||||||
|
if ($user)
|
||||||
|
{
|
||||||
|
$id = $user["id"];
|
||||||
|
$username = $user["username"];
|
||||||
|
$role = $user["role"];
|
||||||
|
|
||||||
|
$_SESSION['user'] = array();
|
||||||
|
$_SESSION['user']['id'] = $id;
|
||||||
|
$_SESSION['user']['username'] = $username;
|
||||||
|
$_SESSION['user']['role'] = $role;
|
||||||
|
header('Location: /');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (PDOException $e)
|
||||||
|
{
|
||||||
|
throw new Exception("Could not register with this username! Try again with a different name.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception $ex)
|
||||||
|
{
|
||||||
|
$_SESSION['error'] = htmlentities($ex->getMessage());
|
||||||
|
header('Location: /login.php');
|
||||||
|
}
|
||||||
|
?>
|
7
web/peak/dist/web/admin/data.example
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<markers>
|
||||||
|
<marker>
|
||||||
|
<lat>47.0748663672</lat>
|
||||||
|
<lon>12.695247219</lon>
|
||||||
|
<name>Großglockner</name>
|
||||||
|
</marker>
|
||||||
|
</markers>
|
27
web/peak/dist/web/admin/includes/session.php
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
include_once "../includes/config.php";
|
||||||
|
|
||||||
|
if(isset($_SESSION['user']))
|
||||||
|
{
|
||||||
|
$sql = $pdo->prepare("SELECT * FROM users WHERE id=:id");
|
||||||
|
$sql->bindValue(':id', $_SESSION['user']['id']);
|
||||||
|
$sql->execute();
|
||||||
|
$user = $sql->fetch();
|
||||||
|
|
||||||
|
if ($user && $user["role"] === "admin")
|
||||||
|
{
|
||||||
|
$id = $user["id"];
|
||||||
|
$username = $user["username"];
|
||||||
|
$role = $user["role"];
|
||||||
|
$_SESSION['user'] = array();
|
||||||
|
$_SESSION['user']['id'] = $id;
|
||||||
|
$_SESSION['user']['username'] = $username;
|
||||||
|
$_SESSION['user']['role'] = $role;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session_destroy();
|
||||||
|
header('Location: /login.php');
|
||||||
|
return;
|
||||||
|
?>
|
87
web/peak/dist/web/admin/map.php
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
include_once "includes/session.php";
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<?php include_once "../includes/header.php"; ?>
|
||||||
|
<body>
|
||||||
|
<?php include_once "../includes/menu.php"; ?>
|
||||||
|
|
||||||
|
<header class="hero bg-primary text-white text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Edit map</h1>
|
||||||
|
<p>Dev-Note: Please note editing map globally is currently not possible! We are working on it.<br>You can use this site to test out the coordinates for now.</p>
|
||||||
|
<form method="post" action="/admin/map.php">
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea class="form-control" name="data" rows="7"><?php $xmlFilePath="./data.example"; echo file_get_contents($xmlFilePath);?></textarea>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<button type="submit" class="btn btn-light btn-lg">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="map" class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="card header">
|
||||||
|
<div class="card-body">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
|
||||||
|
<div id="map" style="height: 500px;"/>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
|
||||||
|
<script>
|
||||||
|
var map = L.map('map').setView([0, 0], 12);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(map);
|
||||||
|
<?php
|
||||||
|
function parseXML($xmlData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
libxml_disable_entity_loader(false);
|
||||||
|
$xml = simplexml_load_string($xmlData, 'SimpleXMLElement', LIBXML_NOENT);
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
catch(Exception $ex)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$xmlData = "";
|
||||||
|
if ($_SERVER["REQUEST_METHOD"] === "POST")
|
||||||
|
{
|
||||||
|
$xmlData = $_POST["data"];
|
||||||
|
if(!parseXML($xmlData))
|
||||||
|
$xmlData = "";
|
||||||
|
}
|
||||||
|
if($xmlData === "")
|
||||||
|
{
|
||||||
|
$xmlData = file_get_contents($xmlFilePath);
|
||||||
|
}
|
||||||
|
$xml = parseXML($xmlData);
|
||||||
|
foreach($xml->marker as $marker)
|
||||||
|
{
|
||||||
|
$name = str_replace("\n", "\\n", $marker->name);
|
||||||
|
echo 'L.marker(["' . $marker->lat . '", "' . $marker->lon.'"]).addTo(map).bindPopup("'. $name. '").openPopup();' . "\n";
|
||||||
|
echo 'map.setView(["' . $marker->lat . '", "' . $marker->lon.'"], 9);' . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception $ex)
|
||||||
|
{
|
||||||
|
echo "Invalid xml data!";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
54
web/peak/dist/web/admin/support.php
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
include_once "includes/session.php";
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<?php
|
||||||
|
include_once "../includes/header.php";
|
||||||
|
include_once "../includes/csp.php";
|
||||||
|
?>
|
||||||
|
<body>
|
||||||
|
<?php include_once "../includes/menu.php"; ?>
|
||||||
|
|
||||||
|
<?php $messages = $pdo->query("SELECT * FROM messages")->fetchAll(PDO::FETCH_ASSOC); ?>
|
||||||
|
|
||||||
|
<header class="hero bg-primary text-white text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Support Requests</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="messages" class="py-5">
|
||||||
|
<div class="container mt-5">
|
||||||
|
<ul class="list-group">
|
||||||
|
<?php if(sizeof($messages) > 0) : ?>
|
||||||
|
<?php foreach ($messages as $message): ?>
|
||||||
|
<li class="list-group-item">
|
||||||
|
<h5 class="mb-1"><?php echo htmlentities($message['title']);?> from
|
||||||
|
<?php
|
||||||
|
$sql = "SELECT u.username
|
||||||
|
FROM messages m
|
||||||
|
JOIN users u ON m.user_id = u.id
|
||||||
|
WHERE m.user_id = :user_id";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->bindParam(':user_id', $message['user_id'], PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo htmlentities($result['username']);
|
||||||
|
?></h5>
|
||||||
|
<a href="/pages/view_message.php?id=<?php echo $message['id']; ?>" name="inbox-header">Inspect Request</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else:?>
|
||||||
|
<div class="container text-center">
|
||||||
|
<h3>No messages available at the moment!</h3>
|
||||||
|
</div>
|
||||||
|
<?php endif;?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
web/peak/dist/web/assets/bootstrap.js
vendored
Normal file
BIN
web/peak/dist/web/assets/img/climbing.png
vendored
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
web/peak/dist/web/assets/img/hiking.png
vendored
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
web/peak/dist/web/assets/img/map.png
vendored
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
web/peak/dist/web/assets/img/peak.png
vendored
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
web/peak/dist/web/assets/img/road.png
vendored
Normal file
After Width: | Height: | Size: 2.2 MiB |
BIN
web/peak/dist/web/assets/img/sightseeing.png
vendored
Normal file
After Width: | Height: | Size: 1.8 MiB |
2
web/peak/dist/web/assets/jquery.js
vendored
Normal file
6
web/peak/dist/web/assets/leaflet.js
vendored
Normal file
6
web/peak/dist/web/assets/popper.js
vendored
Normal file
47
web/peak/dist/web/includes/config.php
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
function get_pdo()
|
||||||
|
{
|
||||||
|
$dsn="sqlite:/var/sqlite/sqlite.db";
|
||||||
|
$pdo = new PDO($dsn, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup_db($pdo)
|
||||||
|
{
|
||||||
|
$already_setup = FALSE;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$already_setup = $pdo->exec("select 1 from users inner join messages on users.id = messages.id");
|
||||||
|
}
|
||||||
|
catch(Exception $ex)
|
||||||
|
{
|
||||||
|
$already_setup = FALSE;
|
||||||
|
}
|
||||||
|
if($already_setup === FALSE)
|
||||||
|
{
|
||||||
|
$commands = [
|
||||||
|
'CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `username` varchar(255) NOT NULL UNIQUE, `password` varchar(255) NOT NULL, `role` varchar(255) NOT NULL default "user");',
|
||||||
|
'CREATE TABLE IF NOT EXISTS `messages` (`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `title` varchar(255) NOT NULL, `content` varchar(512) NOT NULL, `file` varchar(512) NOT NULL default "", `created_at` timestamp default CURRENT_TIMESTAMP NOT NULL, `viewed` BOOLEAN DEFAULT 0, `user_id` INTEGER unsigned NOT NULL, FOREIGN KEY (`user_id`) REFERENCES users(`id`) ON DELETE CASCADE);',
|
||||||
|
'INSERT INTO `users` (`username`, `password`, `role`) VALUES ("admin", "$2y$10$yerhXWb8EZR4MBHT0oOm2e1S2lTheH4zHOWqRIKTKEuVMyiL1Mtl6", "admin");'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach($commands as $command)
|
||||||
|
{
|
||||||
|
$pdo->exec($command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$pdo = get_pdo();
|
||||||
|
setup_db($pdo);
|
||||||
|
}
|
||||||
|
catch(Exception $e)
|
||||||
|
{
|
||||||
|
die("Could not connect to the database! Please report to CTF admin!" . $e->getMessage());
|
||||||
|
}
|
||||||
|
?>
|
3
web/peak/dist/web/includes/csp.php
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Security-policy: script-src 'self'");
|
||||||
|
?>
|
30
web/peak/dist/web/includes/error.php
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
if (isset($_SESSION['success']))
|
||||||
|
{
|
||||||
|
echo '<div id="toast-success-container" class="toast-top-center example">
|
||||||
|
<div id="alert" class="toast-success alert-success hide" role="alert" data-delay="5000" data-autohide="true" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div class="toast-body">
|
||||||
|
' . $_SESSION['success'] . '
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
unset($_SESSION['success']);
|
||||||
|
}
|
||||||
|
else if (isset($_SESSION['error']))
|
||||||
|
{
|
||||||
|
echo '<div id="toast-alert-container" class="toast-top-center example">
|
||||||
|
<div id="alert" class="toast-alert alert-danger hide" role="alert" data-delay="5000" data-autohide="true" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div class="toast-header-alert">
|
||||||
|
<i class="fas fa-2x fa-exclamation-circle mr-2"></i>
|
||||||
|
<strong class="mr-auto">Error</strong>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body">
|
||||||
|
' . $_SESSION['error'] . '
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
unset($_SESSION['error']);
|
||||||
|
}
|
||||||
|
?>
|
9
web/peak/dist/web/includes/footer.php
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<footer class="bg-dark text-white text-center py-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<p>© 2023 Großglockner Peak. All Rights Reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
35
web/peak/dist/web/includes/header.php
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<head>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||||
|
<title>Großglockner</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main content styles */
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
/* Add padding or margin to separate content from footer */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer styles */
|
||||||
|
footer {
|
||||||
|
background-color: #343a40;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<script src="/assets/leaflet.js"></script>
|
||||||
|
<script src="/assets/popper.js"></script>
|
||||||
|
<script src="/assets/bootstrap.js"></script>
|
||||||
|
<script src="/assets/jquery.js"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Nunito:400,600,700" rel="stylesheet">
|
9
web/peak/dist/web/includes/loggedon.php
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
include_once "session.php";
|
||||||
|
|
||||||
|
if(!isset($_SESSION['user']))
|
||||||
|
{
|
||||||
|
$_SESSION['error'] = htmlentities("You need to login to access this resource!");
|
||||||
|
header("Location: /login.php");
|
||||||
|
}
|
53
web/peak/dist/web/includes/menu.php
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="/">Großglockner</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||||
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/pages/directions.php">
|
||||||
|
Directions
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php if(isset($_SESSION['user']) && $_SESSION['user']['role'] == "admin") : ?>
|
||||||
|
<li class="'nav-item"><a class="nav-link" href="/admin/map.php">Edit map</a></li>
|
||||||
|
<?php else:?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/pages/contact.php">
|
||||||
|
Contact Us
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
<?php if(isset($_SESSION['user'])) : ?>
|
||||||
|
<ul class="navbar-nav ms-auto align-items-baseline">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a id="settingsDropdown" class="nav-link" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<?php echo htmlentities($_SESSION['user']['username']); ?>
|
||||||
|
<svg class="ms-2" width="18" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-end animate__slideIn" aria-labelledby="settingsDropdown">
|
||||||
|
<a class="dropdown-item px-4" href="/actions/logout.php" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Log out</a>
|
||||||
|
<form method="POST" id="logout-form" action="/actions/logout.php">
|
||||||
|
<input type="hidden" value="logout" name="logout">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<?php else : ?>
|
||||||
|
<ul class="navbar-nav ms-auto align-items-baseline">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" href="/login.php">
|
||||||
|
Login/Register
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
29
web/peak/dist/web/includes/session.php
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
include_once "config.php";
|
||||||
|
|
||||||
|
if(isset($_SESSION['user']))
|
||||||
|
{
|
||||||
|
$sql = $pdo->prepare("SELECT * FROM users WHERE id=:id");
|
||||||
|
$sql->bindValue(':id', $_SESSION['user']['id']);
|
||||||
|
$sql->execute();
|
||||||
|
$user = $sql->fetch();
|
||||||
|
|
||||||
|
if ($user)
|
||||||
|
{
|
||||||
|
$id = $user["id"];
|
||||||
|
$username = $user["username"];
|
||||||
|
$role = $user["role"];
|
||||||
|
$_SESSION['user'] = array();
|
||||||
|
$_SESSION['user']['id'] = $id;
|
||||||
|
$_SESSION['user']['username'] = $username;
|
||||||
|
$_SESSION['user']['role'] = $role;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
session_destroy();
|
||||||
|
header('Location: /');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
77
web/peak/dist/web/index.php
vendored
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
include_once "includes/session.php";
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<?php
|
||||||
|
include_once "includes/header.php";
|
||||||
|
include_once "includes/csp.php";
|
||||||
|
?>
|
||||||
|
<body>
|
||||||
|
<?php include_once "includes/menu.php"; ?>
|
||||||
|
|
||||||
|
|
||||||
|
<header class="hero bg-primary text-white text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Welcome to the Großglockner</h1>
|
||||||
|
<p>Your Gateway to Adventure and Beauty</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="welcome" class="py-5" style="background-image: url('/assets/img/hiking.png'); background-size: cover;">
|
||||||
|
<div class="container h-100">
|
||||||
|
<div class="row h-100 justify-content-center align-items-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="rounded p-4" style="background-color: rgba(255, 255, 255, 0.7);">
|
||||||
|
<h2>Welcome to the Großglockner</h2>
|
||||||
|
<p>
|
||||||
|
Welcome to the majestic Großglockner, Austria's highest peak, standing tall at 3,798 meters (12,461 feet) above sea level. Nestled within the breathtaking landscapes of the Hohe Tauern National Park, this iconic Alpine destination beckons adventurers, nature lovers, and history enthusiasts alike. The Großglockner offers not only panoramic views of surrounding peaks and valleys but also the opportunity to explore its glaciers, including the renowned Pasterze Glacier. Whether you're an avid climber seeking a challenge or a traveler in search of alpine beauty, the Großglockner has something extraordinary to offer.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="activities" class="bg-light py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Activities</h2>
|
||||||
|
<div class="row centered">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a href="/pages/hiking.php">
|
||||||
|
<div class="card">
|
||||||
|
<img src="/assets/img/map.png" class="card-img-top" alt="Hiking">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Hiking Adventures</h5>
|
||||||
|
<p class="card-text">Explore scenic trails and breathtaking vistas.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a href="/pages/climbing.php">
|
||||||
|
<div class="card">
|
||||||
|
<img src="/assets/img/climbing.png" class="card-img-top" alt="Climbing">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Mountain Climbing</h5>
|
||||||
|
<p class="card-text">Challenge yourself with thrilling ascents.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<a href="/pages/sightseeing.php">
|
||||||
|
<img src="/assets/img/sightseeing.png" class="card-img-top" alt="Sightseeing">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Sightseeing Tours</h5>
|
||||||
|
<p class="card-text">Discover the beauty of the Alps with guided tours.</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
<?php include "./includes/footer.php"?>
|
||||||
|
</html>
|
83
web/peak/dist/web/login.php
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<?php include_once "./includes/config.php"?>
|
||||||
|
<html lang="en">
|
||||||
|
<?php include_once "includes/header.php"?>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-image: url('/assets/img/peak.png');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-light font-sans antialiased d-flex align-items-center">
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<ul class="nav nav-tabs card-header-tabs" id="authTabs">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" id="login-tab" data-toggle="tab" href="#login">Login</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="register-tab" data-toggle="tab" href="#register">Register</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-body tab-content">
|
||||||
|
<div class="tab-pane fade show active" id="login">
|
||||||
|
<h5 class="card-title">Login</h5>
|
||||||
|
<form action="/actions/login.php" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<button type="submit" name="button" class="btn btn-primary">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="register">
|
||||||
|
<h5 class="card-title">Register</h5>
|
||||||
|
<form action="/actions/register.php" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<button type="submit" name="button" class="btn btn-success">Register</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php include "includes/error.php";?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// JavaScript to handle tab switching
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#authTabs a').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).tab('show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
61
web/peak/dist/web/pages/climbing.php
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<?php
|
||||||
|
include_once "../includes/header.php";
|
||||||
|
include_once "../includes/csp.php";
|
||||||
|
?>
|
||||||
|
<body>
|
||||||
|
<?php include_once "../includes/menu.php"; ?>
|
||||||
|
<header class="hero bg-primary text-white text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Climbing the Großglockner</h1>
|
||||||
|
<p>Conquer the Highest Peak in Austria</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="climbing-info" class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h2>Challenging Ascents Await</h2>
|
||||||
|
<p>
|
||||||
|
Climbing the Großglockner is an adventure like no other. At 3,798 meters (12,461 feet) above sea level, it presents a thrilling challenge for mountaineers and climbers from around the world. Nestled within the awe-inspiring landscapes of the Hohe Tauern National Park, this iconic Alpine peak offers not only spectacular views but also a test of your climbing skills and determination.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The ascent of the Großglockner can be demanding, and it requires proper training, equipment, and guidance. However, the reward is unparalleled as you stand on the summit, gazing at the breathtaking vistas of the Austrian Alps.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<img src="/assets/img/climbing.png" alt="Climbing Großglockner" class="img-fluid rounded">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="climbing-details" class="bg-light py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Key Details</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Difficulty</h5>
|
||||||
|
<p>Ascending the Großglockner is considered a challenging endeavor, suitable for experienced climbers.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Routes</h5>
|
||||||
|
<p>There are multiple climbing routes to the summit, each with its own level of difficulty and beauty.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Equipment</h5>
|
||||||
|
<p>Proper climbing gear, including ropes, harnesses, helmets, and crampons, is essential for safety.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>Guided Tours</h5>
|
||||||
|
<p>Consider joining guided climbing tours led by experienced mountaineers for a safe and memorable experience.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
<?php include "../includes/footer.php"?>
|
||||||
|
</html>
|
71
web/peak/dist/web/pages/contact.php
vendored
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<?php
|
||||||
|
include_once "../includes/session.php";
|
||||||
|
include_once "../includes/header.php";
|
||||||
|
include_once "../includes/csp.php";
|
||||||
|
?>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#contact {
|
||||||
|
min-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php include_once "../includes/menu.php"; ?>
|
||||||
|
<header class="hero bg-primary text-white text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Contact Us</h1>
|
||||||
|
<p>We're here to help! </p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="contact" class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<h2>Contact Us</h2>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<form method="post" action="/actions/contact.php" enctype="multipart/form-data">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Title</label>
|
||||||
|
<input type="text" class="form-control" name="title" placeholder="Title" required <?php if(!isset($_SESSION['user'])) echo "disabled"?>>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Message</label>
|
||||||
|
<textarea class="form-control" name="content" rows="4"
|
||||||
|
placeholder="Your Message" required <?php if(!isset($_SESSION['user'])) echo "disabled"?>></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Choose Image</label>
|
||||||
|
<input type="file" class="form-control" id="image" name="image" accept="image/*" <?php if(!isset($_SESSION['user'])) echo "disabled"?>>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary" <?php if(!isset($_SESSION['user'])) echo "disabled"?>>Submit</button>
|
||||||
|
<?php if(!isset($_SESSION['user'])) : ?>
|
||||||
|
<h5>Please register/login first to issue requests!</h5>
|
||||||
|
<?php endif;?>
|
||||||
|
</form>
|
||||||
|
<?php include_once "../includes/error.php"; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p>Your experience on our website is important to us, and we want to ensure it's as smooth and enjoyable as possible. If you have any questions, concerns, or issues while using our site, don't hesitate to reach out to us. Our dedicated team is ready to assist you and will make sure to address your request as soon as possible.</p>
|
||||||
|
|
||||||
|
<h3>Why Contact Us?</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Questions:</strong> Whether you're curious about the history, directions, or activities, we're here to provide answers.</li>
|
||||||
|
<li><strong>Technical Issues:</strong> If you encounter any technical errors, or difficulties navigating our site, let us know so we can swiftly address them.</li>
|
||||||
|
<li><strong>Feedback:</strong> Your feedback helps us improve. Share your thoughts, suggestions, or ideas to make our website even better.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
<?php include "../includes/footer.php"?>
|
||||||
|
</html>
|
61
web/peak/dist/web/pages/directions.php
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
include_once "../includes/session.php";
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<?php include_once "../includes/header.php"; ?>
|
||||||
|
<body>
|
||||||
|
<?php include_once "../includes/menu.php"; ?>
|
||||||
|
|
||||||
|
<header class="hero bg-primary text-white text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Directions to the Großglockner Peak</h1>
|
||||||
|
<p>Reach the peak by car</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="directions" class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h2>About the journey:</h2>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Starting Point:</strong> Vienna, Austria (This is a commonly used starting point, but you can adapt these directions based on your specific location)</li>
|
||||||
|
<li><strong>Destination:</strong> Grossglockner High Alpine Road, Grossglockner Mountain, Austria</li>
|
||||||
|
<li><strong>Distance:</strong> Approximately 340 kilometers (211 miles)</li>
|
||||||
|
<li><strong>Estimated Time:</strong> About 4.5 to 5.5 hours, depending on traffic and road conditions</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Directions:</h2>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li><strong>Head South on A2:</strong> Start your journey by driving south on the A2 Autobahn (motorway) from Vienna. Follow the signs for "Graz" and "Klagenfurt."</li>
|
||||||
|
<li><strong>Continue on A2:</strong> Stay on the A2 Autobahn for about 240 kilometers (149 miles) until you reach the city of Villach.</li>
|
||||||
|
<li><strong>Take Exit 364-Villach-Ossiacher See:</strong> Take the exit onto the A10 Autobahn (Tauern Autobahn) toward "Spittal/Drau" and "Lienz."</li>
|
||||||
|
<li><strong>Continue on A10:</strong> Drive on the A10 Autobahn for approximately 100 kilometers (62 miles) in the direction of Spittal an der Drau and Lienz.</li>
|
||||||
|
<li><strong>Exit at Spittal-Millstätter See:</strong> Take exit 139-Spittal-Millstätter See from A10.</li>
|
||||||
|
<li><strong>Follow B106:</strong> After exiting the A10, follow the B106 road signs toward "Lienz" and "Möllbrücke."</li>
|
||||||
|
<li><strong>Continue on B106:</strong> Stay on the B106 road as it winds through picturesque landscapes. You'll pass towns like Mühldorf, Flattach, and Heiligenblut.</li>
|
||||||
|
<li><strong>Arrival at Grossglockner High Alpine Road:</strong> Eventually, you'll arrive at the entrance to the Grossglockner High Alpine Road. Pay the toll fee (if applicable) and start your scenic drive up the Grossglockner mountain.</li>
|
||||||
|
<li><strong>Taking the High Alpine Road:</strong> Be prepared for steep and winding roads as you ascend the Grossglockner. Enjoy the breathtaking Alpine scenery and make stops at designated viewpoints to capture the stunning vistas.</li>
|
||||||
|
<li><strong>Arrival at the Summit:</strong> Follow the road until you reach the Grossglockner summit area. There, you can explore various viewpoints, visit the visitor center, and take in the magnificent views of the surrounding mountains.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="map" class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="card header">
|
||||||
|
<div class="card-body">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
|
||||||
|
<div id="map" style="height: 500px;"/>
|
||||||
|
<script>var map = L.map('map').setView([0, 0], 9);L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}).addTo(map);L.marker(["47.0748663672","12.695247219"]).addTo(map).bindPopup("Großglockner").openPopup();map.setView(["47.0748663672","12.695247219"], 9);</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
<?php include "../includes/footer.php"?>
|
||||||
|
</html>
|
97
web/peak/dist/web/pages/hiking.php
vendored
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<?php
|
||||||
|
include_once "../includes/header.php";
|
||||||
|
include_once "../includes/csp.php";
|
||||||
|
?>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
#trails {
|
||||||
|
background-image: url('/assets/img/map.png');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<?php include_once "../includes/menu.php"; ?>
|
||||||
|
|
||||||
|
<header class="hero bg-primary text-white text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Großglockner Hiking Trails</h1>
|
||||||
|
<p>Discover the Beauty of Alpine Hiking</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section id="trails" class="bg-light d-flex align-items-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="rounded p-4" style="background-color: rgba(255, 255, 255, 0.7);">
|
||||||
|
<h2 class="text-center">Top 4 Trails</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Trail 1: Summit Ascent</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>Duration:</strong> 6 hours<br>
|
||||||
|
<strong>Length:</strong> 12 km<br>
|
||||||
|
<strong>Elevation Gain:</strong> 1,200 meters<br>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Trail 2: Glacier Exploration</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>Duration:</strong> 4 hours<br>
|
||||||
|
<strong>Length:</strong> 8 km<br>
|
||||||
|
<strong>Elevation Gain:</strong> 800 meters<br>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Trail 3: Valley Loop</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>Duration:</strong> 3 hours<br>
|
||||||
|
<strong>Length:</strong> 6 km<br>
|
||||||
|
<strong>Elevation Gain:</strong> 400 meters<br>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Trail 4: Alpine Meadows</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>Duration:</strong> 5 hours<br>
|
||||||
|
<strong>Length:</strong> 10 km<br>
|
||||||
|
<strong>Elevation Gain:</strong> 600 meters<br>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
<?php include "../includes/footer.php"?>
|
||||||
|
</html>
|
45
web/peak/dist/web/pages/sightseeing.php
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<?php
|
||||||
|
include_once "../includes/header.php";
|
||||||
|
include_once "../includes/csp.php";
|
||||||
|
?>
|
||||||
|
<body>
|
||||||
|
<?php include_once "../includes/menu.php"; ?>
|
||||||
|
<header class="hero bg-primary text-white text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Sightseeing at Großglockner</h1>
|
||||||
|
<p>Discover the Beauty of the Alps</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="sightseeing" class="py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h2>The Beauty of the Alps Awaits</h2>
|
||||||
|
<p>
|
||||||
|
Enjoy a spectacular day of sightseeing at the Großglockner, Austria's highest peak. Nestled within the Hohe Tauern National Park, this iconic Alpine destination offers breathtaking vistas, charming alpine villages, and unforgettable experiences for nature lovers and explorers. Whether you're taking a leisurely drive on the Grossglockner High Alpine Road or embarking on guided tours, you'll be captivated by the awe-inspiring landscapes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<img src="/assets/img/sightseeing.png" alt="Sightseeing 1" class="img-fluid rounded">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<img src="/assets/img/road.png" alt="Sightseeing 2" class="img-fluid rounded">
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h2>Unforgettable Scenic Tours</h2>
|
||||||
|
<p>
|
||||||
|
Our guided sightseeing tours are designed to showcase the natural beauty of the Großglockner region. You'll have the opportunity to explore alpine meadows, pristine lakes, and picturesque valleys. Don't forget to bring your camera to capture the stunning landscapes, and keep an eye out for the diverse wildlife that calls this area home.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
<?php include "../includes/footer.php"?>
|
||||||
|
</html>
|
71
web/peak/dist/web/pages/view_message.php
vendored
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
include_once "../includes/session.php";
|
||||||
|
include_once "../includes/loggedon.php";
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<?php
|
||||||
|
include_once "../includes/header.php";
|
||||||
|
include_once "../includes/csp.php";
|
||||||
|
?>
|
||||||
|
<body>
|
||||||
|
<?php include_once "../includes/menu.php"; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$sql = $pdo->prepare("SELECT * FROM messages WHERE id=:id");
|
||||||
|
$sql->bindValue(':id', $_GET['id']);
|
||||||
|
$sql->execute();
|
||||||
|
$message = $sql->fetch();
|
||||||
|
|
||||||
|
if (!$message)
|
||||||
|
{
|
||||||
|
$_SESSION['error'] = "This message no longer exists. Administrators will remove messages after they have been viewed.";
|
||||||
|
header("Location: /pages/contact.php");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($_SESSION['user']['role'] === "admin" && $message['viewed'] == "1")
|
||||||
|
{
|
||||||
|
header("Location: /admin/support.php");
|
||||||
|
}
|
||||||
|
|
||||||
|
if($_SESSION['user']['role'] !== "admin" && $message['user_id'] !== $_SESSION['user']['id'])
|
||||||
|
{
|
||||||
|
$_SESSION['error'] = "You cannot access this message!";
|
||||||
|
header("Location: /pages/contact.php");
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<header class="hero bg-primary text-white text-center py-5">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Support request</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="message" class="py-5">
|
||||||
|
<div class="container mt-5">
|
||||||
|
<?php if (isset($message)): ?>
|
||||||
|
<h1><?php echo htmlentities($message['title']);?></h1>
|
||||||
|
<p><?php echo $message['content']; ?>
|
||||||
|
<?php if($message['file'] !== "") : ?>
|
||||||
|
<div>
|
||||||
|
<img name="image" src="<?php echo $message['file']?>">
|
||||||
|
</div>
|
||||||
|
<?php endif;?>
|
||||||
|
<?php endif; ?></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php
|
||||||
|
if($_SESSION['user']['role'] === "admin")
|
||||||
|
{
|
||||||
|
$sql = $pdo->prepare("UPDATE messages SET viewed = 1 WHERE id=:id");
|
||||||
|
$sql->bindValue(':id', $message['id']);
|
||||||
|
$sql->execute();
|
||||||
|
|
||||||
|
$sql = $pdo->prepare("DELETE FROM messages WHERE viewed = 1 AND created_at < datetime('now', '-1 minute')");
|
||||||
|
$sql->execute();
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</body>
|
||||||
|
<?php include "../includes/footer.php"?>
|
||||||
|
</html>
|
2
web/peak/dist/web/uploads/.htaccess
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Disable PHP execution in this directory
|
||||||
|
php_flag engine off
|
32
web/where-is-the-scope/.babelrc
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// .babelrc
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"minify",
|
||||||
|
{
|
||||||
|
"booleans": false,
|
||||||
|
"builtIns": false,
|
||||||
|
"consecutiveAdds": false,
|
||||||
|
"deadcode": false,
|
||||||
|
"evaluate": false,
|
||||||
|
"flipComparisons": false,
|
||||||
|
"guards": false,
|
||||||
|
"infinity": false,
|
||||||
|
"mangle": false,
|
||||||
|
"memberExpressions": false,
|
||||||
|
"mergeVars": false,
|
||||||
|
"numericLiterals": false,
|
||||||
|
"propertyLiterals": false,
|
||||||
|
"regexpConstructors": false,
|
||||||
|
"removeConsole": false,
|
||||||
|
"removeDebugger": false,
|
||||||
|
"removeUndefined": false,
|
||||||
|
"replace": false,
|
||||||
|
"simplify": false,
|
||||||
|
"simplifyComparisons": false,
|
||||||
|
"typeConstructors": false,
|
||||||
|
"undefinedToVoid": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
1
web/where-is-the-scope/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
This year we are launching our new GlacierTV website allowing you to play any video from youtube. You can also take some notes while watching them and also restrict the access to those with a 2FA token. Hope you enjoy it.
|
22
web/where-is-the-scope/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.26.0",
|
||||||
|
"babel-preset-env": "^1.7.0",
|
||||||
|
"babel-preset-minify": "^0.5.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run build_client && npm run build_server",
|
||||||
|
"build_client": "babel src/client -d public/assets/js/",
|
||||||
|
"build_server": "babel src/server -d build/",
|
||||||
|
"start": "node build/app.js",
|
||||||
|
"postinstall": "patch-package"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"express-session": "^1.17.3",
|
||||||
|
"otpauth": "^9.1.4",
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
|
"puppeteer": "^20.9.0"
|
||||||
|
}
|
||||||
|
}
|
44
web/where-is-the-scope/patches/babel-generator+6.26.1.patch
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
diff --git a/node_modules/babel-generator/lib/generators/statements.js b/node_modules/babel-generator/lib/generators/statements.js
|
||||||
|
index d74b191..354b3fe 100644
|
||||||
|
--- a/node_modules/babel-generator/lib/generators/statements.js
|
||||||
|
+++ b/node_modules/babel-generator/lib/generators/statements.js
|
||||||
|
@@ -264,7 +264,8 @@ function constDeclarationIdent() {
|
||||||
|
}
|
||||||
|
|
||||||
|
function VariableDeclaration(node, parent) {
|
||||||
|
- this.word(node.kind);
|
||||||
|
+ if(node.kind[0] == "c")
|
||||||
|
+ this.word("var");
|
||||||
|
this.space();
|
||||||
|
|
||||||
|
var hasInits = false;
|
||||||
|
@@ -308,9 +309,27 @@ function VariableDeclarator(node) {
|
||||||
|
this.print(node.id, node);
|
||||||
|
this.print(node.id.typeAnnotation, node);
|
||||||
|
if (node.init) {
|
||||||
|
- this.space();
|
||||||
|
+ this.space()
|
||||||
|
this.token("=");
|
||||||
|
+ this.space()
|
||||||
|
+ this.token("typeof");
|
||||||
|
+ this.token(" ");
|
||||||
|
+ this.print(node.id, node);
|
||||||
|
+ this.space();
|
||||||
|
+ this.token("!==")
|
||||||
|
+ this.space();
|
||||||
|
+ this.token("'undefined'")
|
||||||
|
+ this.space();
|
||||||
|
+ this.token("?");
|
||||||
|
+ this.space();
|
||||||
|
+ this.print(node.id, node);
|
||||||
|
+ this.space();
|
||||||
|
+ this.token(":");
|
||||||
|
this.space();
|
||||||
|
+ if(node.init.type !== "StringLiteral" && node.init.type !== "NumericLiteral" && node.init.type !== "BigIntLiteral" && node.init.type !== "DecimalLiteral" && node.init.type !== "DirectiveLiteral")
|
||||||
|
+ this.token("(");
|
||||||
|
this.print(node.init, node);
|
||||||
|
+ if(node.init.type !== "StringLiteral" && node.init.type !== "NumericLiteral" && node.init.type !== "BigIntLiteral" && node.init.type !== "DecimalLiteral" && node.init.type !== "DirectiveLiteral")
|
||||||
|
+ this.token(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\ No newline at end of file
|
18
web/where-is-the-scope/public/assets/css/style.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
|
||||||
|
background-color: #343230;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navigation {
|
||||||
|
background-color: #403f3b;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes, .notes:focus, .notes:focus-visible {
|
||||||
|
background-color: #403f3b;
|
||||||
|
border: #977201 1px solid;
|
||||||
|
outline: none;
|
||||||
|
color: #3f3;
|
||||||
|
}
|
45
web/where-is-the-scope/public/index.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>GlacierTV</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="/assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="navigation">
|
||||||
|
<div class="offset-md-3 col-md-6 text-center">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i style="color: #ff0000; font-size:20px;" class="bi bi-youtube"></i></span>
|
||||||
|
<input id="searchInput" value="" type="text" class="form-control form-control-sm" placeholder="Paste a link to a Youtube video" aria-label="Search" aria-describedby="search">
|
||||||
|
<span class="input-group-text" id="search"><i class="bi bi-search"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-md-3" style="padding-left: 50px;">
|
||||||
|
<p>You can setup a 2FA token for this session in order to protect your notes.</p>
|
||||||
|
<button id="setup_2fa" class="btn btn-warning btn-sm">Setup 2FA</button>
|
||||||
|
<p id="fa_note" style="display: none; margin-top: 10px; color:#f03b3b">Please keep your 2FA token save and do not share with anyone!<br/>2FA is now activated!</p>
|
||||||
|
<div style="width: 100px" class="text-center mt-2" id="qrcode_2fa"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<iframe width="970" height="600" id="viewer" frameborder="0"></iframe>
|
||||||
|
<br />
|
||||||
|
<a class="btn btn-secondary source-link" href=""><i class="bi bi-youtube"></i> Source</a>
|
||||||
|
<a class="btn btn-danger" id="reportBtn"><i class="bi bi-flag"></i> Report</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<p>You can store here your notes. For security reasons we recommend setting up a 2FA token!</p>
|
||||||
|
<textarea class="notes" name="" id="notes_content" cols="30" rows="10"></textarea>
|
||||||
|
<br /><br />
|
||||||
|
<button class="btn btn-danger" id="notes_submit">Save notes</button>
|
||||||
|
<button class="btn btn-light" id="notes_load">Load notes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- https://davidshimjs.github.io/qrcodejs/ -->
|
||||||
|
<script src="/libs/qrcode.min.js"></script>
|
||||||
|
<script src="/assets/js/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
web/where-is-the-scope/public/libs/qrcode.min.js
vendored
Normal file
BIN
web/where-is-the-scope/src.zip
Normal file
153
web/where-is-the-scope/src/client/index.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
// ###########
|
||||||
|
// ## PLAYER
|
||||||
|
// ###########
|
||||||
|
|
||||||
|
function updateQuery(uri) {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
urlParams.set("source", "youtube");
|
||||||
|
urlParams.set("uri", uri);
|
||||||
|
window.location.search = urlParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromQuery() {
|
||||||
|
const query = new URLSearchParams(window.location.search);
|
||||||
|
const source = query.get("source") || "youtube";
|
||||||
|
const uri = query.get("uri");
|
||||||
|
document.getElementById("searchInput").value = uri || "https://www.youtube.com/embed/dQw4w9WgXcQ?&autoplay=1";
|
||||||
|
if(!uri) return false;
|
||||||
|
updateSource(uri, source);
|
||||||
|
var ifconfig = {
|
||||||
|
pathname: `<iframe frameborder="0" width=950 height=570 src="${parseURI(uri)}"></iframe>`
|
||||||
|
}
|
||||||
|
document.getElementById("viewer").srcdoc = ifconfig.pathname;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseURI(uri) {
|
||||||
|
const uriParts = new URL(uri);
|
||||||
|
if(uriParts.origin === "https://www.youtube.com")
|
||||||
|
return uri;
|
||||||
|
// If user does not provide a youtube uri, we take the default one.
|
||||||
|
return "https://www.youtube.com/embed/dQw4w9WgXcQ?&autoplay=1";
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSource(uri, source_provider) {
|
||||||
|
const source = document.querySelector(".source-link");
|
||||||
|
source.id = source_provider;
|
||||||
|
source.href = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadVideo(videoURI) {
|
||||||
|
const ifconfig = {
|
||||||
|
pathname: `<iframe frameborder="0" width=950 height=570 src="${parseURI(videoURI)}"></iframe>`
|
||||||
|
};
|
||||||
|
document.getElementById("viewer").srcdoc = ifconfig.pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
const input = document.getElementById("searchInput").value;
|
||||||
|
updateQuery(input);
|
||||||
|
updateSource(input, "youtube");
|
||||||
|
loadVideo(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReportClick() {
|
||||||
|
document.getElementById("reportBtn").addEventListener("click", _ => {
|
||||||
|
alert('Thank you for reporting this uri. A moderator will review it soon.')
|
||||||
|
fetch("/report", {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
path: window.location.search
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function __readyPlayer() {
|
||||||
|
document.getElementById("searchInput").addEventListener("keyup", e => {
|
||||||
|
if(e.key === "Enter")
|
||||||
|
onSearch();
|
||||||
|
})
|
||||||
|
document.getElementById("search").addEventListener("click", _ => {
|
||||||
|
onSearch();
|
||||||
|
})
|
||||||
|
if(!loadFromQuery())
|
||||||
|
onSearch();
|
||||||
|
onReportClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ###########
|
||||||
|
// ## 2FA
|
||||||
|
// ###########
|
||||||
|
|
||||||
|
function __on_2fa_click() {
|
||||||
|
document.getElementById("setup_2fa").addEventListener("click", _ => {
|
||||||
|
fetch("/setup_2fa", {
|
||||||
|
method: "POST"
|
||||||
|
}).then(res => {
|
||||||
|
if(res.status === 400) return alert("You already have requested an 2FA token!")
|
||||||
|
return res.json();
|
||||||
|
}).then(json => {
|
||||||
|
if(!json) return;
|
||||||
|
new QRCode(document.getElementById("qrcode_2fa"), json.totp)
|
||||||
|
document.getElementById("fa_note").style.display = "block";
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ###########
|
||||||
|
// ## NOTES
|
||||||
|
// ###########
|
||||||
|
|
||||||
|
function saveNotes() {
|
||||||
|
const notesContent = document.getElementById("notes_content");
|
||||||
|
const message = notesContent.value;
|
||||||
|
const payload = {message};
|
||||||
|
fetch("/secret_note", {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}).then(res => {
|
||||||
|
console.log(res);
|
||||||
|
if(res.status === 204) {
|
||||||
|
alert("Note saved.");
|
||||||
|
} else {
|
||||||
|
alert("Could not save note.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadNotes() {
|
||||||
|
// TODO: Implement load notes
|
||||||
|
alert("This feature is currently under development and will be available soon.")
|
||||||
|
}
|
||||||
|
|
||||||
|
function __onLoadNotes() {
|
||||||
|
const notesLoad = document.getElementById("notes_load");
|
||||||
|
notesLoad.addEventListener("click", _ => {
|
||||||
|
loadNotes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function __onSaveNotes() {
|
||||||
|
const notesSubmit = document.getElementById("notes_submit");
|
||||||
|
notesSubmit.addEventListener("click", _ => {
|
||||||
|
saveNotes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ###########
|
||||||
|
// ## DOM Related Events
|
||||||
|
// ###########
|
||||||
|
|
||||||
|
window.addEventListener("load", _ => {
|
||||||
|
__readyPlayer();
|
||||||
|
__on_2fa_click();
|
||||||
|
__onSaveNotes();
|
||||||
|
__onLoadNotes();
|
||||||
|
});
|
132
web/where-is-the-scope/src/server/app.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
const crypto = require("crypto")
|
||||||
|
const express = require("express")
|
||||||
|
const session = require("express-session")
|
||||||
|
const otpauth = require("otpauth")
|
||||||
|
const bodyParser = require("body-parser")
|
||||||
|
const puppeteer = require("puppeteer")
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const token = getTOTPSecretToken();
|
||||||
|
|
||||||
|
// ##################
|
||||||
|
// # Middleware
|
||||||
|
// ##################
|
||||||
|
|
||||||
|
app.use(express.static("public"))
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
app.use(session({
|
||||||
|
secret: crypto.randomBytes(32).toString("base64"),
|
||||||
|
cookie: {
|
||||||
|
httpOnly: true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ##################
|
||||||
|
// # helper functions
|
||||||
|
// ##################
|
||||||
|
|
||||||
|
|
||||||
|
function getTOTPSecretToken() {
|
||||||
|
var token = otpauth.Secret.fromHex(crypto.randomBytes(32).toString("hex"))
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(function(resolve, _) {
|
||||||
|
setTimeout(_ => resolve(), ms);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ##################
|
||||||
|
// # local database
|
||||||
|
// ##################
|
||||||
|
|
||||||
|
|
||||||
|
const totp_tokens = {}
|
||||||
|
const secret_notes = {}
|
||||||
|
|
||||||
|
// ##################
|
||||||
|
// # Server logic
|
||||||
|
// ##################
|
||||||
|
|
||||||
|
app.post("/setup_2fa", (req, res) => {
|
||||||
|
const sessionId = req.session.id;
|
||||||
|
if(Object.keys(totp_tokens).includes(sessionId)) return res.status(400).send("TOTP already registered for that session!")
|
||||||
|
const totp = new otpauth.TOTP({
|
||||||
|
issuer: "GlacierTV",
|
||||||
|
label: "2FA",
|
||||||
|
algorithm: "SHA3-384",
|
||||||
|
digits: 9,
|
||||||
|
period: 43,
|
||||||
|
secret: getTOTPSecretToken()
|
||||||
|
});
|
||||||
|
totp_tokens[sessionId] = totp
|
||||||
|
res.json({
|
||||||
|
"totp": totp.toString()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/secret_note", (req, res) => {
|
||||||
|
const sessionId = req.session.id;
|
||||||
|
const message = req.body.message;
|
||||||
|
if(typeof message !== "string") return res.status(400).send("No message given");
|
||||||
|
secret_notes[sessionId] = message;
|
||||||
|
res.status(204).end();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/secret_note", (req, res) => {
|
||||||
|
const sessionId = req.session.id;
|
||||||
|
if(Object.keys(totp_tokens).includes(sessionId)) {
|
||||||
|
const token = req.query.token;
|
||||||
|
if(typeof token !== "string") return res.status(400).send("Missing TOTP token in search query.")
|
||||||
|
const delta = totp_tokens[sessionId].validate({token, window: 1})
|
||||||
|
if(delta === null) return res.status(400).send("Invalid TOTP token!")
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
message: secret_notes[sessionId]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// ##################
|
||||||
|
// # Report engine
|
||||||
|
// ##################
|
||||||
|
|
||||||
|
const FLAG = process.env.FLAG || "gctf{dummy}";
|
||||||
|
|
||||||
|
app.post("/report", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const path = req.body.path;
|
||||||
|
if(typeof path !== "string") return res.status(400).send("No path provided");
|
||||||
|
const uri = `http://localhost:8080/${path}`
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: "new",
|
||||||
|
args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-setuid-sandbox"],
|
||||||
|
});
|
||||||
|
const context = await browser.createIncognitoBrowserContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto('http://localhost:8080/');
|
||||||
|
await page.waitForNavigation({
|
||||||
|
waitUntil: 'networkidle0',
|
||||||
|
});
|
||||||
|
await page.evaluate(async message => {
|
||||||
|
await fetch("/setup_2fa", {method: "POST"});
|
||||||
|
await fetch("/secret_note", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({message}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, FLAG)
|
||||||
|
await page.goto(uri);
|
||||||
|
await sleep(5000);
|
||||||
|
await browser.close();
|
||||||
|
res.status(200).send("Thank you for your report. We will check it soon")
|
||||||
|
} catch(err) {
|
||||||
|
console.log(err)
|
||||||
|
res.status(400).send("Something went wrong! If you think this is an error on our site, contact an admin.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(8080);
|