Add CSRF token to all internal session-authenticated API requests.
This commit is contained in:
parent
95a9b8c781
commit
5a2f1a42e5
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -7,8 +7,23 @@ release channel, you can take advantage of these new features and fixes.
|
|||
|
||||
## Code Quality/Technical Changes
|
||||
|
||||
- A number of security fixes are being incorporated into the software as of this version. See below for details.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
## Security Fixes
|
||||
|
||||
- Session cookies are now marked as HTTP-only, avoiding possible use by custom JavaScript that may be injected into a
|
||||
given page.
|
||||
|
||||
- If the "Always Use HTTPS" setting is enabled, session cookies will be sent as "secure only" as well.
|
||||
|
||||
- API calls will now either require API key authentication _or_ both a current active login session and a unique
|
||||
identifier; if you're calling the API externally, you should _always_ use a generated API key and not count on the
|
||||
user's existing session.
|
||||
|
||||
-
|
||||
|
||||
---
|
||||
|
||||
# AzuraCast 0.14.1 (Aug 22, 2021)
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
use App\Environment;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Middleware\Auth\ApiAuth;
|
||||
use App\Session\Csrf;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
/**
|
||||
|
@ -40,19 +42,29 @@ return [
|
|||
],
|
||||
],
|
||||
|
||||
'vue-translations' => [
|
||||
'vue-base' => [
|
||||
'order' => 4,
|
||||
'files' => [
|
||||
'js' => [
|
||||
[
|
||||
'src' => 'dist/VueTranslations.js',
|
||||
'src' => 'dist/VueBase.js',
|
||||
],
|
||||
],
|
||||
],
|
||||
'inline' => [
|
||||
'js' => [
|
||||
function (Request $request) {
|
||||
return 'VueTranslations.default(App.locale);';
|
||||
$csrfJson = 'null';
|
||||
|
||||
$csrf = $request->getAttribute(ServerRequest::ATTR_SESSION_CSRF);
|
||||
if ($csrf instanceof Csrf) {
|
||||
$csrfToken = $csrf->generate(ApiAuth::API_CSRF_NAMESPACE);
|
||||
$csrfJson = json_encode($csrfToken, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
return <<<JS
|
||||
VueBase.default(App.locale, ${csrfJson});
|
||||
JS;
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -76,7 +88,7 @@ return [
|
|||
|
||||
'vue-component-common' => [
|
||||
'order' => 3,
|
||||
'require' => ['vue', 'vue-translations'],
|
||||
'require' => ['vue', 'vue-base'],
|
||||
'files' => [
|
||||
'js' => [
|
||||
[
|
||||
|
|
|
@ -83,7 +83,6 @@ return function (CallableEventDispatcherInterface $dispatcher) {
|
|||
$app->add(Middleware\WrapExceptionsWithRequestData::class);
|
||||
|
||||
$app->add(Middleware\EnforceSecurity::class);
|
||||
$app->add(Middleware\GetCurrentUser::class);
|
||||
|
||||
// Request injection middlewares.
|
||||
$app->add(Middleware\InjectRouter::class);
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
<?php
|
||||
|
||||
use App\Middleware;
|
||||
use Slim\App;
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
|
||||
return function (App $app) {
|
||||
|
||||
call_user_func(include(__DIR__ . '/routes/admin.php'), $app);
|
||||
call_user_func(include(__DIR__ . '/routes/api.php'), $app);
|
||||
call_user_func(include(__DIR__ . '/routes/base.php'), $app);
|
||||
call_user_func(include(__DIR__ . '/routes/public.php'), $app);
|
||||
call_user_func(include(__DIR__ . '/routes/stations.php'), $app);
|
||||
return static function (App $app) {
|
||||
$app->group(
|
||||
'',
|
||||
function (RouteCollectorProxy $group) {
|
||||
call_user_func(include(__DIR__ . '/routes/admin.php'), $group);
|
||||
call_user_func(include(__DIR__ . '/routes/base.php'), $group);
|
||||
call_user_func(include(__DIR__ . '/routes/public.php'), $group);
|
||||
call_user_func(include(__DIR__ . '/routes/stations.php'), $group);
|
||||
}
|
||||
)->add(Middleware\Auth\StandardAuth::class);
|
||||
|
||||
$app->group(
|
||||
'',
|
||||
function (RouteCollectorProxy $group) {
|
||||
call_user_func(include(__DIR__ . '/routes/api.php'), $group);
|
||||
}
|
||||
)->add(Middleware\Auth\ApiAuth::class);
|
||||
};
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
use App\Acl;
|
||||
use App\Controller;
|
||||
use App\Middleware;
|
||||
use Slim\App;
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
|
||||
return function (App $app) {
|
||||
return static function (RouteCollectorProxy $app) {
|
||||
$app->group(
|
||||
'/admin',
|
||||
function (RouteCollectorProxy $group) {
|
||||
|
|
|
@ -5,10 +5,9 @@ use App\Controller;
|
|||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Middleware;
|
||||
use Slim\App;
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
|
||||
return function (App $app) {
|
||||
return static function (RouteCollectorProxy $app) {
|
||||
$app->group(
|
||||
'/api',
|
||||
function (RouteCollectorProxy $group) {
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
|
||||
use App\Controller;
|
||||
use App\Middleware;
|
||||
use Slim\App;
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
|
||||
return function (App $app) {
|
||||
return static function (RouteCollectorProxy $app) {
|
||||
$app->get('/', Controller\Frontend\IndexAction::class)
|
||||
->setName('home');
|
||||
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
|
||||
use App\Controller;
|
||||
use App\Middleware;
|
||||
use Slim\App;
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
|
||||
return function (App $app) {
|
||||
return static function (RouteCollectorProxy $app) {
|
||||
$app->get('/sw.js', Controller\Frontend\PWA\ServiceWorkerAction::class)
|
||||
->setName('public:sw');
|
||||
|
||||
|
|
|
@ -5,10 +5,9 @@ use App\Controller;
|
|||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Middleware;
|
||||
use Slim\App;
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
|
||||
return function (App $app) {
|
||||
return static function (RouteCollectorProxy $app) {
|
||||
$app->group(
|
||||
'/station/{station_id}',
|
||||
function (RouteCollectorProxy $group) {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -64,6 +64,7 @@
|
|||
"store": "^1.3.20",
|
||||
"sweetalert2": "^10.16.6",
|
||||
"vue": "^2.6.12",
|
||||
"vue-axios": "^3.2.5",
|
||||
"vue-gettext": "^2.1.12",
|
||||
"vue-loader": "14.2.2",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
|
|
|
@ -135,7 +135,6 @@
|
|||
<script>
|
||||
import {validationMixin} from "vuelidate";
|
||||
import handleAxiosError from "../../Function/handleAxiosError";
|
||||
import axios from "axios";
|
||||
import CodemirrorTextarea from "../../Common/CodemirrorTextarea";
|
||||
import BWrappedFormGroup from "../../Form/BWrappedFormGroup";
|
||||
|
||||
|
@ -196,7 +195,7 @@ export default {
|
|||
this.$v.form.$reset();
|
||||
this.loading = true;
|
||||
|
||||
axios.get(this.apiUrl).then((resp) => {
|
||||
this.axios.get(this.apiUrl).then((resp) => {
|
||||
this.populateForm(resp.data);
|
||||
this.loading = false;
|
||||
}).catch((error) => {
|
||||
|
@ -224,7 +223,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
axios({
|
||||
this.axios({
|
||||
method: 'PUT',
|
||||
url: this.apiUrl,
|
||||
data: this.form
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import handleAxiosError from "../../Function/handleAxiosError";
|
||||
|
||||
export default {
|
||||
|
@ -44,7 +43,7 @@ export default {
|
|||
this.file = null;
|
||||
this.loading = true;
|
||||
|
||||
axios.get(this.apiUrl).then((resp) => {
|
||||
this.axios.get(this.apiUrl).then((resp) => {
|
||||
this.isUploaded = resp.data.is_uploaded;
|
||||
this.url = resp.data.url;
|
||||
|
||||
|
@ -55,7 +54,7 @@ export default {
|
|||
|
||||
},
|
||||
clear() {
|
||||
axios.delete(this.apiUrl).then((resp) => {
|
||||
this.axios.delete(this.apiUrl).then((resp) => {
|
||||
this.relist();
|
||||
}).catch((error) => {
|
||||
handleAxiosError(error);
|
||||
|
@ -70,7 +69,7 @@ export default {
|
|||
let formData = new FormData();
|
||||
formData.append('file', this.file);
|
||||
|
||||
axios.post(this.apiUrl, formData).then((resp) => {
|
||||
this.axios.post(this.apiUrl, formData).then((resp) => {
|
||||
this.relist();
|
||||
}).catch((error) => {
|
||||
handleAxiosError(error);
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
|
||||
<script>
|
||||
import DataTable from '../Common/DataTable';
|
||||
import axios from 'axios';
|
||||
import EditModal from './StorageLocations/EditModal';
|
||||
import Icon from '../Common/Icon';
|
||||
import handleAxiosError from '../Function/handleAxiosError';
|
||||
|
@ -115,7 +114,7 @@ export default {
|
|||
delay: 3000
|
||||
});
|
||||
|
||||
axios.put(url).then((resp) => {
|
||||
this.axios.put(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.relist();
|
||||
|
@ -135,7 +134,7 @@ export default {
|
|||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.relist();
|
||||
|
|
|
@ -20,8 +20,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { validationMixin } from 'vuelidate';
|
||||
import {validationMixin} from 'vuelidate';
|
||||
import required from 'vuelidate/src/validators/required';
|
||||
import InvisibleSubmitButton from '../../Common/InvisibleSubmitButton';
|
||||
import BaseEditModal from '../../Common/BaseEditModal';
|
||||
|
@ -126,7 +125,7 @@ export default {
|
|||
let data = this.form;
|
||||
data.type = this.type;
|
||||
|
||||
axios({
|
||||
this.axios({
|
||||
method: (this.isEditMode)
|
||||
? 'PUT'
|
||||
: 'POST',
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { validationMixin } from 'vuelidate';
|
||||
import {validationMixin} from 'vuelidate';
|
||||
import handleAxiosError from '../Function/handleAxiosError';
|
||||
|
||||
export default {
|
||||
|
@ -56,7 +55,7 @@ export default {
|
|||
this.doLoad(recordUrl);
|
||||
},
|
||||
doLoad (recordUrl) {
|
||||
axios.get(recordUrl).then((resp) => {
|
||||
this.axios.get(recordUrl).then((resp) => {
|
||||
this.populateForm(resp.data);
|
||||
this.loading = false;
|
||||
}).catch((error) => {
|
||||
|
@ -88,7 +87,7 @@ export default {
|
|||
|
||||
this.error = null;
|
||||
|
||||
axios(this.buildSubmitRequest()).then((resp) => {
|
||||
this.axios(this.buildSubmitRequest()).then((resp) => {
|
||||
let notifyMessage = this.$gettext('Changes saved.');
|
||||
notify('<b>' + notifyMessage + '</b>', 'success');
|
||||
|
||||
|
|
|
@ -146,7 +146,6 @@ table.b-table-selectable {
|
|||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import store from 'store';
|
||||
import _ from 'lodash';
|
||||
import Icon from './Icon';
|
||||
|
@ -385,7 +384,7 @@ export default {
|
|||
requestConfig = this.requestConfig(requestConfig);
|
||||
}
|
||||
|
||||
axios.get(ctx.apiUrl, requestConfig).then((resp) => {
|
||||
this.axios.get(ctx.apiUrl, requestConfig).then((resp) => {
|
||||
this.flushCache = false;
|
||||
this.totalRows = resp.data.total;
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template></template>
|
||||
<script>
|
||||
import NowPlaying from '../Entity/NowPlaying';
|
||||
import axios from 'axios';
|
||||
import NchanSubscriber from 'nchan';
|
||||
|
||||
export const nowPlayingProps = {
|
||||
|
@ -48,7 +47,7 @@ export default {
|
|||
});
|
||||
this.nchan_subscriber.start();
|
||||
} else {
|
||||
axios.get(this.nowPlayingUri).then((response) => {
|
||||
this.axios.get(this.nowPlayingUri).then((response) => {
|
||||
this.setNowPlaying(response.data);
|
||||
|
||||
setTimeout(this.checkNowPlaying, 15000);
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
import WaveSurfer from 'wavesurfer.js';
|
||||
import timeline from 'wavesurfer.js/dist/plugin/wavesurfer.timeline.js';
|
||||
import regions from 'wavesurfer.js/dist/plugin/wavesurfer.regions.js';
|
||||
import axios from 'axios';
|
||||
import getLogarithmicVolume from '../Function/GetLogarithmicVolume.js';
|
||||
import store from 'store';
|
||||
import Icon from './Icon';
|
||||
|
@ -92,7 +91,7 @@ export default {
|
|||
this.$emit('ready');
|
||||
});
|
||||
|
||||
axios.get(this.waveformUrl).then((resp) => {
|
||||
this.axios.get(this.waveformUrl).then((resp) => {
|
||||
let waveform = resp.data;
|
||||
if (waveform.data) {
|
||||
this.wavesurfer.load(this.audioUrl, waveform.data);
|
||||
|
|
|
@ -158,7 +158,6 @@
|
|||
<script>
|
||||
import TimeSeriesChart from './Common/TimeSeriesChart';
|
||||
import DataTable from './Common/DataTable';
|
||||
import axios from 'axios';
|
||||
import store from 'store';
|
||||
import Icon from './Common/Icon';
|
||||
import Avatar, {avatarProps} from './Common/Avatar';
|
||||
|
@ -227,7 +226,7 @@ export default {
|
|||
}
|
||||
|
||||
if (this.showCharts) {
|
||||
axios.get(this.chartsUrl).then((response) => {
|
||||
this.axios.get(this.chartsUrl).then((response) => {
|
||||
this.chartsData = response.data;
|
||||
this.chartsLoading = false;
|
||||
}).catch((error) => {
|
||||
|
@ -235,7 +234,7 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
axios.get(this.notificationsUrl).then((response) => {
|
||||
this.axios.get(this.notificationsUrl).then((response) => {
|
||||
this.notifications = response.data;
|
||||
this.notificationsLoading = false;
|
||||
}).catch((error) => {
|
||||
|
@ -256,7 +255,7 @@ export default {
|
|||
this.$eventHub.$emit('player_toggle', url);
|
||||
},
|
||||
updateNowPlaying () {
|
||||
axios.get(this.stationsUrl).then((response) => {
|
||||
this.axios.get(this.stationsUrl).then((response) => {
|
||||
this.stationsLoading = false;
|
||||
this.stations = response.data;
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ img.album_art {
|
|||
|
||||
<script>
|
||||
import DataTable from '../Common/DataTable';
|
||||
import axios from 'axios';
|
||||
import _ from 'lodash';
|
||||
import AlbumArt from '../Common/AlbumArt';
|
||||
import handleAxiosError from '../Function/handleAxiosError';
|
||||
|
@ -92,7 +91,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
doSubmitRequest (url) {
|
||||
axios.post(url).then((resp) => {
|
||||
this.axios.post(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
this.$emit('submitted');
|
||||
}).catch((err) => {
|
||||
|
|
|
@ -25,8 +25,7 @@
|
|||
</b-modal>
|
||||
</template>
|
||||
<script>
|
||||
import { validationMixin } from 'vuelidate';
|
||||
import axios from 'axios';
|
||||
import {validationMixin} from 'vuelidate';
|
||||
import required from 'vuelidate/src/validators/required';
|
||||
import _ from 'lodash';
|
||||
import MediaFormBasicInfo from './Form/BasicInfo';
|
||||
|
@ -136,7 +135,7 @@ export default {
|
|||
this.recordUrl = recordUrl;
|
||||
this.audioUrl = audioUrl;
|
||||
|
||||
axios.get(recordUrl).then((resp) => {
|
||||
this.axios.get(recordUrl).then((resp) => {
|
||||
let d = resp.data;
|
||||
|
||||
this.songLength = d.length_text;
|
||||
|
@ -189,7 +188,7 @@ export default {
|
|||
|
||||
this.error = null;
|
||||
|
||||
axios.put(this.recordUrl, this.form).then((resp) => {
|
||||
this.axios.put(this.recordUrl, this.form).then((resp) => {
|
||||
let notifyMessage = this.$gettext('Changes saved.');
|
||||
notify('<b>' + notifyMessage + '</b>', 'success');
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'MediaFormAlbumArt',
|
||||
|
@ -55,7 +54,7 @@ export default {
|
|||
let formData = new FormData();
|
||||
formData.append('art', this.artFile);
|
||||
|
||||
axios.post(this.albumArtUrl, formData).then((resp) => {
|
||||
this.axios.post(this.albumArtUrl, formData).then((resp) => {
|
||||
this.reloadArt();
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
|
@ -63,7 +62,7 @@ export default {
|
|||
});
|
||||
},
|
||||
deleteArt () {
|
||||
axios.delete(this.albumArtUrl).then((resp) => {
|
||||
this.axios.delete(this.albumArtUrl).then((resp) => {
|
||||
this.reloadArt();
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
|
|
|
@ -72,7 +72,6 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import _ from 'lodash';
|
||||
import Icon from '../../Common/Icon';
|
||||
import handleAxiosError from '../../Function/handleAxiosError';
|
||||
|
@ -162,7 +161,7 @@ export default {
|
|||
if (this.selectedItems.all.length) {
|
||||
this.notifyPending();
|
||||
|
||||
axios.put(this.batchUrl, {
|
||||
this.axios.put(this.batchUrl, {
|
||||
'do': action,
|
||||
'current_directory': this.currentDirectory,
|
||||
'files': this.selectedItems.files,
|
||||
|
@ -195,7 +194,7 @@ export default {
|
|||
if (this.selectedItems.all.length) {
|
||||
this.notifyPending();
|
||||
|
||||
axios.put(this.batchUrl, {
|
||||
this.axios.put(this.batchUrl, {
|
||||
'do': 'playlist',
|
||||
'playlists': this.checkedPlaylists,
|
||||
'new_playlist_name': this.newPlaylist,
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
</template>
|
||||
<script>
|
||||
import DataTable from '../../Common/DataTable.vue';
|
||||
import axios from 'axios';
|
||||
import _ from 'lodash';
|
||||
import Icon from '../../Common/Icon';
|
||||
import handleAxiosError from '../../Function/handleAxiosError';
|
||||
|
@ -79,7 +78,7 @@ export default {
|
|||
this.$refs.modal.hide();
|
||||
},
|
||||
doMove () {
|
||||
(this.selectedItems.all.length) && axios.put(this.batchUrl, {
|
||||
(this.selectedItems.all.length) && this.axios.put(this.batchUrl, {
|
||||
'do': 'move',
|
||||
'currentDirectory': this.currentDirectory,
|
||||
'directory': this.destinationDirectory,
|
||||
|
|
|
@ -23,9 +23,8 @@
|
|||
</b-modal>
|
||||
</template>
|
||||
<script>
|
||||
import { validationMixin } from 'vuelidate';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import axios from 'axios';
|
||||
import {validationMixin} from 'vuelidate';
|
||||
import {required} from 'vuelidate/lib/validators';
|
||||
import handleAxiosError from '../../Function/handleAxiosError';
|
||||
|
||||
export default {
|
||||
|
@ -35,7 +34,7 @@ export default {
|
|||
currentDirectory: String,
|
||||
mkdirUrl: String
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
newDirectory: null
|
||||
};
|
||||
|
@ -62,7 +61,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
axios.post(this.mkdirUrl, {
|
||||
this.axios.post(this.mkdirUrl, {
|
||||
'currentDirectory': this.currentDirectory,
|
||||
'name': this.newDirectory
|
||||
}).then((resp) => {
|
||||
|
|
|
@ -23,9 +23,8 @@
|
|||
</b-modal>
|
||||
</template>
|
||||
<script>
|
||||
import { validationMixin } from 'vuelidate';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import axios from 'axios';
|
||||
import {validationMixin} from 'vuelidate';
|
||||
import {required} from 'vuelidate/lib/validators';
|
||||
import handleAxiosError from '../../Function/handleAxiosError';
|
||||
|
||||
export default {
|
||||
|
@ -34,7 +33,7 @@ export default {
|
|||
props: {
|
||||
renameUrl: String
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
file: null,
|
||||
|
@ -71,7 +70,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
axios.put(this.renameUrl, this.form).then((resp) => {
|
||||
this.axios.put(this.renameUrl, this.form).then((resp) => {
|
||||
this.$refs.modal.hide();
|
||||
this.$emit('relist');
|
||||
}).catch((err) => {
|
||||
|
|
|
@ -59,7 +59,6 @@
|
|||
|
||||
<script>
|
||||
import DataTable from '../Common/DataTable';
|
||||
import axios from 'axios';
|
||||
import EditModal from './Mounts/EditModal';
|
||||
import Icon from '../Common/Icon';
|
||||
import InfoCard from '../Common/InfoCard';
|
||||
|
@ -114,7 +113,7 @@ export default {
|
|||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.relist();
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import handleAxiosError from '../../../Function/handleAxiosError';
|
||||
import FlowUpload from '../../../Common/FlowUpload';
|
||||
|
||||
|
@ -82,7 +81,7 @@ export default {
|
|||
},
|
||||
deleteIntro() {
|
||||
if (this.editIntroUrl) {
|
||||
axios.delete(this.editIntroUrl).then((resp) => {
|
||||
this.axios.delete(this.editIntroUrl).then((resp) => {
|
||||
this.hasIntro = false;
|
||||
}).catch((err) => {
|
||||
handleAxiosError(err);
|
||||
|
|
|
@ -131,7 +131,6 @@ import EditModal from './Playlists/EditModal';
|
|||
import ReorderModal from './Playlists/ReorderModal';
|
||||
import ImportModal from './Playlists/ImportModal';
|
||||
import QueueModal from './Playlists/QueueModal';
|
||||
import axios from 'axios';
|
||||
import Icon from '../Common/Icon';
|
||||
import handleAxiosError from '../Function/handleAxiosError';
|
||||
import CloneModal from './Playlists/CloneModal';
|
||||
|
@ -260,7 +259,7 @@ export default {
|
|||
delay: 3000
|
||||
});
|
||||
|
||||
axios.put(url).then((resp) => {
|
||||
this.axios.put(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.relist();
|
||||
|
@ -283,7 +282,7 @@ export default {
|
|||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.relist();
|
||||
|
|
|
@ -47,9 +47,8 @@
|
|||
<script>
|
||||
import required from 'vuelidate/src/validators/required';
|
||||
import InvisibleSubmitButton from '../../Common/InvisibleSubmitButton';
|
||||
import axios from 'axios';
|
||||
import handleAxiosError from '../../Function/handleAxiosError';
|
||||
import { validationMixin } from 'vuelidate';
|
||||
import {validationMixin} from 'vuelidate';
|
||||
|
||||
export default {
|
||||
name: 'CloneModal',
|
||||
|
@ -102,7 +101,7 @@ export default {
|
|||
|
||||
this.error = null;
|
||||
|
||||
axios({
|
||||
this.axios({
|
||||
method: 'POST',
|
||||
url: this.cloneUrl,
|
||||
data: this.form
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import InvisibleSubmitButton from '../../Common/InvisibleSubmitButton';
|
||||
|
||||
export default {
|
||||
|
@ -53,7 +52,7 @@ export default {
|
|||
let formData = new FormData();
|
||||
formData.append('playlist_file', this.playlistFile);
|
||||
|
||||
axios.post(this.importPlaylistUrl, formData).then((resp) => {
|
||||
this.axios.post(this.importPlaylistUrl, formData).then((resp) => {
|
||||
if (resp.data.success) {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
} else {
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import handleAxiosError from '../../Function/handleAxiosError';
|
||||
|
||||
export default {
|
||||
|
@ -54,7 +53,7 @@ export default {
|
|||
this.queueUrl = queueUrl;
|
||||
this.loading = true;
|
||||
|
||||
axios.get(this.queueUrl).then((resp) => {
|
||||
this.axios.get(this.queueUrl).then((resp) => {
|
||||
this.media = resp.data;
|
||||
this.loading = false;
|
||||
}).catch((err) => {
|
||||
|
@ -62,7 +61,7 @@ export default {
|
|||
});
|
||||
},
|
||||
doClear () {
|
||||
axios.delete(this.queueUrl).then((resp) => {
|
||||
this.axios.delete(this.queueUrl).then((resp) => {
|
||||
notify('<b>' + this.$gettext('Playlist queue cleared.') + '</b>', 'success');
|
||||
this.close();
|
||||
}).catch((err) => {
|
||||
|
|
|
@ -41,7 +41,6 @@ table.sortable {
|
|||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import Draggable from 'vuedraggable';
|
||||
import Icon from '../../Common/Icon';
|
||||
import handleAxiosError from '../../Function/handleAxiosError';
|
||||
|
@ -76,7 +75,7 @@ export default {
|
|||
this.reorderUrl = reorderUrl;
|
||||
this.loading = true;
|
||||
|
||||
axios.get(this.reorderUrl).then((resp) => {
|
||||
this.axios.get(this.reorderUrl).then((resp) => {
|
||||
this.media = resp.data;
|
||||
this.loading = false;
|
||||
}).catch((err) => {
|
||||
|
@ -100,7 +99,7 @@ export default {
|
|||
newOrder[row.id] = i;
|
||||
});
|
||||
|
||||
axios.put(this.reorderUrl, { 'order': newOrder }).then((resp) => {
|
||||
this.axios.put(this.reorderUrl, {'order': newOrder}).then((resp) => {
|
||||
notify('<b>' + this.$gettext('Playlist order set.') + '</b>', 'success');
|
||||
}).catch((err) => {
|
||||
handleAxiosError(err);
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import handleAxiosError from '../../../Function/handleAxiosError';
|
||||
|
||||
export default {
|
||||
|
@ -66,7 +65,7 @@ export default {
|
|||
let formData = new FormData();
|
||||
formData.append('art', file);
|
||||
|
||||
axios.post(url, formData).then((resp) => {
|
||||
this.axios.post(url, formData).then((resp) => {
|
||||
this.$emit('input', resp.data);
|
||||
}).catch((err) => {
|
||||
handleAxiosError(err);
|
||||
|
@ -74,7 +73,7 @@ export default {
|
|||
},
|
||||
deleteArt () {
|
||||
if (this.editArtUrl) {
|
||||
axios.delete(this.editArtUrl).then((resp) => {
|
||||
this.axios.delete(this.editArtUrl).then((resp) => {
|
||||
this.src = null;
|
||||
}).catch((err) => {
|
||||
handleAxiosError(err);
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import handleAxiosError from '../../../Function/handleAxiosError';
|
||||
import FlowUpload from '../../../Common/FlowUpload';
|
||||
|
||||
|
@ -82,7 +81,7 @@ export default {
|
|||
},
|
||||
deleteMedia () {
|
||||
if (this.editMediaUrl) {
|
||||
axios.delete(this.editMediaUrl).then((resp) => {
|
||||
this.axios.delete(this.editMediaUrl).then((resp) => {
|
||||
this.hasMedia = false;
|
||||
}).catch((err) => {
|
||||
handleAxiosError(err);
|
||||
|
|
|
@ -70,7 +70,6 @@
|
|||
<script>
|
||||
import DataTable from './../../Common/DataTable';
|
||||
import EditModal from './EpisodeEditModal';
|
||||
import axios from 'axios';
|
||||
import Icon from '../../Common/Icon';
|
||||
import AlbumArt from '../../Common/AlbumArt';
|
||||
import EpisodeFormBasicInfo from './EpisodeForm/BasicInfo';
|
||||
|
@ -142,7 +141,7 @@ export default {
|
|||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.relist();
|
||||
|
|
|
@ -62,7 +62,6 @@
|
|||
<script>
|
||||
import DataTable from '../../Common/DataTable';
|
||||
import EditModal from './PodcastEditModal';
|
||||
import axios from 'axios';
|
||||
import AlbumArt from '../../Common/AlbumArt';
|
||||
import handleAxiosError from '../../Function/handleAxiosError';
|
||||
|
||||
|
@ -139,7 +138,7 @@ export default {
|
|||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.relist();
|
||||
|
|
|
@ -38,19 +38,18 @@
|
|||
|
||||
<script>
|
||||
import ProfileStreams from './Profile/StreamsPanel';
|
||||
import ProfileHeader, { profileHeaderProps } from './Profile/HeaderPanel';
|
||||
import ProfileNowPlaying, { profileNowPlayingProps } from './Profile/NowPlayingPanel';
|
||||
import ProfileHeader, {profileHeaderProps} from './Profile/HeaderPanel';
|
||||
import ProfileNowPlaying, {profileNowPlayingProps} from './Profile/NowPlayingPanel';
|
||||
import ProfileSchedule from './Profile/SchedulePanel';
|
||||
import ProfileRequests, { profileRequestsProps } from './Profile/RequestsPanel';
|
||||
import ProfileStreamers, { profileStreamersProps } from './Profile/StreamersPanel';
|
||||
import ProfilePublicPages, { profilePublicProps } from './Profile/PublicPagesPanel';
|
||||
import ProfileFrontend, { profileFrontendProps } from './Profile/FrontendPanel';
|
||||
import ProfileRequests, {profileRequestsProps} from './Profile/RequestsPanel';
|
||||
import ProfileStreamers, {profileStreamersProps} from './Profile/StreamersPanel';
|
||||
import ProfilePublicPages, {profilePublicProps} from './Profile/PublicPagesPanel';
|
||||
import ProfileFrontend, {profileFrontendProps} from './Profile/FrontendPanel';
|
||||
import ProfileBackendNone from './Profile/BackendNonePanel';
|
||||
import ProfileBackend, { profileBackendProps } from './Profile/BackendPanel';
|
||||
import { profileEmbedModalProps } from './Profile/EmbedModal';
|
||||
import { BACKEND_NONE, FRONTEND_REMOTE } from '../Entity/RadioAdapters.js';
|
||||
import ProfileBackend, {profileBackendProps} from './Profile/BackendPanel';
|
||||
import {profileEmbedModalProps} from './Profile/EmbedModal';
|
||||
import {BACKEND_NONE, FRONTEND_REMOTE} from '../Entity/RadioAdapters.js';
|
||||
import NowPlaying from '../Entity/NowPlaying';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -131,7 +130,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
checkNowPlaying () {
|
||||
axios.get(this.profileApiUri).then((response) => {
|
||||
this.axios.get(this.profileApiUri).then((response) => {
|
||||
let np = response.data;
|
||||
np.loading = false;
|
||||
this.np = np;
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
|
||||
<script>
|
||||
import DataTable from '../Common/DataTable';
|
||||
import axios from 'axios';
|
||||
import QueueLogsModal from './Queue/LogsModal';
|
||||
import handleAxiosError from '../Function/handleAxiosError';
|
||||
|
||||
|
@ -97,7 +96,7 @@ export default {
|
|||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.$refs.datatable.refresh();
|
||||
|
|
|
@ -54,7 +54,6 @@
|
|||
|
||||
<script>
|
||||
import DataTable from '../Common/DataTable';
|
||||
import axios from 'axios';
|
||||
import EditModal from './Mounts/EditModal';
|
||||
import Icon from '../Common/Icon';
|
||||
import InfoCard from '../Common/InfoCard';
|
||||
|
@ -108,7 +107,7 @@ export default {
|
|||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.relist();
|
||||
|
|
|
@ -164,7 +164,6 @@
|
|||
<script>
|
||||
import TimeSeriesChart from '../../Common/TimeSeriesChart';
|
||||
import DataTable from '../../Common/DataTable';
|
||||
import axios from 'axios';
|
||||
import Icon from '../../Common/Icon';
|
||||
import Avatar, {avatarProps} from '../../Common/Avatar';
|
||||
import DayOfWeekChart from './DayOfWeekChart';
|
||||
|
@ -220,21 +219,21 @@ export default {
|
|||
created () {
|
||||
moment.tz.setDefault('UTC');
|
||||
|
||||
axios.get(this.chartsUrl).then((response) => {
|
||||
this.axios.get(this.chartsUrl).then((response) => {
|
||||
this.chartsData = response.data;
|
||||
this.chartsLoading = false;
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
axios.get(this.bestAndWorstUrl).then((response) => {
|
||||
this.axios.get(this.bestAndWorstUrl).then((response) => {
|
||||
this.bestAndWorst = response.data;
|
||||
this.bestAndWorstLoading = false;
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
axios.get(this.mostPlayedUrl).then((response) => {
|
||||
this.axios.get(this.mostPlayedUrl).then((response) => {
|
||||
this.mostPlayed = response.data;
|
||||
this.mostPlayedLoading = false;
|
||||
}).catch((error) => {
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
|
||||
<script>
|
||||
import DataTable from '../Common/DataTable';
|
||||
import axios from 'axios';
|
||||
import EditModal from './Streamers/EditModal';
|
||||
import BroadcastsModal from './Streamers/BroadcastsModal';
|
||||
import Schedule from '../Common/ScheduleView';
|
||||
|
@ -129,7 +128,7 @@ export default {
|
|||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.relist();
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
</template>
|
||||
<script>
|
||||
import DataTable from '../../Common/DataTable.vue';
|
||||
import axios from 'axios';
|
||||
import formatFileSize from '../../Function/FormatFileSize.js';
|
||||
import InlinePlayer from '../../InlinePlayer';
|
||||
import Icon from '../../Common/Icon';
|
||||
|
@ -137,7 +136,7 @@ export default {
|
|||
focusCancel: true
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
axios.delete(url).then((resp) => {
|
||||
this.axios.delete(url).then((resp) => {
|
||||
notify('<b>' + resp.data.message + '</b>', 'success');
|
||||
|
||||
this.$refs.datatable.refresh();
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import axios
|
||||
from 'axios';
|
||||
import VueAxios
|
||||
from 'vue-axios';
|
||||
import GetTextPlugin
|
||||
from 'vue-gettext';
|
||||
import translations
|
||||
from '../../resources/locale/translations';
|
||||
|
||||
export default function (lang) {
|
||||
export default function (lang, csrf) {
|
||||
// Configure localization
|
||||
Vue.use(GetTextPlugin, {
|
||||
defaultLanguage: 'en_US',
|
||||
translations: translations,
|
||||
|
@ -11,4 +16,9 @@ export default function (lang) {
|
|||
});
|
||||
|
||||
Vue.config.language = lang;
|
||||
|
||||
// Configure auto-CSRF on requests
|
||||
axios.defaults.headers.common['X-API-CSRF'] = csrf;
|
||||
|
||||
Vue.use(VueAxios, axios);
|
||||
}
|
|
@ -4,7 +4,7 @@ const WebpackAssetsManifest = require('webpack-assets-manifest');
|
|||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
VueTranslations: './vue/VueTranslations.js',
|
||||
VueBase: './vue/VueBase.js',
|
||||
InlinePlayer: './vue/InlinePlayer.vue',
|
||||
Dashboard: './vue/Dashboard.vue',
|
||||
AdminBranding: './vue/Admin/Branding.vue',
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middleware\Auth;
|
||||
|
||||
use App\Acl;
|
||||
use App\Customization;
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
abstract class AbstractAuth implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected Entity\Repository\SettingsRepository $settingsRepo,
|
||||
protected Environment $environment,
|
||||
protected Acl $acl
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
// Initialize Customization (timezones, locales, etc) based on the current logged in user.
|
||||
$customization = new Customization(
|
||||
environment: $this->environment,
|
||||
settingsRepo: $this->settingsRepo,
|
||||
request: $request
|
||||
);
|
||||
|
||||
// Initialize ACL (can only be initialized after Customization as it contains localizations).
|
||||
$acl = $this->acl->withRequest($request);
|
||||
|
||||
$request = $request
|
||||
->withAttribute(ServerRequest::ATTR_LOCALE, $customization->getLocale())
|
||||
->withAttribute(ServerRequest::ATTR_CUSTOMIZATION, $customization)
|
||||
->withAttribute(ServerRequest::ATTR_ACL, $acl);
|
||||
|
||||
// Set the Audit Log user.
|
||||
Entity\AuditLog::setCurrentUser($request->getAttribute(ServerRequest::ATTR_USER));
|
||||
|
||||
$response = $handler->handle($request);
|
||||
|
||||
Entity\AuditLog::setCurrentUser();
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middleware\Auth;
|
||||
|
||||
use App\Acl;
|
||||
use App\Auth;
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Exception\CsrfValidationException;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Session\Csrf;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class ApiAuth extends AbstractAuth
|
||||
{
|
||||
public const API_CSRF_NAMESPACE = 'api';
|
||||
|
||||
public function __construct(
|
||||
protected Entity\Repository\UserRepository $userRepo,
|
||||
protected Entity\Repository\ApiKeyRepository $apiKeyRepo,
|
||||
Entity\Repository\SettingsRepository $settingsRepo,
|
||||
Environment $environment,
|
||||
Acl $acl
|
||||
) {
|
||||
parent::__construct($settingsRepo, $environment, $acl);
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
// Initialize the Auth for this request.
|
||||
$user = $this->getApiUser($request);
|
||||
|
||||
$request = $request->withAttribute(ServerRequest::ATTR_USER, $user);
|
||||
|
||||
return parent::process($request, $handler);
|
||||
}
|
||||
|
||||
protected function getApiUser(ServerRequestInterface $request): ?Entity\User
|
||||
{
|
||||
$apiKey = $this->getApiKey($request);
|
||||
|
||||
if (!empty($apiKey)) {
|
||||
$apiUser = $this->apiKeyRepo->authenticate($apiKey);
|
||||
if (null !== $apiUser) {
|
||||
return $apiUser;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to session login if available.
|
||||
$csrfKey = $request->getHeaderLine('X-API-CSRF');
|
||||
if (empty($csrfKey) && !$this->environment->isTesting()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$auth = new Auth(
|
||||
userRepo: $this->userRepo,
|
||||
session: $request->getAttribute(ServerRequest::ATTR_SESSION),
|
||||
environment: $this->environment,
|
||||
);
|
||||
|
||||
if ($auth->isLoggedIn()) {
|
||||
$csrf = $request->getAttribute(ServerRequest::ATTR_SESSION_CSRF);
|
||||
|
||||
if ($csrf instanceof Csrf) {
|
||||
try {
|
||||
$csrf->verify($csrfKey, self::API_CSRF_NAMESPACE);
|
||||
return $auth->getLoggedInUser();
|
||||
} catch (CsrfValidationException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getApiKey(ServerRequestInterface $request): ?string
|
||||
{
|
||||
// Check authorization header
|
||||
$auth_headers = $request->getHeader('Authorization');
|
||||
$auth_header = $auth_headers[0] ?? '';
|
||||
|
||||
if (preg_match("/Bearer\s+(.*)$/i", $auth_header, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// Check API key header
|
||||
$api_key_headers = $request->getHeader('X-API-Key');
|
||||
if (!empty($api_key_headers[0])) {
|
||||
return $api_key_headers[0];
|
||||
}
|
||||
|
||||
// Check cookies
|
||||
$cookieParams = $request->getCookieParams();
|
||||
if (!empty($cookieParams['token'])) {
|
||||
return $cookieParams['token'];
|
||||
}
|
||||
|
||||
// Check URL parameters as last resort
|
||||
$queryParams = $request->getQueryParams();
|
||||
$queryApiKey = $queryParams['api_key'] ?? null;
|
||||
if (!empty($queryApiKey)) {
|
||||
return $queryApiKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middleware\Auth;
|
||||
|
||||
use App\Acl;
|
||||
use App\Auth;
|
||||
use App\Entity;
|
||||
use App\Environment;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class StandardAuth extends AbstractAuth
|
||||
{
|
||||
public function __construct(
|
||||
protected Entity\Repository\UserRepository $userRepo,
|
||||
Entity\Repository\SettingsRepository $settingsRepo,
|
||||
Environment $environment,
|
||||
Acl $acl
|
||||
) {
|
||||
parent::__construct($settingsRepo, $environment, $acl);
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
// Initialize the Auth for this request.
|
||||
$auth = new Auth(
|
||||
userRepo: $this->userRepo,
|
||||
session: $request->getAttribute(ServerRequest::ATTR_SESSION),
|
||||
environment: $this->environment,
|
||||
);
|
||||
$user = ($auth->isLoggedIn()) ? $auth->getLoggedInUser() : null;
|
||||
|
||||
$request = $request
|
||||
->withAttribute(ServerRequest::ATTR_AUTH, $auth)
|
||||
->withAttribute(ServerRequest::ATTR_USER, $user);
|
||||
|
||||
return parent::process($request, $handler);
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ use Symfony\Component\VarDumper\VarDumper;
|
|||
class Api
|
||||
{
|
||||
public function __construct(
|
||||
protected Entity\Repository\ApiKeyRepository $api_repo,
|
||||
protected Entity\Repository\ApiKeyRepository $apiKeyRepo,
|
||||
protected Entity\Repository\SettingsRepository $settingsRepo,
|
||||
protected Environment $environment
|
||||
) {
|
||||
|
@ -43,35 +43,26 @@ class Api
|
|||
}
|
||||
|
||||
// Attempt API key auth if a key exists.
|
||||
$api_key = $this->getApiKey($request);
|
||||
$api_user = (!empty($api_key)) ? $this->api_repo->authenticate($api_key) : null;
|
||||
|
||||
// Override the request's "user" variable if API authentication is supplied and valid.
|
||||
if ($api_user instanceof Entity\User) {
|
||||
$request = $request->withAttribute(ServerRequest::ATTR_USER, $api_user);
|
||||
$request->getAcl()->setRequest($request);
|
||||
|
||||
Entity\AuditLog::setCurrentUser($api_user);
|
||||
}
|
||||
$apiUser = $request->getAttribute(ServerRequest::ATTR_USER);
|
||||
|
||||
// Set default cache control for API pages.
|
||||
$settings = $this->settingsRepo->readSettings();
|
||||
|
||||
$prefer_browser_url = $settings->getPreferBrowserUrl();
|
||||
$preferBrowserUrl = $settings->getPreferBrowserUrl();
|
||||
|
||||
$response = $handler->handle($request);
|
||||
|
||||
// Check for a user-set CORS header override.
|
||||
$acao_header = trim($settings->getApiAccessControl());
|
||||
if (!empty($acao_header)) {
|
||||
if ('*' === $acao_header) {
|
||||
$acaoHeader = trim($settings->getApiAccessControl());
|
||||
if (!empty($acaoHeader)) {
|
||||
if ('*' === $acaoHeader) {
|
||||
$response = $response->withHeader('Access-Control-Allow-Origin', '*');
|
||||
} else {
|
||||
// Return the proper ACAO header matching the origin (if one exists).
|
||||
$origin = $request->getHeaderLine('Origin');
|
||||
|
||||
if (!empty($origin)) {
|
||||
$rawOrigins = array_map('trim', explode(',', $acao_header));
|
||||
$rawOrigins = array_map('trim', explode(',', $acaoHeader));
|
||||
|
||||
$baseUrl = $settings->getBaseUrl();
|
||||
if (null !== $baseUrl) {
|
||||
|
@ -97,7 +88,7 @@ class Api
|
|||
}
|
||||
}
|
||||
}
|
||||
} elseif ($api_user instanceof Entity\User || in_array($request->getMethod(), ['GET', 'OPTIONS'])) {
|
||||
} elseif ($apiUser instanceof Entity\User || in_array($request->getMethod(), ['GET', 'OPTIONS'])) {
|
||||
// Default behavior:
|
||||
// Only set global CORS for GET requests and API-authenticated requests;
|
||||
// Session-authenticated, non-GET requests should only be made in a same-host situation.
|
||||
|
@ -105,7 +96,7 @@ class Api
|
|||
}
|
||||
|
||||
if ($response instanceof Response && !$response->hasCacheLifetime()) {
|
||||
if ($prefer_browser_url || $request->getAttribute(ServerRequest::ATTR_USER) instanceof Entity\User) {
|
||||
if ($preferBrowserUrl || $request->getAttribute(ServerRequest::ATTR_USER) instanceof Entity\User) {
|
||||
$response = $response->withNoCache();
|
||||
} else {
|
||||
$response = $response->withCacheLifetime(15);
|
||||
|
@ -114,38 +105,4 @@ class Api
|
|||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequest $request
|
||||
*/
|
||||
protected function getApiKey(ServerRequest $request): ?string
|
||||
{
|
||||
// Check authorization header
|
||||
$auth_headers = $request->getHeader('Authorization');
|
||||
$auth_header = $auth_headers[0] ?? '';
|
||||
|
||||
if (preg_match("/Bearer\s+(.*)$/i", $auth_header, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// Check API key header
|
||||
$api_key_headers = $request->getHeader('X-API-Key');
|
||||
if (!empty($api_key_headers[0])) {
|
||||
return $api_key_headers[0];
|
||||
}
|
||||
|
||||
// Check cookies
|
||||
$cookieParams = $request->getCookieParams();
|
||||
if (!empty($cookieParams['token'])) {
|
||||
return $cookieParams['token'];
|
||||
}
|
||||
|
||||
// Check URL parameters as last resort
|
||||
$queryApiKey = $request->getQueryParam('api-key');
|
||||
if (!empty($queryApiKey)) {
|
||||
return $queryApiKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue