feat: Add user command to manage users
This commit is contained in:
parent
8756180ad1
commit
18c35ac8b9
@ -7,9 +7,10 @@ export enum ErrorCode {
|
|||||||
BadToken = 105,
|
BadToken = 105,
|
||||||
Unauthorized = 106,
|
Unauthorized = 106,
|
||||||
NoPermission = 107,
|
NoPermission = 107,
|
||||||
NotImplemented = 108,
|
|
||||||
NotFound = 109,
|
NotFound = 109,
|
||||||
|
NotUnique = 110,
|
||||||
Internal = 200,
|
Internal = 200,
|
||||||
|
NotImplemented = 201,
|
||||||
Timeout = 300,
|
Timeout = 300,
|
||||||
ServerDisconnected = 301,
|
ServerDisconnected = 301,
|
||||||
BrokerDisconnected = 302
|
BrokerDisconnected = 302
|
||||||
@ -22,6 +23,7 @@ export function toHttpStatus(errorCode: ErrorCode): number {
|
|||||||
case ErrorCode.Parse:
|
case ErrorCode.Parse:
|
||||||
case ErrorCode.Range:
|
case ErrorCode.Range:
|
||||||
case ErrorCode.InvalidData:
|
case ErrorCode.InvalidData:
|
||||||
|
case ErrorCode.NotUnique:
|
||||||
return 400; // Bad request
|
return 400; // Bad request
|
||||||
case ErrorCode.Unauthorized:
|
case ErrorCode.Unauthorized:
|
||||||
case ErrorCode.BadToken:
|
case ErrorCode.BadToken:
|
||||||
|
@ -14,6 +14,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*.ts"
|
"./**/*.ts",
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ import { Connection, createConnection, getConnectionOptions } from "typeorm";
|
|||||||
|
|
||||||
import logger from "@common/logger";
|
import logger from "@common/logger";
|
||||||
|
|
||||||
import { User } from "./entities";
|
import { SprinklersDevice, User } from "./entities";
|
||||||
import { SprinklersDeviceRepository, UserRepository } from "./repositories/";
|
import { SprinklersDeviceRepository, UserRepository } from "./repositories/";
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
@ -24,16 +24,21 @@ export class Database {
|
|||||||
Object.assign(options, {
|
Object.assign(options, {
|
||||||
entities: [path.resolve(__dirname, "entities", "*.js")]
|
entities: [path.resolve(__dirname, "entities", "*.js")]
|
||||||
});
|
});
|
||||||
|
if (options.synchronize) {
|
||||||
|
logger.warn("synchronizing database schema");
|
||||||
|
}
|
||||||
this._conn = await createConnection(options);
|
this._conn = await createConnection(options);
|
||||||
this.users = this._conn.getCustomRepository(UserRepository);
|
this.users = this._conn.getCustomRepository(UserRepository);
|
||||||
this.sprinklersDevices = this._conn.getCustomRepository(
|
this.sprinklersDevices = this._conn.getCustomRepository(
|
||||||
SprinklersDeviceRepository
|
SprinklersDeviceRepository
|
||||||
);
|
);
|
||||||
|
logger.info("connected to database");
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
if (this._conn) {
|
if (this._conn) {
|
||||||
return this._conn.close();
|
await this._conn.close();
|
||||||
|
logger.info("disconnected from database");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,18 +46,20 @@ export class Database {
|
|||||||
const NUM = 100;
|
const NUM = 100;
|
||||||
const users: User[] = [];
|
const users: User[] = [];
|
||||||
for (let i = 0; i < NUM; i++) {
|
for (let i = 0; i < NUM; i++) {
|
||||||
const username = "alex" + i;
|
const username = "alex" + i % 50;
|
||||||
let user = await this.users.findByUsername(username);
|
let user = await this.users.findByUsername(username);
|
||||||
if (!user) {
|
// if (!user) {
|
||||||
user = await this.users.create({
|
user = await this.users.create({
|
||||||
name: "Alex Mikhalev" + i,
|
name: "Alex Mikhalev" + i,
|
||||||
username
|
username
|
||||||
});
|
});
|
||||||
}
|
// }
|
||||||
await user.setPassword("kakashka" + i);
|
await user.setPassword("kakashka" + i);
|
||||||
users.push(user);
|
users.push(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const devices: SprinklersDevice[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < NUM; i++) {
|
for (let i = 0; i < NUM; i++) {
|
||||||
const name = "Test" + i;
|
const name = "Test" + i;
|
||||||
let device = await this.sprinklersDevices.findByName(name);
|
let device = await this.sprinklersDevices.findByName(name);
|
||||||
@ -63,13 +70,14 @@ export class Database {
|
|||||||
name,
|
name,
|
||||||
deviceId: "grinklers" + (i === 1 ? "" : i)
|
deviceId: "grinklers" + (i === 1 ? "" : i)
|
||||||
});
|
});
|
||||||
await this.sprinklersDevices.save(device);
|
devices.push(device);
|
||||||
for (let j = 0; j < 5; j++) {
|
for (let j = 0; j < 5; j++) {
|
||||||
const userIdx = (i + j * 10) % NUM;
|
const userIdx = (i + j * 10) % NUM;
|
||||||
const user = users[userIdx];
|
const user = users[userIdx];
|
||||||
user.devices = (user.devices || []).concat([device]);
|
user.devices = (user.devices || []).concat([device]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await this.sprinklersDevices.save(devices);
|
||||||
logger.info("inserted/updated devices");
|
logger.info("inserted/updated devices");
|
||||||
|
|
||||||
await this.users.save(users);
|
await this.users.save(users);
|
||||||
|
21
server/ManageCommand.ts
Normal file
21
server/ManageCommand.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Command from "@oclif/command";
|
||||||
|
|
||||||
|
import { Database, ServerState } from ".";
|
||||||
|
|
||||||
|
export default abstract class ManageCommand extends Command {
|
||||||
|
state!: ServerState;
|
||||||
|
database!: Database;
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
this.state = new ServerState();
|
||||||
|
await this.state.startDatabase();
|
||||||
|
this.database = this.state.database;
|
||||||
|
}
|
||||||
|
|
||||||
|
async finally(e: Error | undefined) {
|
||||||
|
if (this.state) {
|
||||||
|
await this.state.stopDatabase();
|
||||||
|
}
|
||||||
|
await super.finally(e);
|
||||||
|
}
|
||||||
|
}
|
14
server/UniqueConstraintError.ts
Normal file
14
server/UniqueConstraintError.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { QueryFailedError } from "typeorm";
|
||||||
|
|
||||||
|
import ApiError from "@common/ApiError";
|
||||||
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
|
|
||||||
|
export default class UniqueConstraintError extends ApiError {
|
||||||
|
static is(err: any): err is QueryFailedError {
|
||||||
|
return err && err.name === "QueryFailedError" && (err as any).code === 23505; // unique constraint error
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(err: QueryFailedError) {
|
||||||
|
super(`Unsatisfied unique constraint: ${(err as any).detail}`, ErrorCode.NotUnique, err);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +0,0 @@
|
|||||||
import Command from "@oclif/command";
|
|
||||||
|
|
||||||
import { createApp, ServerState, WebSocketApi } from "../";
|
|
||||||
|
|
||||||
import log from "@common/logger";
|
|
||||||
|
|
||||||
export default class ManageCommand extends Command {
|
|
||||||
run(): Promise<any> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
}
|
|
129
server/commands/user.ts
Normal file
129
server/commands/user.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { flags } from "@oclif/command";
|
||||||
|
import { ux } from "cli-ux";
|
||||||
|
import { capitalize } from "lodash";
|
||||||
|
import { FindConditions } from "typeorm";
|
||||||
|
|
||||||
|
import ManageCommand from "../ManageCommand";
|
||||||
|
|
||||||
|
import { Input } from "@oclif/parser/lib/flags";
|
||||||
|
import { User } from "@server/entities";
|
||||||
|
|
||||||
|
type UserFlags = (typeof UserCommand)["flags"] extends Input<infer F>
|
||||||
|
? F
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type Action = "create" | "update" | "delete";
|
||||||
|
|
||||||
|
export default class UserCommand extends ManageCommand {
|
||||||
|
static description = "Manage users";
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
create: flags.boolean({
|
||||||
|
char: "c",
|
||||||
|
exclusive: ["update", "delete", "id"],
|
||||||
|
dependsOn: ["username"],
|
||||||
|
description: "Create a new user"
|
||||||
|
}),
|
||||||
|
update: flags.boolean({
|
||||||
|
char: "u",
|
||||||
|
exclusive: ["create", "delete"],
|
||||||
|
description: "Update an existing user (by --id or --username)"
|
||||||
|
}),
|
||||||
|
delete: flags.boolean({
|
||||||
|
char: "d",
|
||||||
|
exclusive: ["create", "update"],
|
||||||
|
description: "Delete a user (by --id or --username)"
|
||||||
|
}),
|
||||||
|
id: flags.integer({
|
||||||
|
description: "The id of the user to update or delete",
|
||||||
|
}),
|
||||||
|
username: flags.string({
|
||||||
|
description: "The username of the user to create or update"
|
||||||
|
}),
|
||||||
|
name: flags.string({
|
||||||
|
description: "The name of the user, when creating or updating"
|
||||||
|
}),
|
||||||
|
passwordPrompt: flags.boolean({
|
||||||
|
char: "p",
|
||||||
|
description:
|
||||||
|
"Prompts for the password of the user when creating or updating"
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
getAction(flags: UserFlags): Action {
|
||||||
|
if (flags.create) return "create";
|
||||||
|
else if (flags.update) return "update";
|
||||||
|
else if (flags.delete) return "delete";
|
||||||
|
else {
|
||||||
|
this.error("Must specify an action (--create, --update, --delete)", {
|
||||||
|
exit: false
|
||||||
|
});
|
||||||
|
return this._help();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFindConditions(flags: UserFlags, action: Action): FindConditions<User> {
|
||||||
|
if (flags.id != null) {
|
||||||
|
return { id: flags.id };
|
||||||
|
} else if (flags.username) {
|
||||||
|
return { username: flags.username };
|
||||||
|
} else {
|
||||||
|
this.error(`Must specify either --id or --username to ${action}`, {
|
||||||
|
exit: false
|
||||||
|
});
|
||||||
|
return this._help();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrDeleteUser(flags: UserFlags, action: Action): Promise<User | never> {
|
||||||
|
if (action === "create") {
|
||||||
|
return this.database.users.create();
|
||||||
|
} else {
|
||||||
|
const findConditions = this.getFindConditions(flags, action);
|
||||||
|
if (action === "delete") {
|
||||||
|
this.log("findConditions: ", findConditions)
|
||||||
|
const result = await this.database.users.delete(findConditions);
|
||||||
|
this.log(`Deleted user: `, result);
|
||||||
|
return this.exit();
|
||||||
|
} else {
|
||||||
|
const user = await this.database.users.findOneUser(findConditions);
|
||||||
|
if (!user) {
|
||||||
|
return this.error(`The specified user does not exist`);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const parseResult = this.parse(UserCommand);
|
||||||
|
|
||||||
|
const flags = parseResult.flags;
|
||||||
|
const action = this.getAction(flags);
|
||||||
|
|
||||||
|
await this.connect();
|
||||||
|
|
||||||
|
const user = await this.getOrDeleteUser(flags, action);
|
||||||
|
|
||||||
|
if (flags.id != null && flags.username) {
|
||||||
|
user.username = flags.username;
|
||||||
|
}
|
||||||
|
if (flags.name) {
|
||||||
|
user.name = flags.name;
|
||||||
|
}
|
||||||
|
if (flags.passwordPrompt || flags.create) {
|
||||||
|
const password = await ux.prompt("Enter a password to assign the user", {
|
||||||
|
type: "hide"
|
||||||
|
});
|
||||||
|
await user.setPassword(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.database.users.save(user);
|
||||||
|
this.log(`${capitalize(action)}d user id ${user.id} (${user.username})`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,11 @@ import {
|
|||||||
Entity,
|
Entity,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
PrimaryGeneratedColumn
|
PrimaryGeneratedColumn,
|
||||||
|
BeforeInsert,
|
||||||
|
BeforeUpdate,
|
||||||
|
Index,
|
||||||
|
InsertEvent
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
|
|
||||||
import { IUser } from "@common/httpApi";
|
import { IUser } from "@common/httpApi";
|
||||||
@ -18,7 +22,8 @@ export class User implements IUser {
|
|||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ unique: true })
|
@Column()
|
||||||
|
@Index("user_username_unique", { unique: true })
|
||||||
username: string = "";
|
username: string = "";
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
|
@ -5,5 +5,6 @@ import "./env";
|
|||||||
import "./configureLogger";
|
import "./configureLogger";
|
||||||
|
|
||||||
export { ServerState } from "./state";
|
export { ServerState } from "./state";
|
||||||
|
export { Database } from "./Database";
|
||||||
export { createApp } from "./express";
|
export { createApp } from "./express";
|
||||||
export { WebSocketApi } from "./sprinklersRpc/";
|
export { WebSocketApi } from "./sprinklersRpc/";
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { EntityRepository, Repository } from "typeorm";
|
import { DeepPartial, EntityRepository, Repository, SaveOptions } from "typeorm";
|
||||||
|
|
||||||
import { SprinklersDevice, User } from "@server/entities";
|
import { SprinklersDevice, User } from "@server/entities";
|
||||||
|
import UniqueConstraintError from "@server/UniqueConstraintError";
|
||||||
|
|
||||||
@EntityRepository(SprinklersDevice)
|
@EntityRepository(SprinklersDevice)
|
||||||
export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
||||||
@ -39,4 +40,17 @@ export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
|||||||
}
|
}
|
||||||
return user.devices![0];
|
return user.devices![0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
save<T extends DeepPartial<SprinklersDevice>>(entities: T[], options?: SaveOptions): Promise<T[]>;
|
||||||
|
save<T extends DeepPartial<SprinklersDevice>>(entity: T, options?: SaveOptions): Promise<T>;
|
||||||
|
async save(entity: any, options?: SaveOptions): Promise<any> {
|
||||||
|
try {
|
||||||
|
return await super.save(entity, options);
|
||||||
|
} catch (e) {
|
||||||
|
if (UniqueConstraintError.is(e)) {
|
||||||
|
throw new UniqueConstraintError(e);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,50 @@
|
|||||||
import { EntityRepository, FindOneOptions, Repository } from "typeorm";
|
import { EntityRepository, FindOneOptions, Repository, DeepPartial, FindConditions, SaveOptions } from "typeorm";
|
||||||
|
|
||||||
|
import ApiError from "@common/ApiError";
|
||||||
import { User } from "@server/entities";
|
import { User } from "@server/entities";
|
||||||
|
|
||||||
export interface FindUserOptions {
|
export interface FindUserOptions extends FindOneOptions<User> {
|
||||||
devices: boolean;
|
devices: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyDefaultOptions(
|
function computeOptions(
|
||||||
options?: Partial<FindUserOptions>
|
options?: Partial<FindUserOptions>
|
||||||
): FindOneOptions<User> {
|
): FindOneOptions<User> {
|
||||||
const opts: FindUserOptions = { devices: false, ...options };
|
const opts: FindUserOptions = { devices: false, ...options };
|
||||||
const relations = [opts.devices && "devices"].filter(Boolean) as string[];
|
const { devices, ...rest } = opts;
|
||||||
return { relations };
|
const relations = [devices && "devices"].filter(Boolean) as string[];
|
||||||
|
return { relations, ...rest };
|
||||||
}
|
}
|
||||||
|
|
||||||
@EntityRepository(User)
|
@EntityRepository(User)
|
||||||
export class UserRepository extends Repository<User> {
|
export class UserRepository extends Repository<User> {
|
||||||
findAll(options?: Partial<FindUserOptions>) {
|
findAll(options?: Partial<FindUserOptions>) {
|
||||||
const opts = applyDefaultOptions(options);
|
const opts = computeOptions(options);
|
||||||
return super.find(opts);
|
return super.find(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findOneUser(conditions: FindConditions<User>, options?: Partial<FindUserOptions>) {
|
||||||
|
const opts = computeOptions(options);
|
||||||
|
return super.findOne(conditions, opts);
|
||||||
|
}
|
||||||
|
|
||||||
findById(id: number, options?: Partial<FindUserOptions>) {
|
findById(id: number, options?: Partial<FindUserOptions>) {
|
||||||
const opts = applyDefaultOptions(options);
|
return this.findOneUser({ id }, options);
|
||||||
return super.findOne(id, opts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findByUsername(username: string, options?: Partial<FindUserOptions>) {
|
findByUsername(username: string, options?: Partial<FindUserOptions>) {
|
||||||
const opts = applyDefaultOptions(options);
|
return this.findOneUser({ username }, options);
|
||||||
return this.findOne({ username }, opts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// async checkAndSave(entity: User): Promise<User> {
|
||||||
|
// return this.manager.transaction(manager => {
|
||||||
|
// let query = manager.createQueryBuilder<User>(User, "user", manager.queryRunner!);
|
||||||
|
// if (entity.id != null) {
|
||||||
|
// query = query.where("user.id <> :id", { id: entity.id })
|
||||||
|
// }
|
||||||
|
// query
|
||||||
|
|
||||||
|
// return manager.save(entity);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
@ -23,13 +23,20 @@ export class ServerState {
|
|||||||
|
|
||||||
async startDatabase() {
|
async startDatabase() {
|
||||||
await this.database.connect();
|
await this.database.connect();
|
||||||
logger.info("connected to database");
|
|
||||||
|
|
||||||
if (process.env.INSERT_TEST_DATA) {
|
if (process.env.INSERT_TEST_DATA) {
|
||||||
await this.database.insertTestData();
|
try {
|
||||||
logger.info("inserted test data");
|
await this.database.insertTestData();
|
||||||
|
logger.info("inserted test data");
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e, "error inserting test data");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async stopDatabase() {
|
||||||
|
await this.database.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
async startMqtt() {
|
async startMqtt() {
|
||||||
this.mqttClient.username = SUPERUSER;
|
this.mqttClient.username = SUPERUSER;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user