feat: add registration code generation

This commit is contained in:
Fritz Heiden 2025-03-17 15:33:17 +01:00
parent 5ac154b741
commit d869e8d119
8 changed files with 260 additions and 80 deletions

View File

@ -4,7 +4,6 @@ import (
"database/sql"
"fmt"
"path/filepath"
"time"
gonanoid "github.com/matoous/go-nanoid"
_ "github.com/mattn/go-sqlite3"
@ -31,11 +30,10 @@ func (db *DeviceDatabase) Initialize() error {
return fmt.Errorf("error creating devices table: %s", error)
}
_, error = db.Connection.Exec(`CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
device_id INTEGER,
expiry_date TIMESTAMP,
FOREIGN KEY (device_id) REFERENCES devices(id)
_, error = db.Connection.Exec(`CREATE TABLE IF NOT EXISTS integrations (
id TEXT PRIMARY KEY,
name TEXT UNIQUE,
token TEXT
)`)
if error != nil {
return fmt.Errorf("error creating device sessions table: %s", error)
@ -55,18 +53,6 @@ func (db *DeviceDatabase) CreateDevice(device_name string, description string) (
return deviceID, err
}
func (db *DeviceDatabase) CreateSession(deviceID string, expiryDate time.Time) (string, error) {
sessionToken, err := gonanoid.Nanoid(16)
if err != nil {
return "", err
}
_, err = db.Connection.Exec("INSERT INTO sessions (device_id, token, expiry_date) VALUES (?, ?, ?)", deviceID, sessionToken, expiryDate)
if err != nil {
return "", err
}
return sessionToken, nil
}
func (db *DeviceDatabase) GetDeviceById(id string) (*PlaybackDevice, error) {
var device PlaybackDevice
err := db.Connection.QueryRow("SELECT id, device_name, description FROM devices WHERE id = ?", id).Scan(&device.ID, &device.Name, &device.Description)
@ -90,24 +76,6 @@ func (db *DeviceDatabase) UpdateDevice(device *PlaybackDevice) error {
return err
}
func (db *DeviceDatabase) GetSession(sessionToken string) (*DeviceSession, error) {
var session DeviceSession
row := db.Connection.QueryRow("SELECT token, device_id, expiry_date FROM sessions WHERE token = ?", sessionToken)
err := row.Scan(&session.Token, &session.DeviceID, &session.ExpiryDate)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &session, nil
}
func (db *DeviceDatabase) DeleteSessionByToken(token string) error {
_, err := db.Connection.Exec("DELETE FROM sessions WHERE token = ?", token)
return err
}
func (db *DeviceDatabase) GetDevices() (*[]PlaybackDevice, error) {
var devices []PlaybackDevice
@ -136,6 +104,47 @@ func (db *DeviceDatabase) DeleteDevice(ID string) error {
return err
}
func (db *DeviceDatabase) CreateIntegration(name string) (string, string, error) {
id, err := gonanoid.Nanoid(10)
if err != nil {
return "", "", err
}
token, err := gonanoid.Nanoid(16)
if err != nil {
return "", "", err
}
hashed_token, err := hashPassword(token)
if err != nil {
return "", "", err
}
_, err = db.Connection.Exec("INSERT INTO integrations (id, name, token) VALUES (?, ?, ?)", id, name, hashed_token)
if err != nil {
return "", "", err
}
return id, token, nil
}
func (db *DeviceDatabase) GetSession(sessionToken string) (*DeviceSession, error) {
var session DeviceSession
row := db.Connection.QueryRow("SELECT token, device_id, expiry_date FROM sessions WHERE token = ?", sessionToken)
err := row.Scan(&session.Token, &session.DeviceID, &session.ExpiryDate)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &session, nil
}
func (db *DeviceDatabase) DeleteSessionByToken(token string) error {
_, err := db.Connection.Exec("DELETE FROM sessions WHERE token = ?", token)
return err
}
func (db *DeviceDatabase) SetDirectory(directory string) {
db.databaseDirectory = directory
}

View File

@ -2,10 +2,13 @@ package management
import (
d "playback-device-server/data"
gonanoid "github.com/matoous/go-nanoid"
)
type DeviceManager struct {
deviceDatabase *d.DeviceDatabase
deviceDatabase *d.DeviceDatabase
registrationCodes []string
}
func (um *DeviceManager) Initialize(deviceDatabase *d.DeviceDatabase) error {
@ -82,3 +85,9 @@ func (um *DeviceManager) DeleteDevice(ID string) error {
error := um.deviceDatabase.DeleteDevice(ID)
return error
}
func (um *DeviceManager) GetRegistrationCode() (string, error) {
code := gonanoid.MustGenerate("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 6)
um.registrationCodes = append(um.registrationCodes, code)
return code, nil
}

View File

@ -15,14 +15,33 @@ type DeviceApiHandler struct {
func (r *DeviceApiHandler) Initialize(authenticator *Authenticator) {
r.router.Use(authenticator.Authenticate("/api/devices", []string{}))
usersApi := r.router.Group("/api/devices")
devicesApi := r.router.Group("/api/devices")
//usersApi.POST("/register", r.handleRegister)
//usersApi.POST("/deregister", r.handleDeregister)
usersApi.GET("/session/info", r.handleGetSessionInfo)
usersApi.PUT("/:id", r.handleUpdateConfig)
usersApi.GET("", r.handleGetDevices)
usersApi.POST("", r.handleCreateDevice)
usersApi.DELETE("/:id", r.handleDeleteDevice)
devicesApi.GET("/session/info", r.handleGetSessionInfo)
devicesApi.PUT("/:id", r.handleUpdateConfig)
devicesApi.GET("", r.handleGetDevices)
devicesApi.POST("", r.handleCreateDevice)
devicesApi.DELETE("/:id", r.handleDeleteDevice)
integrationsApi := r.router.Group("/api/integrations")
integrationsApi.GET("/register", r.handleIntegrationRegistration)
}
func (r *DeviceApiHandler) handleIntegrationRegistration(context echo.Context) error {
code, error := r.deviceManager.GetRegistrationCode()
if error != nil {
SendError(500, context, fmt.Sprintf("failed to get registration code: %s", error))
return error
}
response := struct {
Code string `json:"code"`
}{
Code: code,
}
return context.JSON(200, response)
}
//func (r DeviceApiHandler) handleRegister(context echo.Context) error {

View File

@ -19,12 +19,26 @@ function ModalHandler() {
function setModalId(modalId) {
_modalId = modalId;
}
function onHidden(callback) {
_ref.addEventListener('hidden.bs.modal', () => {
callback();
});
}
function onShow(callback) {
_ref.addEventListener('show.bs.modal', () => {
callback();
});
}
return {
setRef,
show,
hide,
setModalId,
onHidden,
onShow,
};
}

View File

@ -4,6 +4,7 @@ import CreateUserModal from "./create-user-modal.jsx";
import DeleteUserModal from "./delete-user-modal.jsx";
import UserSettingsModal from "./user-settings-modal.jsx";
import CreateDeviceModal from "./create-device-modal.jsx";
import ShowRegistrationCodeModal from "./show-registration-code-modal.jsx";
const ModalRegistry = (function () {
const modals = [
@ -27,6 +28,11 @@ const ModalRegistry = (function () {
component: CreateDeviceModal,
ref: null,
},
{
id: "showRegistrationCodeModal",
component: ShowRegistrationCodeModal,
ref: null,
},
];
function getModals(props) {

View File

@ -0,0 +1,66 @@
import {
createEffect,
createSignal,
Show,
createResource,
onMount,
} from "solid-js";
import Modal from "./modal.jsx";
import ModalHandler from "./modal-handler.js";
import DeviceService from "../services/device-service.js";
function ShowRegistrationCodeModal(props) {
const [error, setError] = createSignal("");
const [code, { mutate, refetch }] = createResource(async () =>
DeviceService.getRegistrationCode()
);
onMount(() => {
ShowRegistrationCodeModal.Handler.onHidden(() => mutate(null));
ShowRegistrationCodeModal.Handler.onShow(() => refetch());
});
function handleClose() {
ShowRegistrationCodeModal.Handler.hide();
}
return (
<Modal
ref={props.ref}
id="showRegistrationCodeModal"
modalTitle="Register"
centered={true}
>
<div class="modal-body">
<Show when={error() !== ""}>
<div class="alert alert-danger" role="alert">
{error()}
</div>
</Show>
<div class="mb-3 row">
<label for="new_name" class="col-form-label col-sm-4">
Enter this code on the device:
</label>
<div class="col-sm-8">
<input
class="form-control font-monospace"
type="text"
id="new_name"
value={code()}
readonly
/>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" onClick={handleClose} class="btn btn-primary">
OK
</button>
</div>
</Modal>
);
}
ShowRegistrationCodeModal.Handler = new ModalHandler();
export default ShowRegistrationCodeModal;

View File

@ -63,11 +63,27 @@ function DeviceService() {
}
}
async function getRegistrationCode() {
let response = await Net.sendRequest({
method: "GET",
url: "/api/integrations/register",
});
if (response.status !== 200) {
let responseData = JSON.parse(response.data);
throw new Error(responseData.error);
}
let codeObject = JSON.parse(response.data);
return codeObject.code;
}
return {
getDevices,
createDevice,
updateDevice,
deleteDevice,
getRegistrationCode,
};
}

View File

@ -1,8 +1,9 @@
import { createSignal, onMount } from "solid-js";
import { createSignal, onMount, Show } from "solid-js";
import List from "../modules/list";
import CreateDeviceModal from "../modals/create-device-modal";
import DeviceService from "../services/device-service";
import ShowRegistrationCodeModal from "../modals/show-registration-code-modal";
function DevicesView(props) {
const DEVICES_LIST_TYPE = "devices";
@ -27,6 +28,10 @@ function DevicesView(props) {
let devices = await DeviceService.getDevices();
setDevices(devices);
}
function handleRegisterIntegration() {
ShowRegistrationCodeModal.Handler.show();
}
return (
<div
@ -63,44 +68,80 @@ function DevicesView(props) {
</button>
</div>
<div class="d-flex flex-row justify-content-end flex-fill">
<button class="btn btn-dark me-2 mb-3" onClick={handleNewDevice}>
<i class="bi bi-plus-square me-2"></i>
New Device
</button>
<Show when={listType() === DEVICES_LIST_TYPE}>
<button class="btn btn-dark me-2 mb-3" onClick={handleNewDevice}>
<i class="bi bi-plus-square me-2"></i>
New Device
</button>
</Show>
<Show when={listType() === INTEGRATION_LIST_TYPE}>
<button class="btn btn-dark me-2 mb-3" onClick={handleRegisterIntegration}>
<i class="bi bi-plus-square me-2"></i>
Register
</button>
</Show>
</div>
</div>
<List
onListItemClick={() => {}}
items={devices().map((device) => ({
id: {
html: <span class="font-monospace">{device.getId()}</span>,
},
name: {
text: device.getName(),
},
description: {
text: device.getDescription(),
},
device,
}))}
class={"flex-fill"}
columns={[
{
id: "id",
name: "id",
width: 6,
},
{
id: "name",
name: "Name",
width: 10,
},
{
id: "description",
name: "Description",
},
]}
></List>
<Show when={listType() === DEVICES_LIST_TYPE}>
<List
onListItemClick={() => {}}
items={devices().map((device) => ({
id: {
html: <span class="font-monospace">{device.getId()}</span>,
},
name: {
text: device.getName(),
},
description: {
text: device.getDescription(),
},
device,
}))}
class={"flex-fill"}
columns={[
{
id: "id",
name: "id",
width: 6,
},
{
id: "name",
name: "Name",
width: 10,
},
{
id: "description",
name: "Description",
},
]}
></List>
</Show>
<Show when={listType() === INTEGRATION_LIST_TYPE}>
<List
onListItemClick={() => {}}
items={[].map((integration) => ({
id: {
html: <span class="font-monospace">{integration.getId()}</span>,
},
name: {
text: integration.getName(),
},
integration: integration,
}))}
class={"flex-fill"}
columns={[
{
id: "id",
name: "id",
width: 6,
},
{
id: "name",
name: "Name",
},
]}
></List>
</Show>
</div>
);
}