Browse Source

feat: Add user command to manage users

develop
Alex Mikhalev 6 years ago
parent
commit
18c35ac8b9
  1. 4
      common/ErrorCode.ts
  2. 2
      common/tsconfig.json
  3. 20
      server/Database.ts
  4. 21
      server/ManageCommand.ts
  5. 14
      server/UniqueConstraintError.ts
  6. 11
      server/commands/manage.ts
  7. 129
      server/commands/user.ts
  8. 9
      server/entities/User.ts
  9. 1
      server/index.ts
  10. 16
      server/repositories/SprinklersDeviceRepository.ts
  11. 37
      server/repositories/UserRepository.ts
  12. 13
      server/state.ts

4
common/ErrorCode.ts

@ -7,9 +7,10 @@ export enum ErrorCode { @@ -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 { @@ -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:

2
common/tsconfig.json

@ -14,6 +14,6 @@ @@ -14,6 +14,6 @@
}
},
"include": [
"./**/*.ts"
"./**/*.ts",
]
}

20
server/Database.ts

@ -3,7 +3,7 @@ import { Connection, createConnection, getConnectionOptions } from "typeorm"; @@ -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 { @@ -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 { @@ -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 { @@ -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

@ -0,0 +1,21 @@ @@ -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

@ -0,0 +1,14 @@ @@ -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);
}
}

11
server/commands/manage.ts

@ -1,11 +0,0 @@ @@ -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

@ -0,0 +1,129 @@ @@ -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;
}
}
}

9
server/entities/User.ts

@ -5,7 +5,11 @@ import { @@ -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 { @@ -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()

1
server/index.ts

@ -5,5 +5,6 @@ import "./env"; @@ -5,5 +5,6 @@ import "./env";
import "./configureLogger";
export { ServerState } from "./state";
export { Database } from "./Database";
export { createApp } from "./express";
export { WebSocketApi } from "./sprinklersRpc/";

16
server/repositories/SprinklersDeviceRepository.ts

@ -1,6 +1,7 @@ @@ -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> { @@ -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;
}
}
}

37
server/repositories/UserRepository.ts

@ -1,33 +1,50 @@ @@ -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);
// });
// }
}

13
server/state.ts

@ -23,14 +23,21 @@ export class ServerState { @@ -23,14 +23,21 @@ 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;
this.mqttClient.password = await generateSuperuserToken();

Loading…
Cancel
Save