Add Android support with configurable server URL and location permissions

- Add shared_preferences for persisting server URL
- Add SettingsService and PlatformService
- Add server URL input field on non-web platforms
- Make ApiConfig baseUrl configurable at runtime
- Add Android location permissions (ACCESS_FINE/COURSE_LOCATION, INTERNET)
- Request location permission on login and map init
- Fix geo_id type: use String instead of int (UUID format)
- Align share_service with API spec: remove unique_id, use share_id only
- Fix watch endpoint response: last_update instead of created_at
- Add error handling with SnackBars for geo operations
- Wrap login screen in SingleChildScrollView for keyboard handling
- Update map tile layer with userAgentPackageName for OSM
This commit is contained in:
dmit.b
2026-05-15 17:38:56 +03:00
parent ca90c6c3fc
commit f1e88b1ac3
26 changed files with 1255 additions and 297 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

+16
View File
@@ -0,0 +1,16 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
+18
View File
@@ -0,0 +1,18 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="index.css" />
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="swagger-initializer.js" charset="UTF-8"> </script>
</body>
</html>
+79
View File
@@ -0,0 +1,79 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
window.onload = function() {
//<editor-fold desc="Changeable Configuration Block">
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
url: "/swagger/swagger.yaml",
dom_id: '#swagger-ui',
deepLinking: false,
presets: [
SwaggerUIBundle.presets.apis
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "BaseLayout"
});
//</editor-fold>
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+619
View File
@@ -0,0 +1,619 @@
openapi: 3.0.0
info:
title: Mesh backend REST API
version: 1.0.0
description: Описание API
servers:
- url: http://localhost:8888/api
paths:
/info:
get:
tags:
- Common
summary: Запрос статуса сервера
responses:
'200':
description: On success
content:
application/json:
schema:
$ref: '#/components/schemas/BackendInfo'
/blockRule:
get:
tags:
- BlockRule
summary: Запрос правил блокировок
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/BlockPriorityRule'
delete:
tags:
- BlockRule
summary: Удалить правило
requestBody:
description: Удалить правило
content:
application/json:
schema:
$ref: '#/components/schemas/BlockPriorityRule'
required: true
responses:
'200':
description: On success
'400':
description: On error
'404':
description: Не найдено
post:
tags:
- BlockRule
summary: Добавить правило блокировки
requestBody:
description: Правило блокировки (объект json)
content:
application/json:
schema:
$ref: '#/components/schemas/BlockPriorityRule'
required: true
responses:
'200':
description: On success
'400':
description: On error
/priorityRule:
get:
tags:
- PriorityRule
summary: Запрос правил приоритета (Qos)
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/BlockPriorityRule'
delete:
tags:
- PriorityRule
summary: Удалить правила приоритета
requestBody:
description: Удалить правило приоритета (Qos)
content:
application/json:
schema:
$ref: '#/components/schemas/BlockPriorityRule'
required: true
responses:
'200':
description: On success
'400':
description: On error
'404':
description: Не найдено
post:
tags:
- PriorityRule
summary: Добавить правило приоритета (Qos)
requestBody:
description: Правило приоритета (Qos) (объект json)
content:
application/json:
schema:
$ref: '#/components/schemas/BlockPriorityRule'
required: true
responses:
'200':
description: On success
'400':
description: On error
/interface:
get:
tags:
- Interface
summary: Запрос сетевых интерфейсов
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Interface'
put:
tags:
- Interface
summary: Обновить интерфейс
parameters:
- name: interface
in: query
description: Interface object
required: true
schema:
$ref: '#/components/schemas/Interface'
requestBody:
description: Обновить интерфейс
content:
application/json:
schema:
$ref: '#/components/schemas/Interface'
required: true
responses:
'200':
description: On success
'400':
description: On error
/config:
get:
tags:
- Config
summary: Запрос настроек
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Config'
put:
tags:
- Config
summary: Обновить настройки
parameters:
- name: Config
in: query
description: Config object
required: true
schema:
$ref: '#/components/schemas/Config'
requestBody:
description: Обновить Config
content:
application/json:
schema:
$ref: '#/components/schemas/Config'
required: true
responses:
'200':
description: On success
'400':
description: On error
/stats:
get:
tags:
- Common
summary: Запрос статистики сервера
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Stat'
/commandList:
get:
tags:
- CommandList
summary: Запрос списка команд
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Command'
put:
tags:
- CommandList
summary: Перезапись списка команд
requestBody:
description: Перезапись списка команд
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Command'
responses:
'200':
description: On success
/commandRun/{id}:
post:
tags:
- CommandRun
summary: Запустить команду
parameters:
- name: id
in: path
description: ID of command
required: true
schema:
type: string
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CommandResult'
'400':
description: On error
'404':
description: Не найдено
'405':
description: Конфиг нельзя запустить
delete:
tags:
- CommandRun
summary: Остановить команду
parameters:
- name: id
in: path
description: ID of command
required: true
schema:
type: string
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CommandResult'
'400':
description: On error
/command:
post:
tags:
- Command
summary: Добавить команду
parameters:
- name: command
in: query
description: Command object
required: true
schema:
$ref: '#'#/components/schemas/Command'
requestBody:
description: Добавить команду
content:
application/json:
schema:
$ref: '#/components/schemas/Command'
required: true
responses:
'200':
description: Успех
'400':
description: ID занят
'409':
description: Конфиг или скрипт уже существуют
get:
tags:
- Command
summary: Запросить команду с телом
parameters:
- name: id
in: query
description: Command ID
required: true
schema:
type: string
responses:
'200':
description: On success
'400':
description: On error
delete:
tags:
- Command
summary: Удалить команду
parameters:
- name: id
in: query
description: Command ID
required: true
schema:
type: string
responses:
'200':
description: On success
'400':
description: On error
'404':
description: Не найдено
put:
tags:
- Command
summary: Обновить команду
parameters:
- name: command
in: query
description: Command object
required: true
schema:
$ref: '#'#/components/schemas/Command'
requestBody:
description: Обновить команду
content:
application/json:
schema:
$ref: '#/components/schemas/Command'
required: true
responses:
'200':
description: On success
'400':
description: On error
/commandStatuses:
get:
tags:
- CommandStatus
summary: Запрос статусов выполнения команд
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CommandResult'
'400':
description: On error
/graphInfo:
get:
tags:
- NodeInfo
summary: Запрос связей между точками
responses:
'200':
description: On success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/NodeInfo'
'400':
description: On error
/hotspot:
get:
tags:
- Hotspot
summary: Запросить информацию о точке
parameters:
- name: id
in: query
description: Hotspot ID
required: true
schema:
type: string
responses:
'200':
description: On success
'400':
description: On error
components:
schemas:
Interface:
type: object
properties:
name:
type: string
default: eth0
ip:
type: string
default: 192.168.1.1
netmask:
type: string
default: 255.255.255.0
mac:
type: string
default: 4A:30:10:21:10:1A
mtu:
type: integer
default: 0
active:
type: boolean
default: true
loopback:
type: boolean
default: false
readonly:
type: boolean
default: false
Config:
type: object
properties:
secret:
type: string
default: error
channel:
type: integer
default: 0
width:
type: integer
default: 0
power:
type: integer
default: 0
distance:
type: integer
default: 0
Command:
type: object
properties:
id:
type: string
default: error
contentType:
type: string
default: error
path:
type: string
default: error
content:
type: string
default: error
needConfirmation:
type: boolean
default: false
CommandResult:
type: object
properties:
id:
type: string
default: "name unknown"
exitCode:
type: integer
default: 0
output:
type: string
default: "command stdout"
error:
type: string
default: "command stderr"
running:
type: boolean
default: false
Stat:
type: object
properties:
name:
type: string
default: "unknown"
spots:
type: array
items:
type: number
default: 0.0
interval:
type: integer
default: 5
units:
type: string
default: "db"
min:
type: number
default: 0.0
max:
type: number
default: 100.0
HotspotInfo:
type: object
properties:
name:
type: string
default: "unknown"
signal:
type: number
default: 0.0
hops:
type: integer
default: 5
rx:
type: integer
default: 5
tx:
type: integer
default: 5
bytesReceived:
type: integer
default: 5
bytesSent:
type: integer
default: 5
connectionTime:
type: integer
default: 5,
inactiveTime:
type: integer
default: 5,
stats:
type: array
items:
$ref: '#/components/schemas/Stat'
BackendInfo:
type: object
properties:
version:
type: string
default: "unknown"
arch:
type: string
default: "unknown"
uptime:
type: integer
default: 0
hostname:
type: string
default: "unknown"
buildNum:
type: integer
default: 9999
buildType:
type: string
default: "unknown"
buildDate:
type: string
default: "unknown"
pipVersion:
type: string
default: "unknown"
frontVersion:
type: string
default: "unknown"
frontBuild:
type: string
default: "unknown"
NodeInfo:
type: object
properties:
mac:
type: string
default: "unknown"
connections:
type: array
items:
type: string
default: node
BlockPriorityRule:
type: object
properties:
name:
type: string
default: "unknown"
protocol:
type: string
default: "tcp"
port:
type: integer
default: 389
allow:
type: boolean
default: false
direction:
type: boolean
default: true
priority:
type: integer
default: 0