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