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,
|
||||
Unauthorized = 106,
|
||||
NoPermission = 107,
|
||||
NotImplemented = 108,
|
||||
NotFound = 109,
|
||||
NotUnique = 110,
|
||||
Internal = 200,
|
||||
NotImplemented = 201,
|
||||
Timeout = 300,
|
||||
ServerDisconnected = 301,
|
||||
BrokerDisconnected = 302
|
||||
@ -22,6 +23,7 @@ export function toHttpStatus(errorCode: ErrorCode): number {
|
||||
case ErrorCode.Parse:
|
||||
case ErrorCode.Range:
|
||||
case ErrorCode.InvalidData:
|
||||
case ErrorCode.NotUnique:
|
||||
return 400; // Bad request
|
||||
case ErrorCode.Unauthorized:
|
||||
case ErrorCode.BadToken:
|
||||
|
@ -14,6 +14,6 @@
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
"./**/*.ts",
|
||||
]
|
||||
}
|
@ -3,7 +3,7 @@ import { Connection, createConnection, getConnectionOptions } from "typeorm";
|
||||
|
||||
import logger from "@common/logger";
|
||||
|
||||
import { User } from "./entities";
|
||||
import { SprinklersDevice, User } from "./entities";
|
||||
import { SprinklersDeviceRepository, UserRepository } from "./repositories/";
|
||||
|
||||
export class Database {
|
||||
@ -24,16 +24,21 @@ export class Database {
|
||||
Object.assign(options, {
|
||||
entities: [path.resolve(__dirname, "entities", "*.js")]
|
||||
});
|
||||
if (options.synchronize) {
|
||||
logger.warn("synchronizing database schema");
|
||||
}
|
||||
this._conn = await createConnection(options);
|
||||
this.users = this._conn.getCustomRepository(UserRepository);
|
||||
this.sprinklersDevices = this._conn.getCustomRepository(
|
||||
SprinklersDeviceRepository
|
||||
);
|
||||
logger.info("connected to database");
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
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 users: User[] = [];
|
||||
for (let i = 0; i < NUM; i++) {
|
||||
const username = "alex" + i;
|
||||
const username = "alex" + i % 50;
|
||||
let user = await this.users.findByUsername(username);
|
||||
if (!user) {
|
||||
// if (!user) {
|
||||
user = await this.users.create({
|
||||
name: "Alex Mikhalev" + i,
|
||||
username
|
||||
});
|
||||
}
|
||||
// }
|
||||
await user.setPassword("kakashka" + i);
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
const devices: SprinklersDevice[] = [];
|
||||
|
||||
for (let i = 0; i < NUM; i++) {
|
||||
const name = "Test" + i;
|
||||
let device = await this.sprinklersDevices.findByName(name);
|
||||
@ -63,13 +70,14 @@ export class Database {
|
||||
name,
|
||||
deviceId: "grinklers" + (i === 1 ? "" : i)
|
||||
});
|
||||
await this.sprinklersDevices.save(device);
|
||||
devices.push(device);
|
||||
for (let j = 0; j < 5; j++) {
|
||||
const userIdx = (i + j * 10) % NUM;
|
||||
const user = users[userIdx];
|
||||
user.devices = (user.devices || []).concat([device]);
|
||||
}
|
||||
}
|
||||
await this.sprinklersDevices.save(devices);
|
||||
logger.info("inserted/updated devices");
|
||||
|
||||
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,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
PrimaryGeneratedColumn
|
||||
PrimaryGeneratedColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
Index,
|
||||
InsertEvent
|
||||
} from "typeorm";
|
||||
|
||||
import { IUser } from "@common/httpApi";
|
||||
@ -18,7 +22,8 @@ export class User implements IUser {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ unique: true })
|
||||
@Column()
|
||||
@Index("user_username_unique", { unique: true })
|
||||
username: string = "";
|
||||
|
||||
@Column()
|
||||
|
@ -5,5 +5,6 @@ import "./env";
|
||||
import "./configureLogger";
|
||||
|
||||
export { ServerState } from "./state";
|
||||
export { Database } from "./Database";
|
||||
export { createApp } from "./express";
|
||||
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 UniqueConstraintError from "@server/UniqueConstraintError";
|
||||
|
||||
@EntityRepository(SprinklersDevice)
|
||||
export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
||||
@ -39,4 +40,17 @@ export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
|
||||
}
|
||||
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";
|
||||
|
||||
export interface FindUserOptions {
|
||||
export interface FindUserOptions extends FindOneOptions<User> {
|
||||
devices: boolean;
|
||||
}
|
||||
|
||||
function applyDefaultOptions(
|
||||
function computeOptions(
|
||||
options?: Partial<FindUserOptions>
|
||||
): FindOneOptions<User> {
|
||||
const opts: FindUserOptions = { devices: false, ...options };
|
||||
const relations = [opts.devices && "devices"].filter(Boolean) as string[];
|
||||
return { relations };
|
||||
const { devices, ...rest } = opts;
|
||||
const relations = [devices && "devices"].filter(Boolean) as string[];
|
||||
return { relations, ...rest };
|
||||
}
|
||||
|
||||
@EntityRepository(User)
|
||||
export class UserRepository extends Repository<User> {
|
||||
findAll(options?: Partial<FindUserOptions>) {
|
||||
const opts = applyDefaultOptions(options);
|
||||
const opts = computeOptions(options);
|
||||
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>) {
|
||||
const opts = applyDefaultOptions(options);
|
||||
return super.findOne(id, opts);
|
||||
return this.findOneUser({ id }, options);
|
||||
}
|
||||
|
||||
findByUsername(username: string, options?: Partial<FindUserOptions>) {
|
||||
const opts = applyDefaultOptions(options);
|
||||
return this.findOne({ username }, opts);
|
||||
return this.findOneUser({ username }, options);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
await this.database.connect();
|
||||
logger.info("connected to database");
|
||||
|
||||
if (process.env.INSERT_TEST_DATA) {
|
||||
await this.database.insertTestData();
|
||||
logger.info("inserted test data");
|
||||
try {
|
||||
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() {
|
||||
this.mqttClient.username = SUPERUSER;
|
||||
|
Loading…
x
Reference in New Issue
Block a user