diff --git a/api-sandbox/app/main.js b/api-sandbox/app/main.js index 20e223b..b710857 100644 --- a/api-sandbox/app/main.js +++ b/api-sandbox/app/main.js @@ -48,11 +48,10 @@ async function loadApiSpec() { if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } - apiSpec = await response.json() - console.log('API Spec loaded:', apiSpec) - let moduleSpecs = {}; - apiSpec.modules.forEach(m => moduleSpecs[m.name] = m); - return moduleSpecs + const data = await response.json() + apiSpec = data.modules + console.log('API Spec loaded:', data) + return apiSpec } catch (error) { console.error('Ошибка загрузки API спецификации:', error) $('#result').text('Ошибка загрузки API спецификации: ' + error.message) @@ -64,8 +63,8 @@ function populateServices() { $serviceSelect.empty() $serviceSelect.append('') - Object.keys(apiSpec).forEach(serviceName => { - $serviceSelect.append(``) + apiSpec.forEach(service => { + $serviceSelect.append(``) }) } @@ -74,10 +73,13 @@ function populateMethods(serviceName) { $methodSelect.empty() $methodSelect.append('') - if (serviceName && apiSpec[serviceName] && apiSpec[serviceName].methods) { - apiSpec[serviceName].methods.forEach(method => { - $methodSelect.append(``) - }) + if (serviceName) { + const service = apiSpec.find(s => s.name === serviceName) + if (service && service.methods) { + service.methods.forEach(method => { + $methodSelect.append(``) + }) + } } } @@ -87,7 +89,7 @@ function createParamInputs(methodName, serviceName) { if (!serviceName || !methodName) return - const service = apiSpec[serviceName] + const service = apiSpec.find(s => s.name === serviceName) if (!service || !service.methods) return const method = service.methods.find(m => m.name === methodName) @@ -160,7 +162,7 @@ async function sendRequest() { const paramType = $input.attr('id').replace('param-', '') // Find parameter type from method definition - const service = apiSpec[serviceName] + const service = apiSpec.find(s => s.name === serviceName) const method = service.methods.find(m => m.name === methodName) const paramDef = method.params.find(p => p.name === paramName) const isObjectOrArray = paramDef && (paramDef.type === 'object' || paramDef.type === 'array') @@ -240,7 +242,7 @@ async function sendRequest() { } $(document).ready(async () => { - await loadApiSpec() + apiSpec = await loadApiSpec() if (apiSpec) { populateServices() diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/ApiToken.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/ApiToken.java index 4e35cdb..d549c71 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/ApiToken.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/ApiToken.java @@ -27,6 +27,10 @@ public interface ApiToken extends PersistenceEntity { @JsonIgnore default boolean isExpired() { - return getExpirationDate().toInstant().isBefore(Instant.now()); + var expirationDate = getExpirationDate(); + if (expirationDate == null) { + return false; + } + return expirationDate.toInstant().isBefore(Instant.now()); } } diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceEntity.java b/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceEntity.java index 78b4ef8..86b6e18 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceEntity.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceEntity.java @@ -4,10 +4,11 @@ import ru.kirillius.XCP.Commons.GenerateApiSpec; import java.util.UUID; +@GenerateApiSpec public interface PersistenceEntity { @GenerateApiSpec long getId(); - @GenerateApiSpec + @GenerateApiSpec(type = String.class) UUID getUuid(); } diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java index 7b61fb5..045ab33 100644 --- a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java @@ -111,6 +111,7 @@ public class Auth extends JsonRpcService { token.setExpirationDate(permanent ? null : Date.from(Instant.now().plus(30, ChronoUnit.DAYS))); token.setName(name); + token.setUser(call.getCurrentUser()); tokenRepository.save(token); diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java index 5031407..d17b598 100644 --- a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java @@ -18,6 +18,7 @@ public class Profile extends JsonRpcService { @JsonRpcMethod.Parameter(name = "login", description = "User login. Have to be unique", type = String.class), @JsonRpcMethod.Parameter(name = "name", description = "User display name", type = String.class), @JsonRpcMethod.Parameter(name = "password", description = "Change user password if defined", type = String.class, optional = true), + @JsonRpcMethod.Parameter(name = "currentPassword", description = "Current user password", type = String.class, optional = true), @JsonRpcMethod.Parameter(name = "values", description = "User custom values", type = ObjectNode.class) }, returnType = boolean.class) @@ -28,6 +29,8 @@ public class Profile extends JsonRpcService { var login = requireParam(call, "login", JsonNode::asString); var name = requireParam(call, "name", JsonNode::asString); var passwordOptional = getParam(call, "password", JsonNode::asString); + var currentPassword = passwordOptional.isPresent() ? requireParam(call, "currentPassword", JsonNode::asString) : null; + var values = requireParam(call, "values", n -> (ObjectNode) n); if (!user.getLogin().equals(login) && userRepository.getByLogin(login) != null) { @@ -50,6 +53,9 @@ public class Profile extends JsonRpcService { if (password.isBlank()) { throw new RuntimeException("Password is blank"); } + if (!user.verifyPassword(currentPassword)) { + throw new RuntimeException("Current user password is invalid"); + } user.setPassword(password); } diff --git a/web-ui/vue-app/scripts/generate-rpc-client.ts b/web-ui/vue-app/scripts/generate-rpc-client.ts index cd5fe36..cff87eb 100644 --- a/web-ui/vue-app/scripts/generate-rpc-client.ts +++ b/web-ui/vue-app/scripts/generate-rpc-client.ts @@ -27,9 +27,19 @@ interface ApiModule { methods: ApiMethod[] } +interface ApiType { + name: string + type: 'class' | 'enum' + fields?: Array<{ + name: string + type: string + }> + values?: string[] +} + interface ApiSpec { modules: ApiModule[] - types: any[] + types: ApiType[] } function generateMethod(method: ApiMethod): string { @@ -59,6 +69,27 @@ ${sortedParams.map((param) => ` * @param ${param.name} ${param.description}` }` } +function generateType(apiType: ApiType): string { + if (apiType.type === 'enum') { + const values = apiType.values || [] + return `export enum ${apiType.name} { +${values.map((value) => ` ${value} = "${value}"`).join(',\n')} +}` + } else if (apiType.type === 'class') { + const fields = apiType.fields || [] + const fieldDefinitions = fields.map((field) => ` ${field.name}: ${field.type};`).join('\n') + return `export interface ${apiType.name} { +${fieldDefinitions} +}` + } + return '' +} + +function generateTypes(spec: ApiSpec): string { + const types = spec.types.map(generateType).join('\n\n') + return types ? `\n// Generated Types\n${types}` : '' +} + function generateModule(module: ApiModule): string { const className = `${module.name}Module` const methods = module.methods.map(generateMethod).join('\n') @@ -88,9 +119,11 @@ function generateClient(spec: ApiSpec): string { .join('\n') const moduleClasses = spec.modules.map(generateModule).join('\n\n') + const types = generateTypes(spec) return `import {RpcClientBase} from "@/api/RpcClientBase.ts"; import {RpcModuleBase} from "@/api/RpcModuleBase.ts"; +${types} export class RpcClient extends RpcClientBase {${modules} @@ -127,6 +160,11 @@ function main() { fs.writeFileSync(outputPath, clientCode) console.log(`✅ RPC client generated successfully: ${outputPath}`) console.log(`📋 Generated ${spec.modules.length} API modules`) + if (spec.types.length > 0) { + console.log( + `📝 Generated ${spec.types.length} types (${spec.types.filter((t) => t.type === 'class').length} interfaces, ${spec.types.filter((t) => t.type === 'enum').length} enums)` + ) + } } catch (error) { console.error('❌ Error generating RPC client:', error) process.exit(1) diff --git a/web-ui/vue-app/src/App.vue b/web-ui/vue-app/src/App.vue index b6ac7f9..ab9e682 100644 --- a/web-ui/vue-app/src/App.vue +++ b/web-ui/vue-app/src/App.vue @@ -1,17 +1,38 @@ @@ -19,15 +40,24 @@ import { ref, computed, onMounted, onUnmounted } from 'vue' import { RouterView } from 'vue-router' import NavigationMenu from './components/NavigationMenu.vue' -import FooterBar from './components/FooterBar.vue' +import HeaderBar from './components/HeaderBar.vue' +import LoginForm from './components/LoginForm.vue' +import Notifications from './components/Notifications.vue' +import { useAppStore } from '@/stores/app' +import { useAuthStore } from '@/stores/auth' +import { useNotificationStore } from '@/stores/notification' + +const appStore = useAppStore() +const authStore = useAuthStore() const navigationMenu = ref() const windowWidth = ref(window.innerWidth) const isCollapsed = ref(false) +const showFooter = ref(false) const isMobile = computed(() => windowWidth.value < 768) -const toggleMenu = () => { +const toggleMobileMenu = () => { if (navigationMenu.value) { navigationMenu.value.toggle() } @@ -37,8 +67,11 @@ const updateWidth = () => { windowWidth.value = window.innerWidth } -onMounted(() => { +onMounted(async () => { window.addEventListener('resize', updateWidth) + + appStore.initializeApi('http://localhost:8080/api') + await authStore.initializeAuth() }) onUnmounted(() => { @@ -48,17 +81,43 @@ onUnmounted(() => { diff --git a/web-ui/vue-app/src/api/RpcClientBase.ts b/web-ui/vue-app/src/api/RpcClientBase.ts index 32b8de4..29a352a 100644 --- a/web-ui/vue-app/src/api/RpcClientBase.ts +++ b/web-ui/vue-app/src/api/RpcClientBase.ts @@ -9,6 +9,7 @@ export class RpcClientBase { protected async call(method: string, params?: any): Promise { const response = await fetch(this.baseUrl, { method: 'POST', + credentials: "include", headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ jsonrpc: '2.0', diff --git a/web-ui/vue-app/src/assets/main.css b/web-ui/vue-app/src/assets/main.css index fb15eb5..eed72f5 100644 --- a/web-ui/vue-app/src/assets/main.css +++ b/web-ui/vue-app/src/assets/main.css @@ -16,17 +16,15 @@ body { margin: 0; - display: flex; - place-items: center; min-width: 320px; min-height: 100vh; } #app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + width: 100%; + margin: 0; + padding: 0; + text-align: left; } a { @@ -46,4 +44,4 @@ a:hover { a:hover { color: #747bff; } -} \ No newline at end of file +} diff --git a/web-ui/vue-app/src/components/FooterBar.vue b/web-ui/vue-app/src/components/HeaderBar.vue similarity index 73% rename from web-ui/vue-app/src/components/FooterBar.vue rename to web-ui/vue-app/src/components/HeaderBar.vue index 5bb4a3a..c46f84b 100644 --- a/web-ui/vue-app/src/components/FooterBar.vue +++ b/web-ui/vue-app/src/components/HeaderBar.vue @@ -1,9 +1,16 @@