Move SFTP user form to Vue.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-10-08 06:30:03 -05:00
parent 0957624968
commit d696eeda7c
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
14 changed files with 495 additions and 285 deletions

View File

@ -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',
],
],
],
];

View File

@ -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',

View File

@ -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')

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,7 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import SftpUsers from "~/components/Stations/SftpUsers";
export default initBase(SftpUsers);

View File

@ -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
}
};

View File

@ -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": {}}},
* )
*/
}

View File

@ -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,
],
],
);
}
}

View File

@ -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'));
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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")
*