Compare commits
3 Commits
f882cf4c3a
...
b5d021d03c
| Author | SHA1 | Date |
|---|---|---|
|
|
b5d021d03c | |
|
|
b46d1bc359 | |
|
|
4032586662 |
|
|
@ -43,3 +43,6 @@ xcpdata.mv.db
|
||||||
/web-ui/api.spec.json
|
/web-ui/api.spec.json
|
||||||
api-sandbox/app/node_modules
|
api-sandbox/app/node_modules
|
||||||
api-sandbox/app/api.spec.json
|
api-sandbox/app/api.spec.json
|
||||||
|
/xcpdata.trace.db
|
||||||
|
web-ui/vue-app/TODO.md
|
||||||
|
web-ui/vue-app/src/generated/*
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,9 @@ async function loadApiSpec() {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
}
|
}
|
||||||
apiSpec = await response.json()
|
const data = await response.json()
|
||||||
console.log('API Spec loaded:', apiSpec)
|
apiSpec = data.modules
|
||||||
|
console.log('API Spec loaded:', data)
|
||||||
return apiSpec
|
return apiSpec
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки API спецификации:', error)
|
console.error('Ошибка загрузки API спецификации:', error)
|
||||||
|
|
@ -62,8 +63,8 @@ function populateServices() {
|
||||||
$serviceSelect.empty()
|
$serviceSelect.empty()
|
||||||
$serviceSelect.append('<option value="">Выберите сервис</option>')
|
$serviceSelect.append('<option value="">Выберите сервис</option>')
|
||||||
|
|
||||||
Object.keys(apiSpec).forEach(serviceName => {
|
apiSpec.forEach(service => {
|
||||||
$serviceSelect.append(`<option value="${serviceName}">${serviceName}</option>`)
|
$serviceSelect.append(`<option value="${service.name}">${service.name}</option>`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,10 +73,13 @@ function populateMethods(serviceName) {
|
||||||
$methodSelect.empty()
|
$methodSelect.empty()
|
||||||
$methodSelect.append('<option value="">Выберите метод</option>')
|
$methodSelect.append('<option value="">Выберите метод</option>')
|
||||||
|
|
||||||
if (serviceName && apiSpec[serviceName] && apiSpec[serviceName].methods) {
|
if (serviceName) {
|
||||||
apiSpec[serviceName].methods.forEach(method => {
|
const service = apiSpec.find(s => s.name === serviceName)
|
||||||
$methodSelect.append(`<option value="${method.name}">${method.name}</option>`)
|
if (service && service.methods) {
|
||||||
})
|
service.methods.forEach(method => {
|
||||||
|
$methodSelect.append(`<option value="${method.name}">${method.name}</option>`)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +89,7 @@ function createParamInputs(methodName, serviceName) {
|
||||||
|
|
||||||
if (!serviceName || !methodName) return
|
if (!serviceName || !methodName) return
|
||||||
|
|
||||||
const service = apiSpec[serviceName]
|
const service = apiSpec.find(s => s.name === serviceName)
|
||||||
if (!service || !service.methods) return
|
if (!service || !service.methods) return
|
||||||
|
|
||||||
const method = service.methods.find(m => m.name === methodName)
|
const method = service.methods.find(m => m.name === methodName)
|
||||||
|
|
@ -116,7 +120,7 @@ function createParamInputs(methodName, serviceName) {
|
||||||
`)
|
`)
|
||||||
$paramsContainer.append($paramDiv)
|
$paramsContainer.append($paramDiv)
|
||||||
|
|
||||||
$(`#defined-${param.name}`).on('change', function() {
|
$(`#defined-${param.name}`).on('change', function () {
|
||||||
const $input = $(`#param-${param.name}`)
|
const $input = $(`#param-${param.name}`)
|
||||||
const isChecked = $(this).is(':checked')
|
const isChecked = $(this).is(':checked')
|
||||||
$input.prop('disabled', !isChecked)
|
$input.prop('disabled', !isChecked)
|
||||||
|
|
@ -158,7 +162,7 @@ async function sendRequest() {
|
||||||
const paramType = $input.attr('id').replace('param-', '')
|
const paramType = $input.attr('id').replace('param-', '')
|
||||||
|
|
||||||
// Find parameter type from method definition
|
// 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 method = service.methods.find(m => m.name === methodName)
|
||||||
const paramDef = method.params.find(p => p.name === paramName)
|
const paramDef = method.params.find(p => p.name === paramName)
|
||||||
const isObjectOrArray = paramDef && (paramDef.type === 'object' || paramDef.type === 'array')
|
const isObjectOrArray = paramDef && (paramDef.type === 'object' || paramDef.type === 'array')
|
||||||
|
|
@ -238,7 +242,7 @@ async function sendRequest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(async () => {
|
$(document).ready(async () => {
|
||||||
await loadApiSpec()
|
apiSpec = await loadApiSpec()
|
||||||
if (apiSpec) {
|
if (apiSpec) {
|
||||||
populateServices()
|
populateServices()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package ru.kirillius.XCP.ApiGenerator;
|
package ru.kirillius.XCP.ApiGenerator;
|
||||||
|
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
|
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
|
||||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
|
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
|
||||||
import tools.jackson.databind.JsonNode;
|
import tools.jackson.databind.JsonNode;
|
||||||
|
|
@ -9,9 +10,14 @@ import tools.jackson.databind.node.ArrayNode;
|
||||||
import tools.jackson.databind.node.JsonNodeFactory;
|
import tools.jackson.databind.node.JsonNodeFactory;
|
||||||
import tools.jackson.databind.node.ObjectNode;
|
import tools.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class SpecGenerator {
|
public class SpecGenerator {
|
||||||
|
|
@ -46,10 +52,14 @@ public class SpecGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ObjectNode specs = JsonNodeFactory.instance.objectNode();
|
private final ObjectNode specs = JsonNodeFactory.instance.objectNode();
|
||||||
|
private final Set<Class<?>> types = new HashSet<>();
|
||||||
|
private ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public void generate() {
|
public void generate() {
|
||||||
specs.removeAll();
|
specs.removeAll();
|
||||||
|
var modules = specs.putArray("modules");
|
||||||
|
var typesArray = specs.putArray("types");
|
||||||
var scanPath = new File(scanDir, packageName.replaceAll(Pattern.quote("."), "/"));
|
var scanPath = new File(scanDir, packageName.replaceAll(Pattern.quote("."), "/"));
|
||||||
for (var file : Objects.requireNonNull(scanPath.listFiles())) {
|
for (var file : Objects.requireNonNull(scanPath.listFiles())) {
|
||||||
if (!file.getName().endsWith(".java") || file.getName().contains("$")) {
|
if (!file.getName().endsWith(".java") || file.getName().contains("$")) {
|
||||||
|
|
@ -64,7 +74,7 @@ public class SpecGenerator {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var classSpecs = specs.putObject(cls.getSimpleName());
|
var classSpecs = modules.addObject();
|
||||||
classSpecs.put("name", cls.getSimpleName());
|
classSpecs.put("name", cls.getSimpleName());
|
||||||
var methods = classSpecs.putArray("methods");
|
var methods = classSpecs.putArray("methods");
|
||||||
|
|
||||||
|
|
@ -78,7 +88,7 @@ public class SpecGenerator {
|
||||||
methodSpecs.put("description", descriptor.description());
|
methodSpecs.put("description", descriptor.description());
|
||||||
methodSpecs.put("return", getTypeName(descriptor.returnType()));
|
methodSpecs.put("return", getTypeName(descriptor.returnType()));
|
||||||
methodSpecs.put("accessLevel", descriptor.accessLevel().name());
|
methodSpecs.put("accessLevel", descriptor.accessLevel().name());
|
||||||
|
registerType(descriptor.returnType());
|
||||||
var params = methodSpecs.putArray("params");
|
var params = methodSpecs.putArray("params");
|
||||||
|
|
||||||
for (var parameter : descriptor.parameters()) {
|
for (var parameter : descriptor.parameters()) {
|
||||||
|
|
@ -87,14 +97,72 @@ public class SpecGenerator {
|
||||||
paramSpecs.put("type", getTypeName(parameter.type()));
|
paramSpecs.put("type", getTypeName(parameter.type()));
|
||||||
paramSpecs.put("description", parameter.description());
|
paramSpecs.put("description", parameter.description());
|
||||||
paramSpecs.put("optional", parameter.optional());
|
paramSpecs.put("optional", parameter.optional());
|
||||||
|
registerType(parameter.type());
|
||||||
}
|
}
|
||||||
|
|
||||||
methodSpecs.put("return", getTypeName(descriptor.returnType()));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
var typesQueue = new LinkedBlockingQueue<>(types);
|
||||||
|
Class<?> type;
|
||||||
|
while (!typesQueue.isEmpty()) {
|
||||||
|
type = typesQueue.poll();
|
||||||
|
if (type.isEnum()) {
|
||||||
|
var typeDescriptor = typesArray.addObject();
|
||||||
|
typeDescriptor.put("name", type.getSimpleName());
|
||||||
|
typeDescriptor.put("type", "enum");
|
||||||
|
var values = typeDescriptor.putArray("values");
|
||||||
|
for (var value : type.getEnumConstants()) {
|
||||||
|
values.add(value.toString());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var classAnnotation = type.getAnnotation(GenerateApiSpec.class);
|
||||||
|
if (classAnnotation == null || !type.isInterface()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeDescriptor = typesArray.addObject();
|
||||||
|
typeDescriptor.put("name", classAnnotation.alias().isEmpty() ? type.getSimpleName() : classAnnotation.alias());
|
||||||
|
typeDescriptor.put("type", "class");
|
||||||
|
var fields = typeDescriptor.putArray("fields");
|
||||||
|
for (var method : type.getMethods()) {
|
||||||
|
var methodAnnotation = method.getAnnotation(GenerateApiSpec.class);
|
||||||
|
if (methodAnnotation == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var returnType = methodAnnotation.type();
|
||||||
|
if (returnType == void.class) {
|
||||||
|
|
||||||
|
returnType = method.getReturnType();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!types.contains(returnType)) {
|
||||||
|
types.add(returnType);
|
||||||
|
typesQueue.put(returnType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var methodName = method.getName();
|
||||||
|
if (!methodName.startsWith("get") && !methodName.startsWith("is")) {
|
||||||
|
throw new RuntimeException("Invalid @" + GenerateApiSpec.class.getSimpleName() + " usage on " + type.getSimpleName() + "::" + methodName + ": Only getters are supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
var fieldDescriptor = fields.addObject();
|
||||||
|
fieldDescriptor.put("name", methodAnnotation.alias().isEmpty() ? getVariableName(methodName) : methodAnnotation.alias());
|
||||||
|
fieldDescriptor.put("type", getTypeName(returnType));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getVariableName(String methodName) {
|
||||||
|
if (methodName.startsWith("get")) {
|
||||||
|
return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
|
||||||
|
} else if (methodName.startsWith("is")) {
|
||||||
|
return methodName.substring(2, 3).toLowerCase() + methodName.substring(3);
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Invalid method name: " + methodName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeSpecs() throws IOException {
|
public void writeSpecs() throws IOException {
|
||||||
|
|
@ -108,14 +176,31 @@ public class SpecGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getTypeName(Class<?> type) {
|
private void registerType(Class<?> type) {
|
||||||
|
if (type.isArray()) {
|
||||||
|
types.add(type.getComponentType());
|
||||||
|
} else {
|
||||||
|
types.add(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTypeName(Class<?> type) {
|
||||||
if (type == null) return "unknown";
|
if (type == null) return "unknown";
|
||||||
if (type == boolean.class) return "boolean";
|
if (type == boolean.class) return "boolean";
|
||||||
if (type == int.class || type == long.class) return "number";
|
if (type == int.class || type == long.class) return "number";
|
||||||
if (type == String.class) return "string";
|
if (type == String.class) return "string";
|
||||||
if (type == void.class || type == Void.class) return "void";
|
if (type == void.class || type == Void.class) return "void";
|
||||||
|
|
||||||
if (type.isArray() || Collection.class.isAssignableFrom(type) || type == ArrayNode.class) {
|
if (type.isArray()) {
|
||||||
|
var componentType = type.getComponentType();
|
||||||
|
if (componentType != null) {
|
||||||
|
return componentType.getSimpleName() + "[]";
|
||||||
|
} else {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Collection.class.isAssignableFrom(type) || type == ArrayNode.class) {
|
||||||
return "array";
|
return "array";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
140
api.spec.json
140
api.spec.json
|
|
@ -1,140 +0,0 @@
|
||||||
{
|
|
||||||
"Profile" : {
|
|
||||||
"name" : "Profile",
|
|
||||||
"methods" : [ {
|
|
||||||
"name" : "get",
|
|
||||||
"description" : "Get current user profile",
|
|
||||||
"return" : "void",
|
|
||||||
"accessLevel" : "Guest",
|
|
||||||
"params" : [ ]
|
|
||||||
}, {
|
|
||||||
"name" : "save",
|
|
||||||
"description" : "edit current user profile",
|
|
||||||
"return" : "boolean",
|
|
||||||
"accessLevel" : "User",
|
|
||||||
"params" : [ {
|
|
||||||
"name" : "login",
|
|
||||||
"type" : "string",
|
|
||||||
"description" : "User login. Have to be unique",
|
|
||||||
"optional" : false
|
|
||||||
}, {
|
|
||||||
"name" : "name",
|
|
||||||
"type" : "string",
|
|
||||||
"description" : "User display name",
|
|
||||||
"optional" : false
|
|
||||||
}, {
|
|
||||||
"name" : "password",
|
|
||||||
"type" : "string",
|
|
||||||
"description" : "Change user password if defined",
|
|
||||||
"optional" : true
|
|
||||||
}, {
|
|
||||||
"name" : "values",
|
|
||||||
"type" : "object",
|
|
||||||
"description" : "User custom values",
|
|
||||||
"optional" : false
|
|
||||||
} ]
|
|
||||||
} ]
|
|
||||||
},
|
|
||||||
"UserManagement" : {
|
|
||||||
"name" : "UserManagement",
|
|
||||||
"methods" : [ {
|
|
||||||
"name" : "getById",
|
|
||||||
"description" : "Load user object by ID",
|
|
||||||
"return" : "object",
|
|
||||||
"accessLevel" : "Admin",
|
|
||||||
"params" : [ {
|
|
||||||
"name" : "id",
|
|
||||||
"type" : "number",
|
|
||||||
"description" : "User id",
|
|
||||||
"optional" : false
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"name" : "remove",
|
|
||||||
"description" : "Remove user by ID",
|
|
||||||
"return" : "object",
|
|
||||||
"accessLevel" : "Admin",
|
|
||||||
"params" : [ {
|
|
||||||
"name" : "id",
|
|
||||||
"type" : "number",
|
|
||||||
"description" : "User id",
|
|
||||||
"optional" : false
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"name" : "save",
|
|
||||||
"description" : "Get all users as list",
|
|
||||||
"return" : "array",
|
|
||||||
"accessLevel" : "Admin",
|
|
||||||
"params" : [ {
|
|
||||||
"name" : "user",
|
|
||||||
"type" : "object",
|
|
||||||
"description" : "User object to save",
|
|
||||||
"optional" : false
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"name" : "getAll",
|
|
||||||
"description" : "Get all users as list",
|
|
||||||
"return" : "array",
|
|
||||||
"accessLevel" : "Admin",
|
|
||||||
"params" : [ ]
|
|
||||||
} ]
|
|
||||||
},
|
|
||||||
"Auth" : {
|
|
||||||
"name" : "Auth",
|
|
||||||
"methods" : [ {
|
|
||||||
"name" : "authenticateByPassword",
|
|
||||||
"description" : "Authenticates a user using login and password. Returns true if authentication is successful.",
|
|
||||||
"return" : "boolean",
|
|
||||||
"accessLevel" : "Guest",
|
|
||||||
"params" : [ {
|
|
||||||
"name" : "login",
|
|
||||||
"type" : "string",
|
|
||||||
"description" : "User's login name",
|
|
||||||
"optional" : false
|
|
||||||
}, {
|
|
||||||
"name" : "password",
|
|
||||||
"type" : "string",
|
|
||||||
"description" : "User's password",
|
|
||||||
"optional" : false
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"name" : "getTokens",
|
|
||||||
"description" : "Retrieves all API tokens associated with the current user",
|
|
||||||
"return" : "array",
|
|
||||||
"accessLevel" : "User",
|
|
||||||
"params" : [ ]
|
|
||||||
}, {
|
|
||||||
"name" : "generateToken",
|
|
||||||
"description" : "Generates a new API token and returns its details",
|
|
||||||
"return" : "object",
|
|
||||||
"accessLevel" : "User",
|
|
||||||
"params" : [ {
|
|
||||||
"name" : "permanent",
|
|
||||||
"type" : "boolean",
|
|
||||||
"description" : "If true, creates a token that never expires",
|
|
||||||
"optional" : true
|
|
||||||
}, {
|
|
||||||
"name" : "name",
|
|
||||||
"type" : "string",
|
|
||||||
"description" : "Display name for the token. If not provided, the User-Agent header will be used",
|
|
||||||
"optional" : true
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"name" : "authenticateByToken",
|
|
||||||
"description" : "Authenticates a user using an API token. Returns true if authentication is successful.",
|
|
||||||
"return" : "boolean",
|
|
||||||
"accessLevel" : "Guest",
|
|
||||||
"params" : [ {
|
|
||||||
"name" : "token",
|
|
||||||
"type" : "string",
|
|
||||||
"description" : "API token string for authentication",
|
|
||||||
"optional" : false
|
|
||||||
} ]
|
|
||||||
}, {
|
|
||||||
"name" : "logout",
|
|
||||||
"description" : "Logs out the current user. Returns true if logout is successful, false if the user is not logged in",
|
|
||||||
"return" : "boolean",
|
|
||||||
"accessLevel" : "User",
|
|
||||||
"params" : [ ]
|
|
||||||
} ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package ru.kirillius.XCP.Commons;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||||
|
public @interface GenerateApiSpec {
|
||||||
|
String alias() default "";
|
||||||
|
|
||||||
|
Class<?> type() default void.class;
|
||||||
|
}
|
||||||
|
|
@ -1,27 +1,36 @@
|
||||||
package ru.kirillius.XCP.Persistence.Entities;
|
package ru.kirillius.XCP.Persistence.Entities;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||||
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
public interface ApiToken extends PersistenceEntity {
|
public interface ApiToken extends PersistenceEntity {
|
||||||
|
|
||||||
|
@GenerateApiSpec(type = PersistenceEntity.class)
|
||||||
User getUser();
|
User getUser();
|
||||||
|
|
||||||
void setUser(User user);
|
void setUser(User user);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
String getName();
|
String getName();
|
||||||
|
|
||||||
void setName(String name);
|
void setName(String name);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
Date getExpirationDate();
|
Date getExpirationDate();
|
||||||
|
|
||||||
void setExpirationDate(Date expirationDate);
|
void setExpirationDate(Date expirationDate);
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
default boolean isExpired() {
|
default boolean isExpired() {
|
||||||
return getExpirationDate().toInstant().isBefore(Instant.now());
|
var expirationDate = getExpirationDate();
|
||||||
|
if (expirationDate == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return expirationDate.toInstant().isBefore(Instant.now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
package ru.kirillius.XCP.Persistence.Entities;
|
package ru.kirillius.XCP.Persistence.Entities;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||||
import ru.kirillius.XCP.Persistence.NodeEntity;
|
import ru.kirillius.XCP.Persistence.NodeEntity;
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
public interface Group extends NodeEntity {
|
public interface Group extends NodeEntity {
|
||||||
String getIcon();
|
String getIcon();
|
||||||
|
|
||||||
void setIcon(String icon);
|
void setIcon(String icon);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
boolean isPrototype();
|
boolean isPrototype();
|
||||||
|
|
||||||
void setPrototype(boolean prototype);
|
void setPrototype(boolean prototype);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package ru.kirillius.XCP.Persistence.Entities;
|
package ru.kirillius.XCP.Persistence.Entities;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||||
import ru.kirillius.XCP.Data.PollSettings;
|
import ru.kirillius.XCP.Data.PollSettings;
|
||||||
import ru.kirillius.XCP.Persistence.IOEntity;
|
import ru.kirillius.XCP.Persistence.IOEntity;
|
||||||
import ru.kirillius.XCP.Persistence.NodeEntity;
|
import ru.kirillius.XCP.Persistence.NodeEntity;
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
public interface Input extends IOEntity, NodeEntity {
|
public interface Input extends IOEntity, NodeEntity {
|
||||||
|
@GenerateApiSpec
|
||||||
PollSettings getPollSettings();
|
PollSettings getPollSettings();
|
||||||
|
|
||||||
void setPollSettings(PollSettings pollSettings);
|
void setPollSettings(PollSettings pollSettings);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
package ru.kirillius.XCP.Persistence.Entities;
|
package ru.kirillius.XCP.Persistence.Entities;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||||
import ru.kirillius.XCP.Persistence.IOEntity;
|
import ru.kirillius.XCP.Persistence.IOEntity;
|
||||||
import ru.kirillius.XCP.Persistence.NodeEntity;
|
import ru.kirillius.XCP.Persistence.NodeEntity;
|
||||||
|
@GenerateApiSpec
|
||||||
public interface Output extends IOEntity, NodeEntity {
|
public interface Output extends IOEntity, NodeEntity {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package ru.kirillius.XCP.Persistence.Entities;
|
package ru.kirillius.XCP.Persistence.Entities;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||||
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
public interface Tag extends PersistenceEntity {
|
public interface Tag extends PersistenceEntity {
|
||||||
|
@GenerateApiSpec
|
||||||
String getName();
|
String getName();
|
||||||
|
|
||||||
void setName(String name);
|
void setName(String name);
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,32 @@
|
||||||
package ru.kirillius.XCP.Persistence.Entities;
|
package ru.kirillius.XCP.Persistence.Entities;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||||
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
import ru.kirillius.XCP.Persistence.PersistenceEntity;
|
||||||
import ru.kirillius.XCP.Security.UserRole;
|
import ru.kirillius.XCP.Security.UserRole;
|
||||||
import tools.jackson.databind.node.ObjectNode;
|
import tools.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
public interface User extends PersistenceEntity {
|
public interface User extends PersistenceEntity {
|
||||||
void setPassword(String password);
|
void setPassword(String password);
|
||||||
|
|
||||||
boolean verifyPassword(String password);
|
boolean verifyPassword(String password);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
String getLogin();
|
String getLogin();
|
||||||
|
|
||||||
void setLogin(String login);
|
void setLogin(String login);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
UserRole getRole();
|
UserRole getRole();
|
||||||
|
|
||||||
void setRole(UserRole role);
|
void setRole(UserRole role);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
ObjectNode getValues();
|
ObjectNode getValues();
|
||||||
|
|
||||||
void setValues(ObjectNode values);
|
void setValues(ObjectNode values);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
String getName();
|
String getName();
|
||||||
|
|
||||||
void setName(String name);
|
void setName(String name);
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,36 @@
|
||||||
package ru.kirillius.XCP.Persistence;
|
package ru.kirillius.XCP.Persistence;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||||
import ru.kirillius.XCP.Persistence.Entities.Group;
|
import ru.kirillius.XCP.Persistence.Entities.Group;
|
||||||
import tools.jackson.databind.node.ObjectNode;
|
import tools.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
public interface NodeEntity extends PersistenceEntity {
|
public interface NodeEntity extends PersistenceEntity {
|
||||||
|
@GenerateApiSpec
|
||||||
String getName();
|
String getName();
|
||||||
|
|
||||||
void setName(String name);
|
void setName(String name);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
boolean isProtectedEntity();
|
boolean isProtectedEntity();
|
||||||
|
|
||||||
void setProtectedEntity(boolean essential);
|
void setProtectedEntity(boolean essential);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
boolean isEnabled();
|
boolean isEnabled();
|
||||||
|
|
||||||
void setEnabled(boolean enabled);
|
void setEnabled(boolean enabled);
|
||||||
|
|
||||||
|
@GenerateApiSpec(type = PersistenceEntity.class)
|
||||||
Group getParent();
|
Group getParent();
|
||||||
|
|
||||||
void setParent(Group parent);
|
void setParent(Group parent);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
ObjectNode getProperties();
|
ObjectNode getProperties();
|
||||||
|
|
||||||
void setProperties(ObjectNode properties);
|
void setProperties(ObjectNode properties);
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
TagCollection getTags();
|
TagCollection getTags();
|
||||||
|
|
||||||
void setTags(TagCollection tags);
|
void setTags(TagCollection tags);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
package ru.kirillius.XCP.Persistence;
|
package ru.kirillius.XCP.Persistence;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Commons.GenerateApiSpec;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@GenerateApiSpec
|
||||||
public interface PersistenceEntity {
|
public interface PersistenceEntity {
|
||||||
|
@GenerateApiSpec
|
||||||
long getId();
|
long getId();
|
||||||
|
|
||||||
|
@GenerateApiSpec(type = String.class)
|
||||||
UUID getUuid();
|
UUID getUuid();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package ru.kirillius.XCP.RPC.Services;
|
package ru.kirillius.XCP.RPC.Services;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Persistence.Entities.ApiToken;
|
||||||
import ru.kirillius.XCP.Persistence.Repositories.ApiTokenRepository;
|
import ru.kirillius.XCP.Persistence.Repositories.ApiTokenRepository;
|
||||||
import ru.kirillius.XCP.Persistence.Repositories.UserRepository;
|
import ru.kirillius.XCP.Persistence.Repositories.UserRepository;
|
||||||
import ru.kirillius.XCP.Services.RepositoryService;
|
import ru.kirillius.XCP.Services.RepositoryService;
|
||||||
|
|
@ -56,11 +57,10 @@ public class Auth extends JsonRpcService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@JsonRpcMethod(
|
@JsonRpcMethod(
|
||||||
accessLevel = UserRole.User,
|
accessLevel = UserRole.User,
|
||||||
description = "Retrieves all API tokens associated with the current user",
|
description = "Retrieves all API tokens associated with the current user",
|
||||||
returnType = ArrayNode.class
|
returnType = ApiToken[].class
|
||||||
)
|
)
|
||||||
public ArrayNode getTokens(CallContext call) throws IOException {
|
public ArrayNode getTokens(CallContext call) throws IOException {
|
||||||
var tokenRepository = call.getContext().getService(RepositoryService.class).getRepository(ApiTokenRepository.class);
|
var tokenRepository = call.getContext().getService(RepositoryService.class).getRepository(ApiTokenRepository.class);
|
||||||
|
|
@ -70,14 +70,13 @@ public class Auth extends JsonRpcService {
|
||||||
var json = tokenRepository.serialize(token);
|
var json = tokenRepository.serialize(token);
|
||||||
json.remove("uuid");
|
json.remove("uuid");
|
||||||
var uuid = token.getUuid().toString();
|
var uuid = token.getUuid().toString();
|
||||||
json.put("token", uuid.substring(0, 4) + "..." + uuid.substring(uuid.length() - 4));
|
json.put("uuid", uuid.substring(0, 4) + "..." + uuid.substring(uuid.length() - 4));
|
||||||
return json;
|
return json;
|
||||||
}).forEach(tokens::add);
|
}).forEach(tokens::add);
|
||||||
}
|
}
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@JsonRpcMethod(
|
@JsonRpcMethod(
|
||||||
accessLevel = UserRole.User,
|
accessLevel = UserRole.User,
|
||||||
description = "Generates a new API token and returns its details",
|
description = "Generates a new API token and returns its details",
|
||||||
|
|
@ -95,7 +94,7 @@ public class Auth extends JsonRpcService {
|
||||||
optional = true
|
optional = true
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
returnType = ObjectNode.class
|
returnType = ApiToken.class
|
||||||
)
|
)
|
||||||
public ObjectNode generateToken(CallContext call) {
|
public ObjectNode generateToken(CallContext call) {
|
||||||
var repositoryService = call.getContext().getService(RepositoryService.class);
|
var repositoryService = call.getContext().getService(RepositoryService.class);
|
||||||
|
|
@ -112,6 +111,7 @@ public class Auth extends JsonRpcService {
|
||||||
|
|
||||||
token.setExpirationDate(permanent ? null : Date.from(Instant.now().plus(30, ChronoUnit.DAYS)));
|
token.setExpirationDate(permanent ? null : Date.from(Instant.now().plus(30, ChronoUnit.DAYS)));
|
||||||
token.setName(name);
|
token.setName(name);
|
||||||
|
token.setUser(call.getCurrentUser());
|
||||||
|
|
||||||
tokenRepository.save(token);
|
tokenRepository.save(token);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
package ru.kirillius.XCP.RPC.Services;
|
package ru.kirillius.XCP.RPC.Services;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Persistence.Entities.User;
|
||||||
import ru.kirillius.XCP.Persistence.Repositories.UserRepository;
|
import ru.kirillius.XCP.Persistence.Repositories.UserRepository;
|
||||||
import ru.kirillius.XCP.Services.RepositoryService;
|
|
||||||
import ru.kirillius.XCP.RPC.JSONRPC.CallContext;
|
import ru.kirillius.XCP.RPC.JSONRPC.CallContext;
|
||||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
|
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
|
||||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
|
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
|
||||||
import ru.kirillius.XCP.Security.UserRole;
|
import ru.kirillius.XCP.Security.UserRole;
|
||||||
|
import ru.kirillius.XCP.Services.RepositoryService;
|
||||||
import tools.jackson.databind.JsonNode;
|
import tools.jackson.databind.JsonNode;
|
||||||
import tools.jackson.databind.node.ObjectNode;
|
import tools.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
public class Profile extends JsonRpcService {
|
public class Profile extends JsonRpcService {
|
||||||
|
|
||||||
@JsonRpcMethod(
|
@JsonRpcMethod(
|
||||||
accessLevel = UserRole.User,
|
accessLevel = UserRole.User,
|
||||||
description = "edit current user profile",
|
description = "edit current user profile",
|
||||||
|
|
@ -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 = "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 = "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 = "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)
|
@JsonRpcMethod.Parameter(name = "values", description = "User custom values", type = ObjectNode.class)
|
||||||
},
|
},
|
||||||
returnType = boolean.class)
|
returnType = boolean.class)
|
||||||
|
|
@ -28,6 +29,8 @@ public class Profile extends JsonRpcService {
|
||||||
var login = requireParam(call, "login", JsonNode::asString);
|
var login = requireParam(call, "login", JsonNode::asString);
|
||||||
var name = requireParam(call, "name", JsonNode::asString);
|
var name = requireParam(call, "name", JsonNode::asString);
|
||||||
var passwordOptional = getParam(call, "password", 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);
|
var values = requireParam(call, "values", n -> (ObjectNode) n);
|
||||||
|
|
||||||
if (!user.getLogin().equals(login) && userRepository.getByLogin(login) != null) {
|
if (!user.getLogin().equals(login) && userRepository.getByLogin(login) != null) {
|
||||||
|
|
@ -50,6 +53,9 @@ public class Profile extends JsonRpcService {
|
||||||
if (password.isBlank()) {
|
if (password.isBlank()) {
|
||||||
throw new RuntimeException("Password is blank");
|
throw new RuntimeException("Password is blank");
|
||||||
}
|
}
|
||||||
|
if (!user.verifyPassword(currentPassword)) {
|
||||||
|
throw new RuntimeException("Current user password is invalid");
|
||||||
|
}
|
||||||
user.setPassword(password);
|
user.setPassword(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +66,8 @@ public class Profile extends JsonRpcService {
|
||||||
|
|
||||||
@JsonRpcMethod(
|
@JsonRpcMethod(
|
||||||
accessLevel = UserRole.Guest,
|
accessLevel = UserRole.Guest,
|
||||||
description = "Get current user profile"
|
description = "Get current user profile",
|
||||||
|
returnType = User.class
|
||||||
)
|
)
|
||||||
public ObjectNode get(CallContext call) {
|
public ObjectNode get(CallContext call) {
|
||||||
var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class);
|
var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class);
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
package ru.kirillius.XCP.RPC.Services;
|
package ru.kirillius.XCP.RPC.Services;
|
||||||
|
|
||||||
|
import ru.kirillius.XCP.Persistence.Entities.User;
|
||||||
import ru.kirillius.XCP.Persistence.Repositories.UserRepository;
|
import ru.kirillius.XCP.Persistence.Repositories.UserRepository;
|
||||||
import ru.kirillius.XCP.Services.RepositoryService;
|
|
||||||
import ru.kirillius.XCP.RPC.JSONRPC.CallContext;
|
import ru.kirillius.XCP.RPC.JSONRPC.CallContext;
|
||||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
|
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
|
||||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
|
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
|
||||||
import ru.kirillius.XCP.Security.UserRole;
|
import ru.kirillius.XCP.Security.UserRole;
|
||||||
|
import ru.kirillius.XCP.Services.RepositoryService;
|
||||||
import tools.jackson.databind.JsonNode;
|
import tools.jackson.databind.JsonNode;
|
||||||
import tools.jackson.databind.node.ArrayNode;
|
import tools.jackson.databind.node.ArrayNode;
|
||||||
import tools.jackson.databind.node.ObjectNode;
|
import tools.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class UserManagement extends JsonRpcService {
|
public class Users extends JsonRpcService {
|
||||||
@JsonRpcMethod(
|
@JsonRpcMethod(
|
||||||
accessLevel = UserRole.Admin,
|
accessLevel = UserRole.Admin,
|
||||||
description = "Get all users as list",
|
description = "Get all users as list",
|
||||||
parameters = {
|
parameters = {
|
||||||
|
|
||||||
},
|
},
|
||||||
returnType = ArrayNode.class
|
returnType = User[].class
|
||||||
)
|
)
|
||||||
public ArrayNode getAll(CallContext call) throws IOException {
|
public ArrayNode getAll(CallContext call) throws IOException {
|
||||||
var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class);
|
var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class);
|
||||||
|
|
@ -30,11 +31,11 @@ public class UserManagement extends JsonRpcService {
|
||||||
|
|
||||||
@JsonRpcMethod(
|
@JsonRpcMethod(
|
||||||
accessLevel = UserRole.Admin,
|
accessLevel = UserRole.Admin,
|
||||||
description = "Get all users as list",
|
description = "Save user data",
|
||||||
parameters = {
|
parameters = {
|
||||||
@JsonRpcMethod.Parameter(name = "user", description = "User object to save", type = ObjectNode.class)
|
@JsonRpcMethod.Parameter(name = "user", description = "User object to save", type = User.class)
|
||||||
},
|
},
|
||||||
returnType = ArrayNode.class
|
returnType = User.class
|
||||||
)
|
)
|
||||||
public void save(CallContext call) {
|
public void save(CallContext call) {
|
||||||
var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class);
|
var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class);
|
||||||
|
|
@ -48,7 +49,7 @@ public class UserManagement extends JsonRpcService {
|
||||||
parameters = {
|
parameters = {
|
||||||
@JsonRpcMethod.Parameter(name = "id", description = "User id", type = int.class)
|
@JsonRpcMethod.Parameter(name = "id", description = "User id", type = int.class)
|
||||||
},
|
},
|
||||||
returnType = ObjectNode.class
|
returnType = User.class
|
||||||
)
|
)
|
||||||
public ObjectNode getById(CallContext call) {
|
public ObjectNode getById(CallContext call) {
|
||||||
var userId = requireParam(call, "id", JsonNode::asLong);
|
var userId = requireParam(call, "id", JsonNode::asLong);
|
||||||
|
|
@ -62,7 +63,7 @@ public class UserManagement extends JsonRpcService {
|
||||||
parameters = {
|
parameters = {
|
||||||
@JsonRpcMethod.Parameter(name = "id", description = "User id", type = int.class)
|
@JsonRpcMethod.Parameter(name = "id", description = "User id", type = int.class)
|
||||||
},
|
},
|
||||||
returnType = ObjectNode.class
|
returnType = boolean.class
|
||||||
)
|
)
|
||||||
public boolean remove(CallContext call) {
|
public boolean remove(CallContext call) {
|
||||||
var userId = requireParam(call, "id", JsonNode::asLong);
|
var userId = requireParam(call, "id", JsonNode::asLong);
|
||||||
|
|
@ -9,7 +9,7 @@ import ru.kirillius.XCP.Commons.Context;
|
||||||
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcServlet;
|
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcServlet;
|
||||||
import ru.kirillius.XCP.RPC.Services.Auth;
|
import ru.kirillius.XCP.RPC.Services.Auth;
|
||||||
import ru.kirillius.XCP.RPC.Services.Profile;
|
import ru.kirillius.XCP.RPC.Services.Profile;
|
||||||
import ru.kirillius.XCP.RPC.Services.UserManagement;
|
import ru.kirillius.XCP.RPC.Services.Users;
|
||||||
import ru.kirillius.XCP.Services.ServiceLoadPriority;
|
import ru.kirillius.XCP.Services.ServiceLoadPriority;
|
||||||
import ru.kirillius.XCP.Services.WebService;
|
import ru.kirillius.XCP.Services.WebService;
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ public class WebServiceImpl implements WebService {
|
||||||
}
|
}
|
||||||
var jsonRpc = new JsonRpcServlet(context);
|
var jsonRpc = new JsonRpcServlet(context);
|
||||||
jsonRpc.registerRpcService(
|
jsonRpc.registerRpcService(
|
||||||
UserManagement.class,
|
Users.class,
|
||||||
Auth.class,
|
Auth.class,
|
||||||
Profile.class
|
Profile.class
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,4 @@ coverage
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
/src/generated/RpcClient.ts
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import typescript from '@typescript-eslint/eslint-plugin'
|
||||||
|
import typescriptParser from '@typescript-eslint/parser'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ['dist/**/*', 'node_modules/**/*']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/**/*.{vue}'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: pluginVue.parser,
|
||||||
|
parserOptions: {
|
||||||
|
parser: typescriptParser,
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
vue: pluginVue
|
||||||
|
},
|
||||||
|
rules: pluginVue.configs['vue3-essential'].rules
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/**/*.{js,mjs,cjs,ts}'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: typescriptParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': typescript
|
||||||
|
},
|
||||||
|
rules: typescript.configs.recommended.rules
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -8,9 +8,11 @@
|
||||||
"name": "vue-app",
|
"name": "vue-app",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4",
|
||||||
|
"vuetify": "^3.11.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/vite-dev-server": "^7.1.0",
|
"@cypress/vite-dev-server": "^7.1.0",
|
||||||
|
|
@ -19,6 +21,8 @@
|
||||||
"@tsconfig/node18": "^18.2.4",
|
"@tsconfig/node18": "^18.2.4",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.9.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||||
|
"@typescript-eslint/parser": "^8.53.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.6.0",
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
|
|
@ -26,9 +30,12 @@
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"cypress": "^15.9.0",
|
"cypress": "^15.9.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-define-config": "^2.1.0",
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"jsdom": "^27.4.0",
|
"jsdom": "^27.4.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vitest": "^4.0.17",
|
"vitest": "^4.0.17",
|
||||||
|
|
@ -123,6 +130,19 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@csstools/color-helpers": {
|
"node_modules/@csstools/color-helpers": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||||
|
|
@ -1108,12 +1128,39 @@
|
||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mdi/font": {
|
||||||
|
"version": "7.4.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
|
||||||
|
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
@ -1554,6 +1601,34 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tsconfig/node10": {
|
||||||
|
"version": "1.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
|
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node12": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node14": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node16": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/node18": {
|
"node_modules/@tsconfig/node18": {
|
||||||
"version": "18.2.6",
|
"version": "18.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.6.tgz",
|
||||||
|
|
@ -2370,6 +2445,19 @@
|
||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
|
@ -2504,6 +2592,13 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
|
@ -3009,6 +3104,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|
@ -3237,6 +3339,16 @@
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||||
|
|
@ -3602,6 +3714,29 @@
|
||||||
"eslint": ">=7.0.0"
|
"eslint": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-define-config": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-define-config/-/eslint-define-config-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-QUp6pM9pjKEVannNAbSJNeRuYwW3LshejfyBBpjeMGaJjaDUpVps4C6KVR8R7dWZnD3i0synmrE36znjTkJvdQ==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/Shinigami92"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://www.paypal.com/donate/?hosted_button_id=L7GY729FBKTZY"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=9.0.0",
|
||||||
|
"pnpm": ">=8.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-plugin-prettier": {
|
"node_modules/eslint-plugin-prettier": {
|
||||||
"version": "5.5.5",
|
"version": "5.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
|
||||||
|
|
@ -4400,6 +4535,19 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-tsconfig": {
|
||||||
|
"version": "4.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||||
|
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/getpass": {
|
"node_modules/getpass": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||||
|
|
@ -5268,6 +5416,13 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -5986,6 +6141,16 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve-pkg-maps": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/restore-cursor": {
|
"node_modules/restore-cursor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||||
|
|
@ -6679,6 +6844,50 @@
|
||||||
"typescript": ">=4.8.4"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-node": {
|
||||||
|
"version": "10.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.1",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-node": "dist/bin.js",
|
||||||
|
"ts-node-cwd": "dist/bin-cwd.js",
|
||||||
|
"ts-node-esm": "dist/bin-esm.js",
|
||||||
|
"ts-node-script": "dist/bin-script.js",
|
||||||
|
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||||
|
"ts-script": "dist/bin-script-deprecated.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": ">=1.2.50",
|
||||||
|
"@swc/wasm": ">=1.2.50",
|
||||||
|
"@types/node": "*",
|
||||||
|
"typescript": ">=2.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/wasm": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
|
@ -6686,6 +6895,26 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||||
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.27.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tunnel-agent": {
|
"node_modules/tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
|
@ -6821,6 +7050,13 @@
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/verror": {
|
"node_modules/verror": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||||
|
|
@ -7143,6 +7379,33 @@
|
||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vuetify": {
|
||||||
|
"version": "3.11.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.11.6.tgz",
|
||||||
|
"integrity": "sha512-vaWvEpDSeldRoib1tCKNVBp60paTD2/n0a0TmrVLF9BN4CJJn6/A4VKG2Sg+DE8Yc+SNOtFtipChxSlQxjcUvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/johnleider"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.7",
|
||||||
|
"vite-plugin-vuetify": ">=2.1.0",
|
||||||
|
"vue": "^3.5.0",
|
||||||
|
"webpack-plugin-vuetify": ">=3.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/w3c-xmlserializer": {
|
"node_modules/w3c-xmlserializer": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
|
@ -7404,6 +7667,16 @@
|
||||||
"fd-slicer": "~1.1.0"
|
"fd-slicer": "~1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,22 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "npm run generate-client && vite",
|
||||||
"build": "vue-tsc && vite build",
|
"build": "npm run generate-client && vue-tsc && vite build",
|
||||||
|
"build:prod": "npm run generate-client && vue-tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"test:e2e": "cypress run",
|
"test:e2e": "cypress run",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/",
|
||||||
|
"generate-client": "npx tsx scripts/generate-rpc-client.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4",
|
||||||
|
"vuetify": "^3.11.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/vite-dev-server": "^7.1.0",
|
"@cypress/vite-dev-server": "^7.1.0",
|
||||||
|
|
@ -24,6 +28,8 @@
|
||||||
"@tsconfig/node18": "^18.2.4",
|
"@tsconfig/node18": "^18.2.4",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.9.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
||||||
|
"@typescript-eslint/parser": "^8.53.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.6.0",
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
|
|
@ -31,9 +37,12 @@
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"cypress": "^15.9.0",
|
"cypress": "^15.9.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-define-config": "^2.1.0",
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"jsdom": "^27.4.0",
|
"jsdom": "^27.4.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vitest": "^4.0.17",
|
"vitest": "^4.0.17",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
interface ApiMethodParam {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
description: string
|
||||||
|
optional: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiMethod {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
return: string
|
||||||
|
accessLevel: string
|
||||||
|
params: ApiMethodParam[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiModule {
|
||||||
|
name: string
|
||||||
|
methods: ApiMethod[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiType {
|
||||||
|
name: string
|
||||||
|
type: 'class' | 'enum'
|
||||||
|
fields?: Array<{
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
}>
|
||||||
|
values?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiSpec {
|
||||||
|
modules: ApiModule[]
|
||||||
|
types: ApiType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateMethod(method: ApiMethod): string {
|
||||||
|
const sortedParams = [...method.params].sort((a, b) => {
|
||||||
|
if (a.optional && !b.optional) return 1
|
||||||
|
if (!a.optional && b.optional) return -1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const params = sortedParams
|
||||||
|
.map((param) => {
|
||||||
|
const optional = param.optional ? '?' : ''
|
||||||
|
return `${param.name}${optional}: ${param.type}`
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
|
||||||
|
const callParams = method.params.map((param) => `${param.name}: ${param.name}`).join(', ')
|
||||||
|
|
||||||
|
return `
|
||||||
|
/**
|
||||||
|
* @description ${method.description}
|
||||||
|
${sortedParams.map((param) => ` * @param ${param.name} ${param.description}`).join('\n')}
|
||||||
|
* @return ${method.return}
|
||||||
|
*/
|
||||||
|
async ${method.name}(${params}): Promise<${method.return}> {
|
||||||
|
return this.call("${method.name}"${callParams ? ', {' + callParams + '}' : ''});
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
return `export class ${className} extends RpcModuleBase {
|
||||||
|
constructor(client: RpcClientBase) {
|
||||||
|
super(client, "${module.name}");
|
||||||
|
}${methods}
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateClient(spec: ApiSpec): string {
|
||||||
|
const modules = spec.modules
|
||||||
|
.map((module) => {
|
||||||
|
const moduleName = module.name
|
||||||
|
const className = `${module.name}Module`
|
||||||
|
return ` public ${moduleName}: ${className};`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
const initializations = spec.modules
|
||||||
|
.map((module) => {
|
||||||
|
const moduleName = module.name
|
||||||
|
const className = `${module.name}Module`
|
||||||
|
return ` this.${moduleName} = new ${className}(this);`
|
||||||
|
})
|
||||||
|
.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}
|
||||||
|
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
${initializations}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${moduleClasses}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const specPath = path.resolve(__dirname, '../../../api.spec.json')
|
||||||
|
const outputPath = path.resolve(__dirname, '../src/generated/RpcClient.ts')
|
||||||
|
|
||||||
|
console.log('🚀 Generating RPC client...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(specPath)) {
|
||||||
|
throw new Error(`API spec file not found: ${specPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const specContent = fs.readFileSync(specPath, 'utf-8')
|
||||||
|
const spec: ApiSpec = JSON.parse(specContent)
|
||||||
|
|
||||||
|
const clientCode = generateClient(spec)
|
||||||
|
|
||||||
|
const outputDir = path.dirname(outputPath)
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
main()
|
||||||
|
}
|
||||||
|
|
||||||
|
export { main as generateClient }
|
||||||
|
|
@ -1,21 +1,130 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<v-app>
|
||||||
<nav>
|
<LoginForm />
|
||||||
<RouterLink to="/">Home</RouterLink>
|
<Notifications />
|
||||||
<RouterLink to="/about">About</RouterLink>
|
|
||||||
</nav>
|
<template v-if="!authStore.showLoginForm && !appStore.isLoading">
|
||||||
<RouterView />
|
<div class="app-layout">
|
||||||
</div>
|
<!-- Часть А: Меню (слева для ПК, шторка для мобильных) -->
|
||||||
|
<NavigationMenu
|
||||||
|
ref="navigationMenu"
|
||||||
|
@update-collapsed="isCollapsed = $event"
|
||||||
|
@toggle-menu="toggleMobileMenu"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Часть Б: Футер + Контент -->
|
||||||
|
<div
|
||||||
|
class="content-section"
|
||||||
|
:class="{ 'with-menu': !isMobile, 'menu-collapsed': !isMobile && isCollapsed }"
|
||||||
|
:style="{ marginLeft: !isMobile ? (isCollapsed ? '80px' : '250px') : '0' }"
|
||||||
|
>
|
||||||
|
<!-- Футер (HeaderBar) - скрыт для ПК, показан для мобильных -->
|
||||||
|
<HeaderBar v-if="isMobile || showFooter" @toggle-menu="toggleMobileMenu" />
|
||||||
|
|
||||||
|
<!-- Основной контент -->
|
||||||
|
<v-main
|
||||||
|
:class="{
|
||||||
|
'mobile-main': isMobile,
|
||||||
|
'with-menu': !isMobile
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<RouterView />
|
||||||
|
</v-main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink, RouterView } from 'vue-router'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
import NavigationMenu from './components/NavigationMenu.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 toggleMobileMenu = () => {
|
||||||
|
if (navigationMenu.value) {
|
||||||
|
navigationMenu.value.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateWidth = () => {
|
||||||
|
windowWidth.value = window.innerWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
window.addEventListener('resize', updateWidth)
|
||||||
|
|
||||||
|
appStore.initializeApi('http://localhost:8080/api')
|
||||||
|
await authStore.initializeAuth()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', updateWidth)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
nav {
|
#app {
|
||||||
padding: 1rem;
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-application {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Основной layout контейнер */
|
||||||
|
.app-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Часть Б: Контентная секция */
|
||||||
|
.content-section {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: rgb(var(--v-theme-background));
|
||||||
|
transition: margin-left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для ПК - основной контент без отступов */
|
||||||
|
.v-main.with-menu {
|
||||||
|
padding: 16px;
|
||||||
|
flex: 1;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для мобильных - контент с отступом под футером */
|
||||||
|
.mobile-main {
|
||||||
|
padding-top: 45px !important;
|
||||||
|
flex: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-main :deep(.v-main__content) {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-main :deep(.v-container) {
|
||||||
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
export class RpcClientBase {
|
||||||
|
constructor(private baseUrl: string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async callModuleMethod<T>(module: string, methodName: string, params?: any):Promise<T> {
|
||||||
|
return this.call(module + "." + methodName, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async call<T>(method: string, params?: any): Promise<T> {
|
||||||
|
const response = await fetch(this.baseUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: "include",
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
id: Date.now()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`RPC call ${method} failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(`RPC error: ${data.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type {RpcClientBase} from "@/api/RpcClientBase.ts";
|
||||||
|
|
||||||
|
export class RpcModuleBase {
|
||||||
|
constructor(private client: RpcClientBase, private moduleName: string) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async call<T>(method: string, params?: any): Promise<T> {
|
||||||
|
return this.client.callModuleMethod(this.moduleName, method, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,17 +16,15 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
max-width: 1280px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0;
|
||||||
padding: 2rem;
|
padding: 0;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<template>
|
||||||
|
<v-app-bar
|
||||||
|
v-if="isMobile"
|
||||||
|
app
|
||||||
|
color="grey-darken-3"
|
||||||
|
class="mobile-header-bar"
|
||||||
|
elevation="4"
|
||||||
|
height="45"
|
||||||
|
>
|
||||||
|
<v-container fluid class="pa-0">
|
||||||
|
<v-row align="center" no-gutters>
|
||||||
|
<v-col cols="8" class="d-flex align-center">
|
||||||
|
<v-icon :icon="currentPageInfo.icon" color="white" class="ms-4 me-2" />
|
||||||
|
<span class="current-page-text">{{ currentPageInfo.title }}</span>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="4" class="text-end">
|
||||||
|
<v-btn icon="mdi-menu" variant="text" @click="toggleMenu" color="white" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-app-bar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const isMobile = ref(window.innerWidth < 768)
|
||||||
|
|
||||||
|
const getUserDisplayName = () => {
|
||||||
|
const user = appStore.currentUser
|
||||||
|
if (user?.name && user.name.trim()) {
|
||||||
|
return user.name
|
||||||
|
}
|
||||||
|
return user?.login || 'Профиль'
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPageInfo = computed(() => {
|
||||||
|
const routeMap: Record<string, { title: string; icon: string }> = {
|
||||||
|
'/': { title: 'Главная', icon: 'mdi-home' },
|
||||||
|
'/profile': { title: getUserDisplayName(), icon: 'mdi-account' },
|
||||||
|
'/settings': { title: 'Настройки', icon: 'mdi-cog' },
|
||||||
|
'/dashboard': { title: 'Панель управления', icon: 'mdi-view-dashboard' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return routeMap[route.path] || { title: 'Неизвестная страница', icon: 'mdi-help' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
toggleMenu: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
emit('toggleMenu')
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkMobile = () => {
|
||||||
|
isMobile.value = window.innerWidth < 768
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', checkMobile)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', checkMobile)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header-bar {
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-page-text {
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="show" persistent max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">Авторизация</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit.prevent="handleSubmit">
|
||||||
|
<v-text-field
|
||||||
|
ref="loginField"
|
||||||
|
v-model="loginForm.login"
|
||||||
|
label="Логин"
|
||||||
|
prepend-inner-icon="mdi-account"
|
||||||
|
variant="outlined"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="mb-3"
|
||||||
|
@keyup.enter="focusPassword"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
ref="passwordField"
|
||||||
|
v-model="loginForm.password"
|
||||||
|
label="Пароль"
|
||||||
|
prepend-inner-icon="mdi-lock"
|
||||||
|
type="password"
|
||||||
|
variant="outlined"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="mb-3"
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-checkbox v-model="loginForm.rememberMe" label="Запомнить меня" :disabled="isLoading" />
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn @click="handleSubmit" :loading="isLoading" color="primary"> Войти </v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch, nextTick } from 'vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const loginField = ref()
|
||||||
|
const passwordField = ref()
|
||||||
|
|
||||||
|
const loginForm = computed(() => authStore.loginForm)
|
||||||
|
const show = computed({
|
||||||
|
get: () => authStore.showLoginForm,
|
||||||
|
set: (value) => {
|
||||||
|
authStore.showLoginForm = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const isLoading = computed(() => appStore.isLoading)
|
||||||
|
|
||||||
|
// Фокус на поле логина при открытии диалога
|
||||||
|
watch(show, (newShow) => {
|
||||||
|
if (newShow) {
|
||||||
|
nextTick(() => {
|
||||||
|
loginField.value?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const focusPassword = () => {
|
||||||
|
passwordField.value?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const success = await authStore.login()
|
||||||
|
if (success) {
|
||||||
|
// Успешная авторизация
|
||||||
|
} else {
|
||||||
|
// Ошибка авторизации
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
<template>
|
||||||
|
<!-- Мобильное меню - как Android шторка с плитками -->
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-if="isMobile"
|
||||||
|
v-model="menuOpen"
|
||||||
|
temporary
|
||||||
|
location="top"
|
||||||
|
class="mobile-menu-drawer"
|
||||||
|
>
|
||||||
|
<v-container fluid class="pa-4">
|
||||||
|
<v-row dense>
|
||||||
|
<v-col v-for="item in menuItems" :key="item.path" cols="4" class="d-flex justify-center">
|
||||||
|
<v-card
|
||||||
|
class="menu-tile text-center"
|
||||||
|
:color="currentPath === item.path ? 'primary' : 'surface-variant'"
|
||||||
|
elevation="2"
|
||||||
|
@click="navigateTo(item.path)"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<v-card-text class="pa-3">
|
||||||
|
<v-icon
|
||||||
|
size="32"
|
||||||
|
:icon="item.icon"
|
||||||
|
:color="currentPath === item.path ? 'white' : '#757575'"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="text-caption mt-2 font-weight-medium"
|
||||||
|
:class="currentPath === item.path ? 'text-white' : ''"
|
||||||
|
:style="currentPath === item.path ? {} : { color: '#757575' }"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<!-- Десктопное меню - обычный div -->
|
||||||
|
<div
|
||||||
|
v-if="!isMobile"
|
||||||
|
class="desktop-menu"
|
||||||
|
:class="{ collapsed: collapsed }"
|
||||||
|
:style="{ width: collapsed ? '80px' : '250px' }"
|
||||||
|
>
|
||||||
|
<div class="menu-header">
|
||||||
|
<v-btn
|
||||||
|
v-if="!collapsed"
|
||||||
|
icon="mdi-chevron-left"
|
||||||
|
variant="text"
|
||||||
|
@click="toggleCollapsed"
|
||||||
|
class="collapse-btn"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
v-else
|
||||||
|
icon="mdi-chevron-right"
|
||||||
|
variant="text"
|
||||||
|
@click="toggleCollapsed"
|
||||||
|
class="collapse-btn"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-list v-model="selectedItem" class="menu-list">
|
||||||
|
<v-list-item
|
||||||
|
v-for="item in menuItems"
|
||||||
|
:key="item.path"
|
||||||
|
:prepend-icon="collapsed ? item.icon : undefined"
|
||||||
|
:title="collapsed ? '' : item.title"
|
||||||
|
:value="item.path"
|
||||||
|
@click="navigateTo(item.path)"
|
||||||
|
class="menu-item"
|
||||||
|
>
|
||||||
|
<template v-if="!collapsed" #prepend>
|
||||||
|
<v-icon :icon="item.icon" />
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
title: string
|
||||||
|
icon: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = computed(() => [
|
||||||
|
{
|
||||||
|
title: getUserDisplayName(),
|
||||||
|
icon: 'mdi-account',
|
||||||
|
path: '/profile'
|
||||||
|
},
|
||||||
|
{ title: 'Настройки', icon: 'mdi-cog', path: '/settings' },
|
||||||
|
{ title: 'Панель управления', icon: 'mdi-view-dashboard', path: '/dashboard' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const menuOpen = ref(false)
|
||||||
|
const collapsed = ref(false)
|
||||||
|
const isMobile = ref(window.innerWidth < 768)
|
||||||
|
|
||||||
|
const currentPath = computed(() => route.path)
|
||||||
|
const selectedItem = computed(() => currentPath.value)
|
||||||
|
|
||||||
|
const getUserDisplayName = () => {
|
||||||
|
const user = appStore.currentUser
|
||||||
|
if (user?.name && user.name.trim()) {
|
||||||
|
return user.name
|
||||||
|
}
|
||||||
|
return user?.login || 'Профиль'
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
updateCollapsed: [value: boolean]
|
||||||
|
toggleMenu: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const navigateTo = (path: string) => {
|
||||||
|
router.push(path)
|
||||||
|
if (isMobile.value) {
|
||||||
|
menuOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCollapsed = () => {
|
||||||
|
collapsed.value = !collapsed.value
|
||||||
|
emit('updateCollapsed', collapsed.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkMobile = () => {
|
||||||
|
isMobile.value = window.innerWidth < 768
|
||||||
|
if (!isMobile.value) {
|
||||||
|
menuOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', checkMobile)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', checkMobile)
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
emit('toggleMenu')
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
toggle: () => {
|
||||||
|
menuOpen.value = !menuOpen.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mobile-menu-drawer {
|
||||||
|
z-index: 1001;
|
||||||
|
height: auto !important;
|
||||||
|
max-height: 50vh;
|
||||||
|
border-radius: 0 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-menu {
|
||||||
|
background: rgb(var(--v-theme-surface));
|
||||||
|
border-right: 1px solid rgb(var(--v-theme-outline));
|
||||||
|
height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-menu.collapsed {
|
||||||
|
width: 80px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-tile {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-tile:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-tile:active {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-header {
|
||||||
|
padding: 16px 8px 8px 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-btn {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list {
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
margin: 2px 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item :deep(.v-list-item__content) {
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item :deep(.v-list-item-title) {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-menu-drawer {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="currentNotification"
|
||||||
|
class="notification-toast"
|
||||||
|
:style="{ background: getColor(currentNotification.color) }"
|
||||||
|
>
|
||||||
|
{{ currentNotification.message }}
|
||||||
|
<button @click="hide" class="close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import { useNotificationStore } from '@/stores/notification'
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
id: string
|
||||||
|
message: string
|
||||||
|
color: string
|
||||||
|
timeout: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationStore = useNotificationStore()
|
||||||
|
const notifications = computed(() => notificationStore.notifications)
|
||||||
|
const removeNotification = (id: string) => notificationStore.removeNotification(id)
|
||||||
|
|
||||||
|
const currentNotification = ref<Notification | null>(null)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
notifications,
|
||||||
|
(newNotifications) => {
|
||||||
|
if (newNotifications && newNotifications.length > 0) {
|
||||||
|
currentNotification.value = newNotifications[0]
|
||||||
|
|
||||||
|
// Автоматически скрываем через timeout
|
||||||
|
const notif = newNotifications[0]
|
||||||
|
if (notif.timeout > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
hide()
|
||||||
|
}, notif.timeout)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentNotification.value = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
if (currentNotification.value) {
|
||||||
|
removeNotification(currentNotification.value.id)
|
||||||
|
currentNotification.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getColor = (color: string) => {
|
||||||
|
const colors = {
|
||||||
|
success: '#4caf50',
|
||||||
|
error: '#f44336',
|
||||||
|
warning: '#ff9800',
|
||||||
|
info: '#2196f3'
|
||||||
|
}
|
||||||
|
return colors[color] || colors.info
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notification-toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: white;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 9999999;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-width: 300px;
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
margin-left: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%) translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import type { DefineComponent } from 'vue'
|
import type { DefineComponent } from 'vue'
|
||||||
const component: DefineComponent<{}, {}, any>
|
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
|
||||||
export default component
|
export default component
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,44 @@
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
import 'vuetify/styles'
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
import * as components from 'vuetify/components'
|
||||||
|
import * as directives from 'vuetify/directives'
|
||||||
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
|
|
||||||
|
const vuetify = createVuetify({
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'dark',
|
||||||
|
themes: {
|
||||||
|
dark: {
|
||||||
|
colors: {
|
||||||
|
background: '#121212',
|
||||||
|
surface: '#1e1e1e',
|
||||||
|
primary: '#2196F3',
|
||||||
|
'primary-darken-1': '#1976D2',
|
||||||
|
secondary: '#424242',
|
||||||
|
'secondary-darken-1': '#616161',
|
||||||
|
error: '#FF5252',
|
||||||
|
info: '#2196F3',
|
||||||
|
success: '#4CAF50',
|
||||||
|
warning: '#FB8C00'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(vuetify)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
@ -1,18 +1,29 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import HomeView from '../views/HomeView.vue'
|
import ProfileView from '../views/ProfileView.vue'
|
||||||
|
import SettingsView from '../views/SettingsView.vue'
|
||||||
|
import DashboardView from '../views/DashboardView.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'home',
|
redirect: '/profile'
|
||||||
component: HomeView
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: '/profile',
|
||||||
name: 'about',
|
name: 'profile',
|
||||||
component: () => import('../views/AboutView.vue')
|
component: ProfileView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
component: SettingsView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
name: 'dashboard',
|
||||||
|
component: DashboardView
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { RpcClient, UserRole } from '@/generated/RpcClient'
|
||||||
|
import type { User } from '@/generated/RpcClient'
|
||||||
|
import { RpcClientBase } from '@/api/RpcClientBase'
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app', () => {
|
||||||
|
const api = ref<RpcClient | null>(null)
|
||||||
|
const currentUser = ref<User | null>(null)
|
||||||
|
const isAuthenticated = computed(
|
||||||
|
() => currentUser.value?.role !== UserRole.Guest && currentUser.value !== null
|
||||||
|
)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const initializeApi = (baseUrl: string) => {
|
||||||
|
api.value = new RpcClient(baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCurrentUser = (user: User) => {
|
||||||
|
currentUser.value = user
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLoading = (loading: boolean) => {
|
||||||
|
isLoading.value = loading
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
api,
|
||||||
|
currentUser,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
initializeApi,
|
||||||
|
setCurrentUser,
|
||||||
|
setLoading
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { useAppStore } from './app'
|
||||||
|
import { useNotificationStore } from './notification'
|
||||||
|
import { UserRole } from '@/generated/RpcClient'
|
||||||
|
|
||||||
|
interface LoginFormData {
|
||||||
|
login: string
|
||||||
|
password: string
|
||||||
|
rememberMe: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const loginForm = ref<LoginFormData>({
|
||||||
|
login: '',
|
||||||
|
password: '',
|
||||||
|
rememberMe: false
|
||||||
|
})
|
||||||
|
const showLoginForm = ref(false)
|
||||||
|
const notificationStore = useNotificationStore()
|
||||||
|
const showSuccess = notificationStore.showSuccess
|
||||||
|
const showError = notificationStore.showError
|
||||||
|
|
||||||
|
const initializeAuth = async () => {
|
||||||
|
if (!appStore.api) return
|
||||||
|
|
||||||
|
appStore.setLoading(true)
|
||||||
|
try {
|
||||||
|
const user = await appStore.api.Profile.get()
|
||||||
|
appStore.setCurrentUser(user)
|
||||||
|
|
||||||
|
if (user.role === UserRole.Guest) {
|
||||||
|
const authToken = localStorage.getItem('auth-token')
|
||||||
|
if (authToken) {
|
||||||
|
const success = await appStore.api.Auth.authenticateByToken(authToken)
|
||||||
|
if (success) {
|
||||||
|
const updatedUser = await appStore.api.Profile.get()
|
||||||
|
appStore.setCurrentUser(updatedUser)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('auth-token')
|
||||||
|
showLoginForm.value = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showLoginForm.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth initialization error:', error)
|
||||||
|
showLoginForm.value = true
|
||||||
|
} finally {
|
||||||
|
appStore.setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
if (!appStore.api) return false
|
||||||
|
|
||||||
|
appStore.setLoading(true)
|
||||||
|
try {
|
||||||
|
const success = await appStore.api.Auth.authenticateByPassword(
|
||||||
|
loginForm.value.login,
|
||||||
|
loginForm.value.password
|
||||||
|
)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const user = await appStore.api.Profile.get()
|
||||||
|
appStore.setCurrentUser(user)
|
||||||
|
|
||||||
|
if (loginForm.value.rememberMe) {
|
||||||
|
const tokenData = await appStore.api.Auth.generateToken(true)
|
||||||
|
localStorage.setItem('auth-token', tokenData.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess('Авторизация успешна!')
|
||||||
|
resetLoginForm()
|
||||||
|
showLoginForm.value = false
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
showError('Неверный логин или пароль')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
showError('Ошибка авторизации')
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
appStore.setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
if (!appStore.api) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await appStore.api.Auth.logout()
|
||||||
|
localStorage.removeItem('auth-token')
|
||||||
|
appStore.setCurrentUser({ ...appStore.currentUser!, role: UserRole.Guest })
|
||||||
|
showLoginForm.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetLoginForm = () => {
|
||||||
|
loginForm.value = {
|
||||||
|
login: '',
|
||||||
|
password: '',
|
||||||
|
rememberMe: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginForm,
|
||||||
|
showLoginForm,
|
||||||
|
initializeAuth,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
resetLoginForm
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
id: string
|
||||||
|
message: string
|
||||||
|
color: string
|
||||||
|
timeout: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNotificationStore = defineStore('notification', () => {
|
||||||
|
const notifications = ref<Notification[]>([])
|
||||||
|
|
||||||
|
const showNotification = (message: string, color: string = 'info', timeout: number = 3000) => {
|
||||||
|
const id = Date.now().toString()
|
||||||
|
notifications.value.push({ id, message, color, timeout })
|
||||||
|
|
||||||
|
if (timeout > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
removeNotification(id)
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeNotification = (id: string) => {
|
||||||
|
const index = notifications.value.findIndex((n) => n.id === id)
|
||||||
|
if (index > -1) {
|
||||||
|
notifications.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSuccess = (message: string) => showNotification(message, 'success')
|
||||||
|
const showError = (message: string) => showNotification(message, 'error')
|
||||||
|
const showWarning = (message: string) => showNotification(message, 'warning')
|
||||||
|
const showInfo = (message: string) => showNotification(message, 'info')
|
||||||
|
|
||||||
|
return {
|
||||||
|
notifications,
|
||||||
|
showNotification,
|
||||||
|
removeNotification,
|
||||||
|
showSuccess,
|
||||||
|
showError,
|
||||||
|
showWarning,
|
||||||
|
showInfo
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="mobile-full-height">
|
||||||
|
<div class="d-flex justify-center align-center fill-height">
|
||||||
|
<div class="text-center">
|
||||||
|
<v-icon size="64" color="primary" class="mb-4"> mdi-view-dashboard </v-icon>
|
||||||
|
<h1 class="text-h4 mb-2">Панель управления</h1>
|
||||||
|
<p class="text-body-1 text-medium-emphasis">Панель управления и мониторинга</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mobile-full-height {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-full-height {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Панель управления
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,347 @@
|
||||||
|
<template>
|
||||||
|
<div class="profile-container">
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row justify="center">
|
||||||
|
<v-col cols="12" md="8" lg="6">
|
||||||
|
<v-card elevation="2" class="profile-card">
|
||||||
|
<v-card-text>
|
||||||
|
<!-- Форма профиля -->
|
||||||
|
<v-form ref="profileForm" v-model="isFormValid">
|
||||||
|
<!-- Аватар и имя -->
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<v-avatar size="120" color="primary" class="mb-3">
|
||||||
|
<v-icon size="60" color="white">mdi-account</v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
<div class="text-h6">{{ getUserDisplayName() }}</div>
|
||||||
|
<div class="text-body-2 text-medium-emphasis">{{ userRole }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Поля для редактирования -->
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profileData.login"
|
||||||
|
:rules="loginRules"
|
||||||
|
:disabled="!isEditing"
|
||||||
|
label="Логин"
|
||||||
|
prepend-inner-icon="mdi-account"
|
||||||
|
variant="outlined"
|
||||||
|
hint="Только буквы, цифры, точка и дефис"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profileData.name"
|
||||||
|
:rules="nameRules"
|
||||||
|
:disabled="!isEditing"
|
||||||
|
label="Отображаемое имя"
|
||||||
|
prepend-inner-icon="mdi-account-circle"
|
||||||
|
variant="outlined"
|
||||||
|
hint="Имя для отображения в интерфейсе"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Поля для смены пароля (только в режиме редактирования) -->
|
||||||
|
<template v-if="isEditing">
|
||||||
|
<v-divider class="my-4" />
|
||||||
|
|
||||||
|
<div class="text-subtitle-2 mb-3">Смена пароля</div>
|
||||||
|
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profileData.currentPassword"
|
||||||
|
:disabled="!isEditing"
|
||||||
|
label="Текущий пароль"
|
||||||
|
type="password"
|
||||||
|
prepend-inner-icon="mdi-lock"
|
||||||
|
variant="outlined"
|
||||||
|
hint="Введите текущий пароль для смены"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profileData.newPassword"
|
||||||
|
:rules="passwordRules"
|
||||||
|
:disabled="!isEditing"
|
||||||
|
label="Новый пароль"
|
||||||
|
type="password"
|
||||||
|
prepend-inner-icon="mdi-lock-reset"
|
||||||
|
variant="outlined"
|
||||||
|
hint="Минимум 6 символов"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="profileData.confirmPassword"
|
||||||
|
:rules="confirmPasswordRules"
|
||||||
|
:disabled="!isEditing"
|
||||||
|
label="Подтверждение пароля"
|
||||||
|
type="password"
|
||||||
|
prepend-inner-icon="mdi-lock-check"
|
||||||
|
variant="outlined"
|
||||||
|
hint="Повторите новый пароль"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Кнопки действий -->
|
||||||
|
<div class="d-flex justify-end mt-4">
|
||||||
|
<v-btn
|
||||||
|
v-if="!isEditing"
|
||||||
|
@click="startEditing"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-pencil"
|
||||||
|
>
|
||||||
|
Редактировать
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<v-btn @click="cancelEditing" variant="outlined" class="me-2"> Отмена </v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
@click="saveProfile"
|
||||||
|
color="primary"
|
||||||
|
:loading="isSaving"
|
||||||
|
:disabled="!isFormValid"
|
||||||
|
prepend-icon="mdi-content-save"
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Карта с дополнительной информацией -->
|
||||||
|
<v-card elevation="2" class="mt-4">
|
||||||
|
<v-card-text>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon>mdi-fingerprint</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>ID пользователя</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ currentUser?.id }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon>mdi-identifier</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>UUID</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ currentUser?.uuid }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon>mdi-shield-account</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Роль</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ userRole }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useNotificationStore } from '@/stores/notification'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { showSuccess, showError } = useNotificationStore()
|
||||||
|
|
||||||
|
// Данные формы
|
||||||
|
const profileForm = ref()
|
||||||
|
const isFormValid = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const isSaving = ref(false)
|
||||||
|
|
||||||
|
const currentUser = computed(() => appStore.currentUser)
|
||||||
|
const userRole = computed(() => {
|
||||||
|
const role = currentUser.value?.role
|
||||||
|
const roleMap: Record<string, string> = {
|
||||||
|
Admin: 'Администратор',
|
||||||
|
Operator: 'Оператор',
|
||||||
|
User: 'Пользователь',
|
||||||
|
Guest: 'Гость'
|
||||||
|
}
|
||||||
|
return roleMap[role || ''] || role || 'Неизвестно'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Данные профиля для редактирования
|
||||||
|
const profileData = ref({
|
||||||
|
login: '',
|
||||||
|
name: '',
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const originalData = ref({
|
||||||
|
login: '',
|
||||||
|
name: '',
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Валидация логина: только точка, дефис, A-z, 0-9
|
||||||
|
const loginRules = [
|
||||||
|
(v: string) => !!v || 'Логин обязателен',
|
||||||
|
(v: string) =>
|
||||||
|
/^[a-zA-Z0-9.-]+$/.test(v) || 'Логин может содержать только буквы, цифры, точку и дефис',
|
||||||
|
(v: string) => v.length >= 3 || 'Минимальная длина логина - 3 символа',
|
||||||
|
(v: string) => v.length <= 50 || 'Максимальная длина логина - 50 символов'
|
||||||
|
]
|
||||||
|
|
||||||
|
const nameRules = [
|
||||||
|
(v: string) => !!v || 'Имя обязательно',
|
||||||
|
(v: string) => v.length >= 2 || 'Минимальная длина имени - 2 символа',
|
||||||
|
(v: string) => v.length <= 100 || 'Максимальная длина имени - 100 символов'
|
||||||
|
]
|
||||||
|
|
||||||
|
const passwordRules = [
|
||||||
|
(v: string) => !v || v.length >= 6 || 'Минимальная длина пароля - 6 символов'
|
||||||
|
]
|
||||||
|
|
||||||
|
const confirmPasswordRules = [
|
||||||
|
(v: string) => !profileData.newPassword || v === profileData.newPassword || 'Пароли не совпадают'
|
||||||
|
]
|
||||||
|
|
||||||
|
const getUserDisplayName = () => {
|
||||||
|
const user = currentUser.value
|
||||||
|
if (user?.name && user.name.trim()) {
|
||||||
|
return user.name
|
||||||
|
}
|
||||||
|
return user?.login || 'Профиль'
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEditing = () => {
|
||||||
|
originalData.value = {
|
||||||
|
login: profileData.value.login,
|
||||||
|
name: profileData.value.name
|
||||||
|
}
|
||||||
|
isEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEditing = () => {
|
||||||
|
profileData.value = { ...originalData.value }
|
||||||
|
isEditing.value = false
|
||||||
|
profileForm.value?.resetValidation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveProfile = async () => {
|
||||||
|
if (!profileForm.value?.validate() || !appStore.api) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, если хочет сменить пароль - нужен текущий пароль
|
||||||
|
if (profileData.value.newPassword && !profileData.value.currentPassword) {
|
||||||
|
showError('Для смены пароля введите текущий пароль')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Определяем параметр password только если меняем пароль
|
||||||
|
const password = profileData.value.newPassword ? profileData.value.newPassword : undefined
|
||||||
|
|
||||||
|
const success = await appStore.api.Profile.save(
|
||||||
|
profileData.value.login,
|
||||||
|
profileData.value.name,
|
||||||
|
currentUser.value?.values || {},
|
||||||
|
password,
|
||||||
|
profileData.value.currentPassword
|
||||||
|
)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Обновляем данные пользователя в store
|
||||||
|
const updatedUser = await appStore.api.Profile.get()
|
||||||
|
appStore.setCurrentUser(updatedUser)
|
||||||
|
|
||||||
|
showSuccess('Профиль успешно обновлен!')
|
||||||
|
isEditing.value = false
|
||||||
|
|
||||||
|
// Очищаем поля пароля после успешного сохранения
|
||||||
|
profileData.value.currentPassword = ''
|
||||||
|
profileData.value.newPassword = ''
|
||||||
|
profileData.value.confirmPassword = ''
|
||||||
|
originalData.value = { ...profileData.value }
|
||||||
|
} else {
|
||||||
|
showError('Не удалось обновить профиль')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving profile:', error)
|
||||||
|
showError('Ошибка при сохранении профиля')
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadProfileData = () => {
|
||||||
|
const user = currentUser.value
|
||||||
|
if (user) {
|
||||||
|
profileData.value = {
|
||||||
|
login: user.login || '',
|
||||||
|
name: user.name || '',
|
||||||
|
currentPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
}
|
||||||
|
originalData.value = { ...profileData.value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем данные при монтировании
|
||||||
|
onMounted(() => {
|
||||||
|
loadProfileData()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Следим за изменениями currentUser
|
||||||
|
watch(
|
||||||
|
currentUser,
|
||||||
|
() => {
|
||||||
|
loadProfileData()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.profile-container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="mobile-full-height">
|
||||||
|
<div class="d-flex justify-center align-center fill-height">
|
||||||
|
<div class="text-center">
|
||||||
|
<v-icon size="64" color="primary" class="mb-4"> mdi-cog </v-icon>
|
||||||
|
<h1 class="text-h4 mb-2">Настройки</h1>
|
||||||
|
<p class="text-body-1 text-medium-emphasis">Страница настроек приложения</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mobile-full-height {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-full-height {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Настройки приложения
|
||||||
|
</script>
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import { defineConfig } from 'tsbuild'
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
export default defineConfig({
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
extends: '@vue/tsconfig/tsconfig.dom.json',
|
"exclude": ["src/**/__tests__/*"],
|
||||||
include: ['env.d.ts', 'src/**/*', 'src/**/*.vue'],
|
"compilerOptions": {
|
||||||
exclude: ['src/**/__tests__/*'],
|
"composite": true,
|
||||||
compilerOptions: {
|
"baseUrl": ".",
|
||||||
composite: true,
|
"paths": {
|
||||||
baseUrl: '.',
|
"@/*": ["./src/*"]
|
||||||
paths: {
|
|
||||||
'@/*': ['./src/*']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,9 +1,27 @@
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import { defineConfig } from 'vitest/config'
|
import { defineConfig } from 'vitest/config'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
{
|
||||||
|
name: 'generate-rpc-client',
|
||||||
|
buildStart() {
|
||||||
|
console.log('🔄 Generating RPC client before build...')
|
||||||
|
try {
|
||||||
|
execSync('npx tsx scripts/generate-rpc-client.ts', {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: process.cwd()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to generate RPC client:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue