Move SFTP user form to Vue.
This commit is contained in:
parent
0957624968
commit
d696eeda7c
|
@ -1,43 +0,0 @@
|
|||
<?php
|
||||
return [
|
||||
'elements' => [
|
||||
|
||||
'username' => [
|
||||
'text',
|
||||
[
|
||||
'label' => __('Username'),
|
||||
'class' => 'half-width',
|
||||
'maxLength' => 32,
|
||||
],
|
||||
],
|
||||
|
||||
'password' => [
|
||||
'password',
|
||||
[
|
||||
'label' => __('New Password'),
|
||||
'description' => __('Leave blank to use the current password.'),
|
||||
'autocomplete' => 'off',
|
||||
'required' => false,
|
||||
],
|
||||
],
|
||||
|
||||
'publicKeys' => [
|
||||
'textarea',
|
||||
[
|
||||
'label' => __('SSH Public Keys'),
|
||||
'class' => 'text-preformatted',
|
||||
'description' => __('Optionally supply SSH public keys this user can use to connect instead of a password. Enter one key per line.'),
|
||||
'required' => false,
|
||||
],
|
||||
],
|
||||
|
||||
'submit' => [
|
||||
'submit',
|
||||
[
|
||||
'type' => 'submit',
|
||||
'label' => __('Save Changes'),
|
||||
'class' => 'btn btn-lg btn-primary',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -420,6 +420,12 @@ return static function (RouteCollectorProxy $app) {
|
|||
Acl::STATION_MEDIA,
|
||||
],
|
||||
['remote', 'remotes', Controller\Api\Stations\RemotesController::class, Acl::STATION_REMOTES],
|
||||
[
|
||||
'sftp-user',
|
||||
'sftp-users',
|
||||
Controller\Api\Stations\SftpUsersController::class,
|
||||
Acl::STATION_MEDIA,
|
||||
],
|
||||
[
|
||||
'streamer',
|
||||
'streamers',
|
||||
|
|
|
@ -113,26 +113,9 @@ return static function (RouteCollectorProxy $app) {
|
|||
}
|
||||
)->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));
|
||||
|
||||
$group->group(
|
||||
'/sftp_users',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('', Controller\Stations\SftpUsersController::class . ':indexAction')
|
||||
->setName('stations:sftp_users:index');
|
||||
|
||||
$group->map(
|
||||
['GET', 'POST'],
|
||||
'/edit/{id}',
|
||||
Controller\Stations\SftpUsersController::class . ':editAction'
|
||||
)
|
||||
->setName('stations:sftp_users:edit');
|
||||
|
||||
$group->map(['GET', 'POST'], '/add', Controller\Stations\SftpUsersController::class . ':editAction')
|
||||
->setName('stations:sftp_users:add');
|
||||
|
||||
$group->get('/delete/{id}/{csrf}', Controller\Stations\SftpUsersController::class . ':deleteAction')
|
||||
->setName('stations:sftp_users:delete');
|
||||
}
|
||||
)->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
|
||||
$group->get('/sftp_users', Controller\Stations\SftpUsersAction::class)
|
||||
->setName('stations:sftp_users:index')
|
||||
->add(new Middleware\Permissions(Acl::STATION_MEDIA, true));
|
||||
|
||||
$group->get('/streamers', Controller\Stations\StreamersAction::class)
|
||||
->setName('stations:streamers:index')
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<b-card no-body>
|
||||
<b-card-header header-bg-variant="primary-dark">
|
||||
<h2 class="card-title" key="lang_title" v-translate>SFTP Users</h2>
|
||||
</b-card-header>
|
||||
|
||||
<b-card-body body-class="card-padding-sm">
|
||||
<b-button variant="outline-primary" @click.prevent="doCreate">
|
||||
<icon icon="add"></icon>
|
||||
<translate key="lang_add_btn">Add SFTP User</translate>
|
||||
</b-button>
|
||||
</b-card-body>
|
||||
|
||||
<data-table ref="datatable" id="station_remotes" :show-toolbar="false" :fields="fields"
|
||||
:api-url="listUrl">
|
||||
<template #cell(actions)="row">
|
||||
<b-button-group size="sm">
|
||||
<b-button size="sm" variant="primary" @click.prevent="doEdit(row.item.links.self)">
|
||||
<translate key="lang_btn_edit">Edit</translate>
|
||||
</b-button>
|
||||
<b-button size="sm" variant="danger" @click.prevent="doDelete(row.item.links.self)">
|
||||
<translate key="lang_btn_delete">Delete</translate>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</template>
|
||||
</data-table>
|
||||
</b-card>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title">
|
||||
<translate key="lang_connection_info">Connection Information</translate>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl>
|
||||
<dt class="mb-1">
|
||||
<translate key="lang_connection_server">Server:</translate>
|
||||
</dt>
|
||||
<dd><code>{{ connectionInfo.url }}</code></dd>
|
||||
|
||||
<dd v-if="connectionInfo.ip">
|
||||
<translate key="lang_connection_ip">You may need to connect directly to your IP address:</translate>
|
||||
<code>{{ connectionInfo.ip }}</code>
|
||||
</dd>
|
||||
|
||||
<dt class="mb-1">
|
||||
<translate key="lang_connection_port">Port:</translate>
|
||||
</dt>
|
||||
<dd><code>{{ connectionInfo.port }}</code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<sftp-users-edit-modal ref="editModal" :create-url="listUrl" @relist="relist"></sftp-users-edit-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataTable from "~/components/Common/DataTable";
|
||||
import confirmDelete from "~/functions/confirmDelete";
|
||||
import SftpUsersEditModal from "./SftpUsers/EditModal";
|
||||
import Icon from "~/components/Common/Icon";
|
||||
|
||||
export default {
|
||||
name: 'SftpUsers',
|
||||
components: {Icon, SftpUsersEditModal, DataTable},
|
||||
props: {
|
||||
listUrl: String,
|
||||
connectionInfo: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fields: [
|
||||
{key: 'username', isRowHeader: true, label: this.$gettext('Username'), sortable: false},
|
||||
{key: 'actions', label: this.$gettext('Actions'), sortable: false, class: 'shrink'}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
relist() {
|
||||
this.$refs.datatable.refresh();
|
||||
},
|
||||
doCreate() {
|
||||
this.$refs.editModal.create();
|
||||
},
|
||||
doEdit(url) {
|
||||
this.$refs.editModal.edit(url);
|
||||
},
|
||||
doDelete(url) {
|
||||
confirmDelete({
|
||||
title: this.$gettext('Delete SFTP User?'),
|
||||
confirmButtonText: this.$gettext('Delete'),
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
this.$wrapWithLoading(
|
||||
this.axios.delete(url)
|
||||
).then((resp) => {
|
||||
this.$notifySuccess(resp.data.message);
|
||||
this.relist();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<modal-form ref="modal" :loading="loading" :title="langTitle" :error="error" :disable-save-button="$v.form.$invalid"
|
||||
@submit="doSubmit" @hidden="clearContents">
|
||||
|
||||
<sftp-users-form :form="$v.form" :is-edit-mode="isEditMode"></sftp-users-form>
|
||||
|
||||
</modal-form>
|
||||
</template>
|
||||
<script>
|
||||
import {required} from 'vuelidate/dist/validators.min.js';
|
||||
import BaseEditModal from '~/components/Common/BaseEditModal';
|
||||
import SftpUsersForm from "./Form";
|
||||
|
||||
export default {
|
||||
name: 'SftpUsersEditModal',
|
||||
mixins: [BaseEditModal],
|
||||
components: {SftpUsersForm},
|
||||
validations() {
|
||||
return {
|
||||
form: {
|
||||
username: {required},
|
||||
password: this.isEditMode ? {} : {required},
|
||||
publicKeys: {}
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
langTitle() {
|
||||
return this.isEditMode
|
||||
? this.$gettext('Edit SFTP User')
|
||||
: this.$gettext('Add SFTP User');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetForm() {
|
||||
this.form = {
|
||||
username: '',
|
||||
password: null,
|
||||
publicKeys: null
|
||||
};
|
||||
},
|
||||
populateForm(data) {
|
||||
this.form = {
|
||||
username: data.username,
|
||||
publicKeys: data.publicKeys
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<b-form-group>
|
||||
<b-row>
|
||||
<b-wrapped-form-group class="col-md-6" id="edit_form_username" :field="form.username">
|
||||
<template #label>
|
||||
<translate key="lang_edit_form_username">Username</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-6" id="edit_form_password" :field="form.password"
|
||||
input-type="password">
|
||||
<template #label v-if="isEditMode">
|
||||
<translate key="lang_edit_form_new_password">New Password</translate>
|
||||
</template>
|
||||
<template #label v-else>
|
||||
<translate key="lang_edit_form_password">Password</translate>
|
||||
</template>
|
||||
|
||||
<template #description v-if="isEditMode">
|
||||
<translate key="lang_edit_form_password_desc">Leave blank to use the current password.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group class="col-md-12" id="edit_form_publicKeys" :field="form.publicKeys"
|
||||
input-type="textarea">
|
||||
<template #label>
|
||||
<translate key="lang_edit_form_publickeys">SSH Public Keys</translate>
|
||||
</template>
|
||||
<template #description>
|
||||
<translate key="lang_edit_form_publickeys_desc">Optionally supply SSH public keys this user can use to connect instead of a password. Enter one key per line.</translate>
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
</b-row>
|
||||
</b-form-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
|
||||
|
||||
export default {
|
||||
name: 'SftpUsersForm',
|
||||
components: {BWrappedFormGroup},
|
||||
props: {
|
||||
form: Object,
|
||||
isEditMode: Boolean
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
import initBase from '~/base.js';
|
||||
|
||||
import '~/vendor/bootstrapVue.js';
|
||||
|
||||
import SftpUsers from "~/components/Stations/SftpUsers";
|
||||
|
||||
export default initBase(SftpUsers);
|
|
@ -1,120 +1,121 @@
|
|||
const webpack = require('webpack');
|
||||
const WebpackAssetsManifest = require('webpack-assets-manifest');
|
||||
const { VueLoaderPlugin } = require('vue-loader');
|
||||
const {VueLoaderPlugin} = require('vue-loader');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: (process.env.NODE_ENV === 'production') ? 'production' : 'development',
|
||||
entry: {
|
||||
Dashboard: '~/pages/Dashboard.js',
|
||||
AdminAuditLog: '~/pages/Admin/AuditLog.js',
|
||||
AdminBranding: '~/pages/Admin/Branding.js',
|
||||
AdminCustomFields: '~/pages/Admin/CustomFields.js',
|
||||
AdminPermissions: '~/pages/Admin/Permissions.js',
|
||||
AdminStorageLocations: '~/pages/Admin/StorageLocations.js',
|
||||
PublicFullPlayer: '~/pages/Public/FullPlayer.js',
|
||||
PublicHistory: '~/pages/Public/History.js',
|
||||
PublicOnDemand: '~/pages/Public/OnDemand.js',
|
||||
PublicPlayer: '~/pages/Public/Player.js',
|
||||
PublicRequests: '~/pages/Public/Requests.js',
|
||||
PublicSchedule: '~/pages/Public/Schedule.js',
|
||||
PublicWebDJ: '~/pages/Public/WebDJ.js',
|
||||
StationsMedia: '~/pages/Stations/Media.js',
|
||||
StationsMounts: '~/pages/Stations/Mounts.js',
|
||||
StationsPlaylists: '~/pages/Stations/Playlists.js',
|
||||
StationsPodcasts: '~/pages/Stations/Podcasts.js',
|
||||
StationsProfile: '~/pages/Stations/Profile.js',
|
||||
StationsQueue: '~/pages/Stations/Queue.js',
|
||||
StationsRemotes: '~/pages/Stations/Remotes.js',
|
||||
StationsStreamers: '~/pages/Stations/Streamers.js',
|
||||
StationsReportsListeners: '~/pages/Stations/Reports/Listeners.js',
|
||||
StationsReportsRequests: '~/pages/Stations/Reports/Requests.js',
|
||||
StationsReportsOverview: '~/pages/Stations/Reports/Overview.js',
|
||||
StationsReportsPerformance: '~/pages/Stations/Reports/Performance.js',
|
||||
StationsReportsTimeline: '~/pages/Stations/Reports/Timeline.js',
|
||||
StationsWebhooks: '~/pages/Stations/Webhooks.js'
|
||||
},
|
||||
resolve: {
|
||||
enforceExtension: false,
|
||||
alias: {
|
||||
'~': path.resolve(__dirname, './vue')
|
||||
mode: (process.env.NODE_ENV === 'production') ? 'production' : 'development',
|
||||
entry: {
|
||||
Dashboard: '~/pages/Dashboard.js',
|
||||
AdminAuditLog: '~/pages/Admin/AuditLog.js',
|
||||
AdminBranding: '~/pages/Admin/Branding.js',
|
||||
AdminCustomFields: '~/pages/Admin/CustomFields.js',
|
||||
AdminPermissions: '~/pages/Admin/Permissions.js',
|
||||
AdminStorageLocations: '~/pages/Admin/StorageLocations.js',
|
||||
PublicFullPlayer: '~/pages/Public/FullPlayer.js',
|
||||
PublicHistory: '~/pages/Public/History.js',
|
||||
PublicOnDemand: '~/pages/Public/OnDemand.js',
|
||||
PublicPlayer: '~/pages/Public/Player.js',
|
||||
PublicRequests: '~/pages/Public/Requests.js',
|
||||
PublicSchedule: '~/pages/Public/Schedule.js',
|
||||
PublicWebDJ: '~/pages/Public/WebDJ.js',
|
||||
StationsMedia: '~/pages/Stations/Media.js',
|
||||
StationsMounts: '~/pages/Stations/Mounts.js',
|
||||
StationsPlaylists: '~/pages/Stations/Playlists.js',
|
||||
StationsPodcasts: '~/pages/Stations/Podcasts.js',
|
||||
StationsProfile: '~/pages/Stations/Profile.js',
|
||||
StationsQueue: '~/pages/Stations/Queue.js',
|
||||
StationsRemotes: '~/pages/Stations/Remotes.js',
|
||||
StationsStreamers: '~/pages/Stations/Streamers.js',
|
||||
StationsReportsListeners: '~/pages/Stations/Reports/Listeners.js',
|
||||
StationsReportsRequests: '~/pages/Stations/Reports/Requests.js',
|
||||
StationsReportsOverview: '~/pages/Stations/Reports/Overview.js',
|
||||
StationsReportsPerformance: '~/pages/Stations/Reports/Performance.js',
|
||||
StationsReportsTimeline: '~/pages/Stations/Reports/Timeline.js',
|
||||
StationsSftpUsers: '~/pages/Stations/SftpUsers.js',
|
||||
StationsWebhooks: '~/pages/Stations/Webhooks.js'
|
||||
},
|
||||
extensions: ['.js', '.vue', '.json']
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../web/static/webpack_dist'),
|
||||
publicPath: '/static/webpack_dist/',
|
||||
filename: '[name].[contenthash].js',
|
||||
sourceMapFilename: '[name].[contenthash].map',
|
||||
library: '[name]',
|
||||
assetModuleFilename: 'images/[contenthash][ext]'
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
translations: {
|
||||
test: /translations\.json$/,
|
||||
chunks: 'all',
|
||||
enforce: true,
|
||||
name: 'translations'
|
||||
resolve: {
|
||||
enforceExtension: false,
|
||||
alias: {
|
||||
'~': path.resolve(__dirname, './vue')
|
||||
},
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
chunks: 'all',
|
||||
enforce: true,
|
||||
name (module) {
|
||||
// get the name. E.g. node_modules/packageName/not/this/part.js
|
||||
// or node_modules/packageName
|
||||
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
|
||||
extensions: ['.js', '.vue', '.json']
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../web/static/webpack_dist'),
|
||||
publicPath: '/static/webpack_dist/',
|
||||
filename: '[name].[contenthash].js',
|
||||
sourceMapFilename: '[name].[contenthash].map',
|
||||
library: '[name]',
|
||||
assetModuleFilename: 'images/[contenthash][ext]'
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
translations: {
|
||||
test: /translations\.json$/,
|
||||
chunks: 'all',
|
||||
enforce: true,
|
||||
name: 'translations'
|
||||
},
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
chunks: 'all',
|
||||
enforce: true,
|
||||
name(module) {
|
||||
// get the name. E.g. node_modules/packageName/not/this/part.js
|
||||
// or node_modules/packageName
|
||||
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
|
||||
|
||||
// npm package names are URL-safe, but some servers don't like @ symbols
|
||||
return `vendor-${packageName.replace('@', '')}`;
|
||||
}
|
||||
// npm package names are URL-safe, but some servers don't like @ symbols
|
||||
return `vendor-${packageName.replace('@', '')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/i,
|
||||
use: [
|
||||
'vue-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/i,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
|
||||
type: 'asset/resource'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new WebpackAssetsManifest({
|
||||
output: path.resolve(__dirname, '../web/static/webpack.json'),
|
||||
writeToDisk: true,
|
||||
merge: true,
|
||||
publicPath: true,
|
||||
entrypoints: true
|
||||
}),
|
||||
new VueLoaderPlugin()
|
||||
],
|
||||
target: 'web',
|
||||
performance: {
|
||||
hints: false
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/i,
|
||||
use: [
|
||||
'vue-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/i,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
|
||||
type: 'asset/resource'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new WebpackAssetsManifest({
|
||||
output: path.resolve(__dirname, '../web/static/webpack.json'),
|
||||
writeToDisk: true,
|
||||
merge: true,
|
||||
publicPath: true,
|
||||
entrypoints: true
|
||||
}),
|
||||
new VueLoaderPlugin()
|
||||
],
|
||||
target: 'web',
|
||||
performance: {
|
||||
hints: false
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations;
|
||||
|
||||
use App\Entity;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
/**
|
||||
* @extends AbstractStationApiCrudController<Entity\SftpUser>
|
||||
*/
|
||||
class SftpUsersController extends AbstractStationApiCrudController
|
||||
{
|
||||
protected string $entityClass = Entity\SftpUser::class;
|
||||
protected string $resourceRouteName = 'api:stations:sftp-user';
|
||||
|
||||
/**
|
||||
* @OA\Get(path="/station/{station_id}/sftp-users",
|
||||
* tags={"Stations: SFTP Users"},
|
||||
* description="List all current SFTP users.",
|
||||
* @OA\Parameter(ref="#/components/parameters/station_id_required"),
|
||||
* @OA\Response(response=200, description="Success",
|
||||
* @OA\JsonContent(type="array", @OA\Items(ref="#/components/schemas/SftpUser"))
|
||||
* ),
|
||||
* @OA\Response(response=403, description="Access denied"),
|
||||
* security={{"api_key": {}}},
|
||||
* )
|
||||
*
|
||||
* @OA\Post(path="/station/{station_id}/sftp-users",
|
||||
* tags={"Stations: SFTP Users"},
|
||||
* description="Create a new SFTP user.",
|
||||
* @OA\Parameter(ref="#/components/parameters/station_id_required"),
|
||||
* @OA\RequestBody(
|
||||
* @OA\JsonContent(ref="#/components/schemas/SftpUser")
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Success",
|
||||
* @OA\JsonContent(ref="#/components/schemas/SftpUser")
|
||||
* ),
|
||||
* @OA\Response(response=403, description="Access denied"),
|
||||
* security={{"api_key": {}}},
|
||||
* )
|
||||
*
|
||||
* @OA\Get(path="/station/{station_id}/sftp-user/{id}",
|
||||
* tags={"Stations: SFTP Users"},
|
||||
* description="Retrieve details for a single SFTP user.",
|
||||
* @OA\Parameter(ref="#/components/parameters/station_id_required"),
|
||||
* @OA\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* description="SFTP User ID",
|
||||
* required=true,
|
||||
* @OA\Schema(type="integer", format="int64")
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Success",
|
||||
* @OA\JsonContent(ref="#/components/schemas/SftpUser")
|
||||
* ),
|
||||
* @OA\Response(response=403, description="Access denied"),
|
||||
* security={{"api_key": {}}},
|
||||
* )
|
||||
*
|
||||
* @OA\Put(path="/station/{station_id}/sftp-user/{id}",
|
||||
* tags={"Stations: SFTP Users"},
|
||||
* description="Update details of a single SFTP user.",
|
||||
* @OA\RequestBody(
|
||||
* @OA\JsonContent(ref="#/components/schemas/SftpUser")
|
||||
* ),
|
||||
* @OA\Parameter(ref="#/components/parameters/station_id_required"),
|
||||
* @OA\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* description="Remote Relay ID",
|
||||
* required=true,
|
||||
* @OA\Schema(type="integer", format="int64")
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Success",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Api_Status")
|
||||
* ),
|
||||
* @OA\Response(response=403, description="Access denied"),
|
||||
* security={{"api_key": {}}},
|
||||
* )
|
||||
*
|
||||
* @OA\Delete(path="/station/{station_id}/sftp-user/{id}",
|
||||
* tags={"Stations: SFTP Users"},
|
||||
* description="Delete a single remote relay.",
|
||||
* @OA\Parameter(ref="#/components/parameters/station_id_required"),
|
||||
* @OA\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* description="Remote Relay ID",
|
||||
* required=true,
|
||||
* @OA\Schema(type="integer", format="int64")
|
||||
* ),
|
||||
* @OA\Response(response=200, description="Success",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Api_Status")
|
||||
* ),
|
||||
* @OA\Response(response=403, description="Access denied"),
|
||||
* security={{"api_key": {}}},
|
||||
* )
|
||||
*/
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Stations;
|
||||
|
||||
use App\Environment;
|
||||
use App\Exception\StationUnsupportedException;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Service\AzuraCastCentral;
|
||||
use App\Service\SftpGo;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class SftpUsersAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
Environment $environment,
|
||||
AzuraCastCentral $acCentral
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
if (!SftpGo::isSupportedForStation($station)) {
|
||||
throw new StationUnsupportedException(__('This feature is not currently supported on this station.'));
|
||||
}
|
||||
|
||||
$baseUrl = $request->getRouter()->getBaseUrl()
|
||||
->withScheme('sftp')
|
||||
->withPort(null);
|
||||
|
||||
$port = $environment->getSftpPort();
|
||||
|
||||
$router = $request->getRouter();
|
||||
|
||||
return $request->getView()->renderVuePage(
|
||||
response: $response,
|
||||
component: 'Vue_StationsSftpUsers',
|
||||
id: 'station-sftp-users',
|
||||
title: __('SFTP Users'),
|
||||
props: [
|
||||
'listUrl' => (string)$router->fromHere('api:stations:sftp-users'),
|
||||
'connectionInfo' => [
|
||||
'url' => (string)$baseUrl,
|
||||
'ip' => $acCentral->getIp(),
|
||||
'port' => $port,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Stations;
|
||||
|
||||
use App\Environment;
|
||||
use App\Exception\StationUnsupportedException;
|
||||
use App\Form\SftpUserForm;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Service\AzuraCastCentral;
|
||||
use App\Service\SftpGo;
|
||||
use App\Session\Flash;
|
||||
use DI\FactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class SftpUsersController extends AbstractStationCrudController
|
||||
{
|
||||
public function __construct(
|
||||
protected AzuraCastCentral $ac_central,
|
||||
protected Environment $environment,
|
||||
FactoryInterface $factory
|
||||
) {
|
||||
parent::__construct($factory->make(SftpUserForm::class));
|
||||
|
||||
$this->csrf_namespace = 'stations_sftp_users';
|
||||
}
|
||||
|
||||
public function indexAction(ServerRequest $request, Response $response): ResponseInterface
|
||||
{
|
||||
$station = $request->getStation();
|
||||
|
||||
if (!SftpGo::isSupportedForStation($station)) {
|
||||
throw new StationUnsupportedException(__('This feature is not currently supported on this station.'));
|
||||
}
|
||||
|
||||
$baseUrl = $request->getRouter()->getBaseUrl()
|
||||
->withScheme('sftp')
|
||||
->withPort(null);
|
||||
|
||||
$port = $this->environment->getSftpPort();
|
||||
|
||||
$sftpInfo = [
|
||||
'url' => (string)$baseUrl,
|
||||
'ip' => $this->ac_central->getIp(),
|
||||
'port' => $port,
|
||||
];
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/sftp_users/index', [
|
||||
'users' => $station->getSftpUsers(),
|
||||
'sftp_info' => $sftpInfo,
|
||||
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
|
||||
]);
|
||||
}
|
||||
|
||||
public function editAction(ServerRequest $request, Response $response, int $id = null): ResponseInterface
|
||||
{
|
||||
if (false !== $this->doEdit($request, $id)) {
|
||||
$request->getFlash()->addMessage('<b>' . __('Changes saved.') . '</b>', Flash::SUCCESS);
|
||||
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:sftp_users:index'));
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse(
|
||||
$response,
|
||||
'system/form_page',
|
||||
[
|
||||
'form' => $this->form,
|
||||
'render_mode' => 'edit',
|
||||
'title' => $id ? __('Edit SFTP User') : __('Add SFTP User'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
int $id,
|
||||
string $csrf
|
||||
): ResponseInterface {
|
||||
$this->doDelete($request, $id, $csrf);
|
||||
|
||||
$request->getFlash()->addMessage('<b>' . __('SFTP User deleted.') . '</b>', Flash::SUCCESS);
|
||||
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:sftp_users:index'));
|
||||
}
|
||||
}
|
|
@ -8,10 +8,12 @@ use App\Entity\Attributes\Auditable;
|
|||
use App\Entity\Interfaces\IdentifiableEntityInterface;
|
||||
use App\Validator\Constraints\UniqueEntity;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
use const PASSWORD_ARGON2ID;
|
||||
|
||||
/** @OA\Schema(type="object") */
|
||||
#[ORM\Entity, ORM\Table(name: 'sftp_user')]
|
||||
#[ORM\UniqueConstraint(name: 'username_idx', columns: ['username'])]
|
||||
#[UniqueEntity(fields: ['username'])]
|
||||
|
@ -24,16 +26,19 @@ class SftpUser implements IdentifiableEntityInterface
|
|||
#[ORM\JoinColumn(name: 'station_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
protected Station $station;
|
||||
|
||||
/** @OA\Property() */
|
||||
#[ORM\Column(length: 32)]
|
||||
#[Assert\Length(min: 1, max: 32)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Regex(pattern: '/^[a-zA-Z0-9-_.~]+$/')]
|
||||
protected string $username;
|
||||
|
||||
/** @OA\Property() */
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotBlank]
|
||||
protected string $password;
|
||||
|
||||
/** @OA\Property() */
|
||||
#[ORM\Column(name: 'public_keys', type: 'text', nullable: true)]
|
||||
protected ?string $publicKeys = null;
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Config;
|
||||
use App\Entity;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class SftpUserForm extends EntityForm
|
||||
{
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
Serializer $serializer,
|
||||
ValidatorInterface $validator,
|
||||
Config $config
|
||||
) {
|
||||
$form_config = $config->get('forms/sftp_user');
|
||||
|
||||
parent::__construct($em, $serializer, $validator, $form_config);
|
||||
|
||||
$this->entityClass = Entity\SftpUser::class;
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ use OpenApi\Annotations as OA;
|
|||
* @OA\Tag(name="Stations: Podcasts")
|
||||
* @OA\Tag(name="Stations: Queue")
|
||||
* @OA\Tag(name="Stations: Remote Relays")
|
||||
* @OA\Tag(name="Stations: SFTP Users")
|
||||
* @OA\Tag(name="Stations: Streamers/DJs")
|
||||
* @OA\Tag(name="Stations: Web Hooks")
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue