Various little improvements. More work on permissions

This commit is contained in:
simon987 2019-02-14 22:04:00 -05:00
parent c3e5bd77f7
commit 8fe41b8fbb
31 changed files with 434 additions and 96 deletions

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/simon987/task_tracker/storage" "github.com/simon987/task_tracker/storage"
"strconv"
) )
const MinPasswordLength = 8 const MinPasswordLength = 8
@ -31,6 +32,12 @@ type AccountDetails struct {
Manager *storage.Manager `json:"manager,omitempty"` Manager *storage.Manager `json:"manager,omitempty"`
} }
type GetAllManagersResponse struct {
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
Managers *[]storage.Manager `json:"managers"`
}
func (r *RegisterRequest) isValid() bool { func (r *RegisterRequest) isValid() bool {
return MinUsernameLength <= len(r.Username) && return MinUsernameLength <= len(r.Username) &&
len(r.Username) <= MaxUsernameLength && len(r.Username) <= MaxUsernameLength &&
@ -158,3 +165,100 @@ func (api *WebAPI) AccountDetails(r *Request) {
}) })
} }
} }
func (api *WebAPI) GetAllManagers(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
if manager == nil {
r.Json(GetAllManagersResponse{
Ok: false,
Message: "Unauthorized",
}, 401)
return
}
managers := api.Database.GetAllManagers()
r.OkJson(GetAllManagersResponse{
Ok: true,
Managers: managers,
})
}
func (api *WebAPI) PromoteManager(r *Request) {
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
if err != nil || id <= 0 {
r.Json(CreateProjectResponse{
Ok: false,
Message: "Invalid manager id",
}, 400)
return
}
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
if !manager.(*storage.Manager).WebsiteAdmin || manager.(*storage.Manager).Id == id {
r.Json(GetAllManagersResponse{
Ok: false,
Message: "Unauthorized",
}, 401)
return
}
if !manager.(*storage.Manager).WebsiteAdmin {
r.Json(GetAllManagersResponse{
Ok: false,
Message: "Unauthorized",
}, 403)
return
}
api.Database.UpdateManager(&storage.Manager{
Id: id,
WebsiteAdmin: true,
})
r.Ctx.Response.SetStatusCode(204)
}
func (api *WebAPI) DemoteManager(r *Request) {
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
if err != nil || id <= 0 {
r.Json(CreateProjectResponse{
Ok: false,
Message: "Invalid manager id",
}, 400)
return
}
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
if manager == nil {
r.Json(GetAllManagersResponse{
Ok: false,
Message: "Unauthorized",
}, 401)
return
}
if !manager.(*storage.Manager).WebsiteAdmin || manager.(*storage.Manager).Id == id {
r.Json(GetAllManagersResponse{
Ok: false,
Message: "Unauthorized",
}, 403)
return
}
api.Database.UpdateManager(&storage.Manager{
Id: id,
WebsiteAdmin: false,
})
r.Ctx.Response.SetStatusCode(204)
}

View File

@ -106,6 +106,9 @@ func New() *WebAPI {
api.router.POST("/login", LogRequestMiddleware(api.Login)) api.router.POST("/login", LogRequestMiddleware(api.Login))
api.router.GET("/logout", LogRequestMiddleware(api.Logout)) api.router.GET("/logout", LogRequestMiddleware(api.Logout))
api.router.GET("/account", LogRequestMiddleware(api.AccountDetails)) api.router.GET("/account", LogRequestMiddleware(api.AccountDetails))
api.router.GET("/manager/list", LogRequestMiddleware(api.GetAllManagers))
api.router.GET("/manager/promote/:id", LogRequestMiddleware(api.PromoteManager))
api.router.GET("/manager/demote/:id", LogRequestMiddleware(api.DemoteManager))
api.router.NotFound = func(ctx *fasthttp.RequestCtx) { api.router.NotFound = func(ctx *fasthttp.RequestCtx) {

View File

@ -3,15 +3,14 @@ package main
import ( import (
"github.com/simon987/task_tracker/api" "github.com/simon987/task_tracker/api"
"github.com/simon987/task_tracker/config" "github.com/simon987/task_tracker/config"
"github.com/simon987/task_tracker/storage"
"math/rand" "math/rand"
"time" "time"
) )
func tmpDebugSetup() { func tmpDebugSetup() {
db := storage.Database{} //db := storage.Database{}
db.Reset() //db.Reset()
} }

View File

@ -69,7 +69,7 @@ CREATE TABLE manager
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
register_time INTEGER NOT NULL, register_time INTEGER NOT NULL,
website_admin BOOLEAN NOT NULL, tracker_admin BOOLEAN NOT NULL,
username TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL,
password BYTEA NOT NULL password BYTEA NOT NULL
); );
@ -117,7 +117,7 @@ CREATE OR REPLACE FUNCTION on_manager_insert() RETURNS TRIGGER AS
$$ $$
BEGIN BEGIN
IF NEW.id = 1 THEN IF NEW.id = 1 THEN
UPDATE manager SET website_admin= TRUE WHERE id = 1; UPDATE manager SET tracker_admin= TRUE WHERE id = 1;
end if; end if;
RETURN NEW; RETURN NEW;
END; END;

View File

@ -17,21 +17,22 @@ const (
) )
type Manager struct { type Manager struct {
Id int `json:"id"` Id int64 `json:"id"`
Username string `json:"username"` Username string `json:"username"`
WebsiteAdmin bool `json:"website_admin"` WebsiteAdmin bool `json:"tracker_admin"`
RegisterTime int64 `json:"register_time"`
} }
func (database *Database) ValidateCredentials(username []byte, password []byte) (*Manager, error) { func (database *Database) ValidateCredentials(username []byte, password []byte) (*Manager, error) {
db := database.getDB() db := database.getDB()
row := db.QueryRow(`SELECT id, password, website_admin FROM manager WHERE username=$1`, row := db.QueryRow(`SELECT id, password, tracker_admin, register_time FROM manager WHERE username=$1`,
username) username)
manager := &Manager{} manager := &Manager{}
var passwordHash []byte var passwordHash []byte
err := row.Scan(&manager.Id, &passwordHash, &manager.WebsiteAdmin) err := row.Scan(&manager.Id, &passwordHash, &manager.WebsiteAdmin, &manager.RegisterTime)
if err != nil { if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithError(err).WithFields(logrus.Fields{
"username": username, "username": username,
@ -66,11 +67,11 @@ func (database *Database) SaveManager(manager *Manager, password []byte) error {
hash.Write([]byte(manager.Username)) hash.Write([]byte(manager.Username))
hashedPassword := hash.Sum(nil) hashedPassword := hash.Sum(nil)
row := db.QueryRow(`INSERT INTO manager (username, password, website_admin, register_time) row := db.QueryRow(`INSERT INTO manager (username, password, tracker_admin, register_time)
VALUES ($1,$2,$3, extract(epoch from now() at time zone 'utc')) RETURNING ID`, VALUES ($1,$2,$3, extract(epoch from now() at time zone 'utc')) RETURNING ID, register_time`,
manager.Username, hashedPassword, manager.WebsiteAdmin) manager.Username, hashedPassword, manager.WebsiteAdmin)
err := row.Scan(&manager.Id) err := row.Scan(&manager.Id, &manager.RegisterTime)
if err != nil { if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithError(err).WithFields(logrus.Fields{
"username": manager, "username": manager,
@ -92,16 +93,16 @@ func (database *Database) UpdateManager(manager *Manager) {
db := database.getDB() db := database.getDB()
res, err := db.Exec(`UPDATE manager SET website_admin=$1 WHERE id=$2`, res, err := db.Exec(`UPDATE manager SET tracker_admin=$1 WHERE id=$2`,
manager.WebsiteAdmin, manager.Id) manager.WebsiteAdmin, manager.Id)
handleErr(err) handleErr(err)
rowsAffected, _ := res.RowsAffected() rowsAffected, _ := res.RowsAffected()
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected, "rowsAffected": rowsAffected,
"manager": manager, "manager": manager,
}).Warning("Database.UpdateManager UPDATE") }).Trace("Database.UpdateManager UPDATE")
} }
func (database *Database) UpdateManagerPassword(manager *Manager, newPassword []byte) { func (database *Database) UpdateManagerPassword(manager *Manager, newPassword []byte) {
@ -119,10 +120,10 @@ func (database *Database) UpdateManagerPassword(manager *Manager, newPassword []
rowsAffected, _ := res.RowsAffected() rowsAffected, _ := res.RowsAffected()
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected, "rowsAffected": rowsAffected,
"id": manager.Id, "id": manager.Id,
}).Warning("Database.UpdateManagerPassword UPDATE") }).Trace("Database.UpdateManagerPassword UPDATE")
} }
func (database *Database) ManagerHasRoleOn(manager *Manager, projectId int64) ManagerRole { func (database *Database) ManagerHasRoleOn(manager *Manager, projectId int64) ManagerRole {
@ -140,3 +141,20 @@ func (database *Database) ManagerHasRoleOn(manager *Manager, projectId int64) Ma
return role return role
} }
func (database *Database) GetAllManagers() *[]Manager {
db := database.getDB()
rows, _ := db.Query(`SELECT id, register_time, tracker_admin, username FROM manager`)
managers := make([]Manager, 0)
for rows.Next() {
m := Manager{}
_ = rows.Scan(&m.Id, &m.RegisterTime, &m.WebsiteAdmin, &m.Username)
managers = append(managers, m)
}
return &managers
}

View File

@ -69,7 +69,7 @@ CREATE TABLE manager
( (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
register_time INTEGER NOT NULL, register_time INTEGER NOT NULL,
website_admin BOOLEAN NOT NULL, tracker_admin BOOLEAN NOT NULL,
username TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL,
password BYTEA NOT NULL password BYTEA NOT NULL
); );
@ -116,7 +116,7 @@ CREATE OR REPLACE FUNCTION on_manager_insert() RETURNS TRIGGER AS
$$ $$
BEGIN BEGIN
IF NEW.id = 1 THEN IF NEW.id = 1 THEN
UPDATE manager SET website_admin= TRUE WHERE id = 1; UPDATE manager SET tracker_admin= TRUE WHERE id = 1;
end if; end if;
RETURN NEW; RETURN NEW;
END; END;

View File

@ -13,6 +13,10 @@
{{"account.username" | translate}}:&nbsp; {{"account.username" | translate}}:&nbsp;
<pre>{{authService.account.username}}</pre> <pre>{{authService.account.username}}</pre>
</mat-list-item> </mat-list-item>
<mat-list-item>
{{"account.register_time" | translate}}:&nbsp;
<pre>{{moment.unix(authService.account.register_time).utc().format("YYYY-MM-DD HH:mm:ss UTC")}}</pre>
</mat-list-item>
</mat-list> </mat-list>
<mat-expansion-panel> <mat-expansion-panel>

View File

@ -1,6 +1,8 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {AuthService} from "../auth.service"; import {AuthService} from "../auth.service";
import * as moment from "moment"
@Component({ @Component({
selector: 'app-account-details', selector: 'app-account-details',
templateUrl: './account-details.component.html', templateUrl: './account-details.component.html',
@ -8,6 +10,8 @@ import {AuthService} from "../auth.service";
}) })
export class AccountDetailsComponent implements OnInit { export class AccountDetailsComponent implements OnInit {
public moment = moment;
constructor(public authService: AuthService) { constructor(public authService: AuthService) {
} }

View File

@ -69,4 +69,16 @@ export class ApiService {
return this.http.get(this.url + `/project/requests/${project}`) return this.http.get(this.url + `/project/requests/${project}`)
} }
getAllManagers() {
return this.http.get(this.url + "/manager/list")
}
promote(managerId: number) {
return this.http.get(this.url + `/manager/promote/${managerId}`)
}
demote(managerId: number) {
return this.http.get(this.url + `/manager/demote/${managerId}`)
}
} }

View File

@ -12,6 +12,7 @@ import {LoginComponent} from "./login/login.component";
import {AccountDetailsComponent} from "./account-details/account-details.component"; import {AccountDetailsComponent} from "./account-details/account-details.component";
import {WorkerDashboardComponent} from "./worker-dashboard/worker-dashboard.component"; import {WorkerDashboardComponent} from "./worker-dashboard/worker-dashboard.component";
import {ProjectPermsComponent} from "./project-perms/project-perms.component"; import {ProjectPermsComponent} from "./project-perms/project-perms.component";
import {ManagerListComponent} from "./manager-list/manager-list.component";
const routes: Routes = [ const routes: Routes = [
{path: "log", component: LogsComponent}, {path: "log", component: LogsComponent},
@ -22,7 +23,8 @@ const routes: Routes = [
{path: "project/:id/update", component: UpdateProjectComponent}, {path: "project/:id/update", component: UpdateProjectComponent},
{path: "project/:id/perms", component: ProjectPermsComponent}, {path: "project/:id/perms", component: ProjectPermsComponent},
{path: "new_project", component: CreateProjectComponent}, {path: "new_project", component: CreateProjectComponent},
{path: "workers", component: WorkerDashboardComponent} {path: "workers", component: WorkerDashboardComponent},
{path: "manager_list", component: ManagerListComponent}
]; ];
@NgModule({ @NgModule({

View File

@ -11,6 +11,10 @@
<button mat-button [class.mat-accent]="router.url == '/new_project'" class="nav-link" <button mat-button [class.mat-accent]="router.url == '/new_project'" class="nav-link"
[routerLink]="'new_project'" [routerLink]="'new_project'"
*ngIf="authService.logged">{{"nav.new_project" | translate}}</button> *ngIf="authService.logged">{{"nav.new_project" | translate}}</button>
<button mat-button [class.mat-accent]="router.url == '/manager_list'" class="nav-link"
[routerLink]="'manager_list'"
*ngIf="authService.logged && authService.account.tracker_admin"
>{{"nav.manager_list" | translate}}</button>
</div> </div>
<div class="small-nav"> <div class="small-nav">
<button mat-button [matMenuTriggerFor]="smallNav"> <button mat-button [matMenuTriggerFor]="smallNav">
@ -28,6 +32,10 @@
<button mat-menu-item [class.mat-accent]="router.url == '/new_project'" class="nav-link" <button mat-menu-item [class.mat-accent]="router.url == '/new_project'" class="nav-link"
[routerLink]="'new_project'" [routerLink]="'new_project'"
*ngIf="authService.logged">{{"nav.new_project" | translate}}</button> *ngIf="authService.logged">{{"nav.new_project" | translate}}</button>
<button mat-button [class.mat-accent]="router.url == '/manager_list'" class="nav-link"
[routerLink]="'manager_list'"
*ngIf="authService.logged && authService.account.tracker_admin"
>{{"nav.manager_list" | translate}}</button>
</mat-menu> </mat-menu>
</div> </div>
<span class="nav-spacer"></span> <span class="nav-spacer"></span>

View File

@ -48,6 +48,7 @@ import {LoginComponent} from './login/login.component';
import {AccountDetailsComponent} from './account-details/account-details.component'; import {AccountDetailsComponent} from './account-details/account-details.component';
import {WorkerDashboardComponent} from './worker-dashboard/worker-dashboard.component'; import {WorkerDashboardComponent} from './worker-dashboard/worker-dashboard.component';
import {ProjectPermsComponent} from './project-perms/project-perms.component'; import {ProjectPermsComponent} from './project-perms/project-perms.component';
import {ManagerListComponent} from './manager-list/manager-list.component';
export function createTranslateLoader(http: HttpClient) { export function createTranslateLoader(http: HttpClient) {
@ -68,6 +69,7 @@ export function createTranslateLoader(http: HttpClient) {
AccountDetailsComponent, AccountDetailsComponent,
WorkerDashboardComponent, WorkerDashboardComponent,
ProjectPermsComponent, ProjectPermsComponent,
ManagerListComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -60,6 +60,7 @@ export class AuthService {
.subscribe(() => .subscribe(() =>
this.apiService.getAccountDetails() this.apiService.getAccountDetails()
.subscribe((data: any) => { .subscribe((data: any) => {
this.logged = true;
this.account = data.manager; this.account = data.manager;
this.router.navigateByUrl("/account"); this.router.navigateByUrl("/account");
}), }),

View File

@ -11,7 +11,7 @@
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>{{ "project.clone_url" | translate}}</mat-label> <mat-label>{{ "project.clone_url" | translate}}</mat-label>
<input type="text" matInput [(ngModel)]="project.clone_url" <input type="text" matInput [(ngModel)]="project.clone_url" (change)="cloneUrlChange()"
[placeholder]="'project.clone_url_placeholder' | translate"> [placeholder]="'project.clone_url_placeholder' | translate">
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
@ -23,7 +23,9 @@
</mat-hint> </mat-hint>
</mat-form-field> </mat-form-field>
<mat-checkbox matInput [(ngModel)]="project.public" style="padding-top: 1em"> <mat-checkbox [(ngModel)]="project.public"
[disabled]="!authService.logged || !authService.account.tracker_admin"
style="padding-top: 1em">
{{"project.public" | translate}}</mat-checkbox> {{"project.public" | translate}}</mat-checkbox>
</mat-card-content> </mat-card-content>

View File

@ -3,6 +3,7 @@ import {Project} from "../models/project";
import {ApiService} from "../api.service"; import {ApiService} from "../api.service";
import {MessengerService} from "../messenger.service"; import {MessengerService} from "../messenger.service";
import {Router} from "@angular/router"; import {Router} from "@angular/router";
import {AuthService} from "../auth.service";
@Component({ @Component({
@ -16,14 +17,21 @@ export class CreateProjectComponent implements OnInit {
constructor(private apiService: ApiService, constructor(private apiService: ApiService,
private messengerService: MessengerService, private messengerService: MessengerService,
public authService: AuthService,
private router: Router) { private router: Router) {
this.project.name = "test";
this.project.public = true;
} }
ngOnInit() { ngOnInit() {
} }
cloneUrlChange() {
let tokens = this.project.clone_url.split("/");
if (tokens.length > 2) {
this.project.git_repo = tokens[tokens.length - 2] + "/" + tokens[tokens.length - 1]
}
}
onSubmit() { onSubmit() {
this.apiService.createProject(this.project).subscribe( this.apiService.createProject(this.project).subscribe(
data => { data => {

View File

@ -1,5 +1,5 @@
.table-container { .table-container {
height: 600px; /*height: 600px;*/
} }
.mat-table { .mat-table {

View File

@ -1,56 +1,63 @@
<div class="container"> <div class="container">
<div class="table-container"> <mat-card class="table-container">
<mat-form-field style="margin-right: 10px"> <mat-card-header>
<input matInput (keyup)="applyFilter($event.target.value)" [placeholder]="'logs.filter' | translate"> <mat-card-title>{{"logs.title" | translate}}</mat-card-title>
</mat-form-field> <mat-card-subtitle>{{"logs.subtitle" | translate}}</mat-card-subtitle>
<mat-button-toggle-group name="level" aria-label="Font Style" (change)="filterLevelChange($event)"> </mat-card-header>
<mat-button-toggle value="1">{{"logs.fatal" | translate}}</mat-button-toggle> <mat-card-content>
<mat-button-toggle value="2">{{"logs.panic" | translate}}</mat-button-toggle> <mat-form-field style="margin-right: 10px">
<mat-button-toggle value="3">{{"logs.error" | translate}}</mat-button-toggle> <input matInput (keyup)="applyFilter($event.target.value)" [placeholder]="'logs.filter' | translate">
<mat-button-toggle value="4">{{"logs.warn" | translate}}</mat-button-toggle> </mat-form-field>
<mat-button-toggle value="5">{{"logs.info" | translate}}</mat-button-toggle> <mat-button-toggle-group name="level" aria-label="Font Style" (change)="filterLevelChange($event)">
<mat-button-toggle value="6">{{"logs.debug" | translate}}</mat-button-toggle> <mat-button-toggle value="1">{{"logs.fatal" | translate}}</mat-button-toggle>
<mat-button-toggle value="7">{{"logs.trace" | translate}}</mat-button-toggle> <mat-button-toggle value="2">{{"logs.panic" | translate}}</mat-button-toggle>
</mat-button-toggle-group> <mat-button-toggle value="3">{{"logs.error" | translate}}</mat-button-toggle>
<mat-button-toggle value="4">{{"logs.warn" | translate}}</mat-button-toggle>
<mat-button-toggle value="5">{{"logs.info" | translate}}</mat-button-toggle>
<mat-button-toggle value="6">{{"logs.debug" | translate}}</mat-button-toggle>
<mat-button-toggle value="7">{{"logs.trace" | translate}}</mat-button-toggle>
</mat-button-toggle-group>
<button mat-raised-button style="float: right" <button mat-raised-button style="float: right"
[title]="'dashboard.refresh' | translate" [title]="'dashboard.refresh' | translate"
(click)="refresh()"> (click)="refresh()">
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
</button> </button>
<div class="mat-elevation-z8"> <div class="mat-elevation-z8">
<mat-table [dataSource]="data" matSort matSortActive="timestamp" <mat-table [dataSource]="data" matSort matSortActive="timestamp"
matSortDirection="desc"> matSortDirection="desc">
<ng-container matColumnDef="level"> <ng-container matColumnDef="level">
<mat-header-cell style="flex: 0 0 9em" mat-sort-header <mat-header-cell style="flex: 0 0 9em" mat-sort-header
*matHeaderCellDef>{{"logs.level" | translate}}</mat-header-cell> *matHeaderCellDef>{{"logs.level" | translate}}</mat-header-cell>
<mat-cell style="flex: 0 0 8em" <mat-cell style="flex: 0 0 8em"
*matCellDef="let entry"> {{("logs." + entry.level) | translate}} </mat-cell> *matCellDef="let entry"> {{("logs." + entry.level) | translate}} </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="timestamp"> <ng-container matColumnDef="timestamp">
<mat-header-cell style="flex: 0 0 15em" mat-sort-header <mat-header-cell style="flex: 0 0 15em" mat-sort-header
*matHeaderCellDef>{{"logs.time" | translate}}</mat-header-cell> *matHeaderCellDef>{{"logs.time" | translate}}</mat-header-cell>
<mat-cell style="flex: 0 0 12em" *matCellDef="let entry"> {{entry.timestamp}} </mat-cell> <mat-cell style="flex: 0 0 12em" *matCellDef="let entry"> {{entry.timestamp}} </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="message"> <ng-container matColumnDef="message">
<mat-header-cell mat-sort-header *matHeaderCellDef>{{"logs.message" | translate}}</mat-header-cell> <mat-header-cell mat-sort-header
<mat-cell style="flex: 0 0 30em" *matCellDef="let entry"> {{entry.message}} </mat-cell> *matHeaderCellDef>{{"logs.message" | translate}}</mat-header-cell>
</ng-container> <mat-cell style="flex: 0 0 30em" *matCellDef="let entry"> {{entry.message}} </mat-cell>
<ng-container matColumnDef="data"> </ng-container>
<mat-header-cell mat-sort-header *matHeaderCellDef>{{"logs.data" | translate}}</mat-header-cell> <ng-container matColumnDef="data">
<mat-cell *matCellDef="let entry"> <mat-header-cell mat-sort-header *matHeaderCellDef>{{"logs.data" | translate}}</mat-header-cell>
<pre>{{entry.data}}</pre> <mat-cell *matCellDef="let entry">
</mat-cell> <pre>{{entry.data}}</pre>
</ng-container> </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="logsCols"></mat-header-row> <mat-header-row *matHeaderRowDef="logsCols"></mat-header-row>
<mat-row *matRowDef="let row; columns: logsCols;"></mat-row> <mat-row *matRowDef="let row; columns: logsCols;"></mat-row>
</mat-table> </mat-table>
<mat-paginator [length]="logs.length" [pageSizeOptions]="[5,10,25,100]" [pageSize]="5"></mat-paginator> <mat-paginator [length]="logs.length" [pageSizeOptions]="[5,10,25,100]" [pageSize]="5"></mat-paginator>
</div> </div>
</div> </mat-card-content>
</mat-card>
</div> </div>

View File

@ -39,7 +39,7 @@ export class LogsComponent implements OnInit {
this.getLogs(Number(event.value)) this.getLogs(Number(event.value))
} }
private refresh() { public refresh() {
this.getLogs(this.filterLevel) this.getLogs(this.filterLevel)
} }

View File

@ -0,0 +1,53 @@
<div class="container">
<mat-card class="mat-elevation-z8">
<mat-card-header>
<mat-card-title>{{"manager_list.title" | translate}}</mat-card-title>
<mat-card-subtitle>{{"manager_list.subtitle" | translate}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<mat-table [dataSource]="data" matSort matSortActive="username" matSortDirection="asc">
<ng-container matColumnDef="username">
<mat-header-cell mat-sort-header
*matHeaderCellDef>{{"manager_list.username" | translate}}</mat-header-cell>
<mat-cell *matCellDef="let manager"> {{manager.username}} </mat-cell>
</ng-container>
<ng-container matColumnDef="tracker_admin">
<mat-header-cell *matHeaderCellDef
mat-sort-header>{{"manager_list.role" | translate}}</mat-header-cell>
<mat-cell *matCellDef="let manager">
<mat-icon *ngIf="manager.tracker_admin" [title]="'manager_list.tracker_admin' | translate">
supervisor_account
</mat-icon>
</mat-cell>
</ng-container>
<ng-container matColumnDef="register_time">
<mat-header-cell *matHeaderCellDef>{{"manager_list.register_time" | translate}}</mat-header-cell>
<mat-cell *matCellDef="let manager">
{{moment.unix(manager.register_time).utc().format("UTC YYYY-MM-DD HH:mm:ss")}}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>{{"manager_list.actions" | translate}}</mat-header-cell>
<mat-cell *matCellDef="let manager">
<button mat-raised-button color="primary"
*ngIf="canPromote(manager)"
(click)="promote(manager)">{{"manager_list.promote" | translate}}</button>
<button mat-raised-button color="warn"
*ngIf="canDemote(manager)"
(click)="demote(manager)">{{"manager_list.demote" | translate}}</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="cols"></mat-header-row>
<mat-row *matRowDef="let row; columns: cols;"></mat-row>
</mat-table>
<mat-paginator [class.hidden]="managers.length<25" [length]="managers.length"
[pageSizeOptions]="[25,50,100]" [pageSize]="25"></mat-paginator>
</mat-card-content>
</mat-card>
</div>

View File

@ -0,0 +1,70 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {ApiService} from "../api.service";
import {MessengerService} from "../messenger.service";
import {TranslateService} from "@ngx-translate/core";
import {MatPaginator, MatSort, MatTableDataSource} from "@angular/material";
import * as moment from "moment"
import {AuthService} from "../auth.service";
@Component({
selector: 'app-manager-list',
templateUrl: './manager-list.component.html',
styleUrls: ['./manager-list.component.css']
})
export class ManagerListComponent implements OnInit {
managers = [];
data;
moment = moment;
cols = ['username', 'tracker_admin', 'register_time', 'actions'];
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(private apiService: ApiService,
private messengerService: MessengerService,
private translate: TranslateService,
private authService: AuthService
) {
this.data = new MatTableDataSource<Manager>()
}
ngOnInit() {
this.getManagers();
this.data.paginator = this.paginator;
this.data.sort = this.sort;
}
canPromote(manager: Manager) {
return !manager.tracker_admin
}
canDemote(manager: Manager) {
return manager.tracker_admin && manager.username != this.authService.account.username
}
public promote(manager: Manager) {
this.apiService.promote(manager.id)
.subscribe(() => this.getManagers())
}
public demote(manager: Manager) {
this.apiService.demote(manager.id)
.subscribe(() => this.getManagers())
}
private getManagers() {
this.apiService.getAllManagers()
.subscribe(data => {
this.data.data = data["managers"]
},
error => {
if (error && (error.status == 401 || error.status == 403)) {
console.log(error.error.message);
this.translate.get("manager_list.unauthorized")
.subscribe(t => this.messengerService.show(t));
}
})
}
}

View File

@ -1,5 +1,6 @@
interface Manager { interface Manager {
id: number; id: number;
username: string username: string
website_admin: boolean; tracker_admin: boolean
register_time: number
} }

View File

@ -18,9 +18,11 @@
<div> <div>
<button mat-raised-button color="primary" [routerLink]="'/project/' + project.id"> <button mat-raised-button color="primary" [routerLink]="'/project/' + project.id">
<mat-icon>timeline</mat-icon>{{"projects.dashboard" | translate}}</button> <mat-icon>timeline</mat-icon>{{"projects.dashboard" | translate}}</button>
<button mat-raised-button color="primary" [routerLink]="'/project/' + project.id + '/update'"> <button mat-raised-button color="primary" [routerLink]="'/project/' + project.id + '/update'"
*ngIf="authService.logged">
<mat-icon>build</mat-icon>{{"project.update" | translate}}</button> <mat-icon>build</mat-icon>{{"project.update" | translate}}</button>
<button mat-raised-button color="primary" [routerLink]="'/project/' + project.id + '/perms'"> <button mat-raised-button color="primary" [routerLink]="'/project/' + project.id + '/perms'"
*ngIf="authService.logged">
<mat-icon>perm_identity</mat-icon> <mat-icon>perm_identity</mat-icon>
{{"project.perms" | translate}}</button> {{"project.perms" | translate}}</button>
</div> </div>

View File

@ -1,6 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ApiService} from "../api.service"; import {ApiService} from "../api.service";
import {Project} from "../models/project"; import {Project} from "../models/project";
import {AuthService} from "../auth.service";
@Component({ @Component({
selector: 'app-project-list', selector: 'app-project-list',
@ -9,7 +10,8 @@ import {Project} from "../models/project";
}) })
export class ProjectListComponent implements OnInit { export class ProjectListComponent implements OnInit {
constructor(private apiService: ApiService) { constructor(private apiService: ApiService,
public authService: AuthService) {
} }
projects: Project[]; projects: Project[];

View File

@ -1,10 +1,3 @@
.unauthorized {
text-align: center;
color: #616161;
min-height: 3em;
margin-top: 2em !important;
}
.text-mono { .text-mono {
font-family: Hack, Courier, "Courier New", monospace; font-family: Hack, Courier, "Courier New", monospace;
color: #ff4081; color: #ff4081;

View File

@ -16,7 +16,8 @@
<h4 mat-line>{{w.alias}}</h4> <h4 mat-line>{{w.alias}}</h4>
<div mat-line> <div mat-line>
Id=<span class="text-mono">{{w.id}}</span>, {{"perms.created" | translate}} Id=<span class="text-mono">{{w.id}}</span>, {{"perms.created" | translate}}
<span class="text-mono">{{moment.unix(w.created).format("YYYY-MM-DD HH:mm:ss UTC")}}</span> <span
class="text-mono">{{moment.unix(w.created).utc().format("UTC YYYY-MM-DD HH:mm:ss")}}</span>
</div> </div>
<span style="flex: 1 1 auto;"></span> <span style="flex: 1 1 auto;"></span>
<button mat-raised-button color="primary" [title]="'perms.grant' | translate"> <button mat-raised-button color="primary" [title]="'perms.grant' | translate">

View File

@ -54,7 +54,7 @@ export class ProjectPermsComponent implements OnInit {
}) })
} }
private refresh() { public refresh() {
this.getProjectRequests() this.getProjectRequests()
} }
} }

View File

@ -20,7 +20,7 @@ export class WorkerDashboardComponent implements OnInit {
this.refresh() this.refresh()
} }
private refresh() { public refresh() {
this.apiService.getWorkerStats() this.apiService.getWorkerStats()
.subscribe((data: any) => { .subscribe((data: any) => {
this.updateChart(data.stats) this.updateChart(data.stats)

View File

@ -7,9 +7,12 @@
"new_project": "New Project", "new_project": "New Project",
"login": "Login", "login": "Login",
"worker_dashboard": "Workers", "worker_dashboard": "Workers",
"account": "Account" "account": "Account",
"manager_list": "Managers"
}, },
"logs": { "logs": {
"title": "Logs",
"subtitle": "",
"filter": "Filter", "filter": "Filter",
"time": "Time (UTC)", "time": "Time (UTC)",
"level": "Level", "level": "Level",
@ -43,7 +46,8 @@
"login": "Login", "login": "Login",
"new_account": "Create account", "new_account": "Create account",
"account": "Account details", "account": "Account details",
"workers": "Workers" "workers": "Workers",
"manager_list": "Managers"
}, },
"project": { "project": {
"name": "Project name", "name": "Project name",
@ -82,7 +86,8 @@
"title": "Account details", "title": "Account details",
"subtitle": "toto: subtitle", "subtitle": "toto: subtitle",
"username": "Username", "username": "Username",
"logout": "Logout" "logout": "Logout",
"register_time": "Register date"
}, },
"workers": { "workers": {
"title": "Completed tasks per worker", "title": "Completed tasks per worker",
@ -100,5 +105,17 @@
"messenger": { "messenger": {
"close": "Close", "close": "Close",
"unauthorized": "Unauthorized" "unauthorized": "Unauthorized"
},
"manager_list": {
"title": "Manager list",
"subtitle": "",
"username": "Username",
"role": "Role",
"tracker_admin": "Tracker administrator",
"actions": "Actions",
"unauthorized": "You are not authorized to access this page",
"promote": "Promote",
"demote": "Demote",
"register_time": "Register date"
} }
} }

View File

@ -2,14 +2,17 @@
"nav": { "nav": {
"title": "task_tracker (fr)", "title": "task_tracker (fr)",
"lang_select": "Langue", "lang_select": "Langue",
"logs": "Journal", "logs": "Journaux",
"project_list": "Projets", "project_list": "Projets",
"new_project": "Nouveau projet", "new_project": "Nouveau projet",
"login": "Ouvrir un session", "login": "Ouvrir un session",
"worker_dashboard": "Workers", "worker_dashboard": "Workers",
"account": "Compte" "account": "Compte",
"manager_list": "Managers"
}, },
"logs": { "logs": {
"title": "Journaux",
"subtitle": "",
"filter": "Filtrer", "filter": "Filtrer",
"time": "Date (UTC)", "time": "Date (UTC)",
"level": "Niveau", "level": "Niveau",
@ -44,7 +47,8 @@
"login": "Ouverture de session", "login": "Ouverture de session",
"new_account": "Création de compte", "new_account": "Création de compte",
"account": "Compte", "account": "Compte",
"workers": "Workers" "workers": "Workers",
"manager_list": "Managers"
}, },
"project": { "project": {
"name": "Nom du projet", "name": "Nom du projet",
@ -84,7 +88,8 @@
"title": "Détails du compte", "title": "Détails du compte",
"subtitle": "toto: sous-titre", "subtitle": "toto: sous-titre",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"logout": "Fermer la session" "logout": "Fermer la session",
"register_time": "Date de création"
}, },
"workers": { "workers": {
"title": "Tâches complétés par worker", "title": "Tâches complétés par worker",
@ -102,6 +107,18 @@
"messenger": { "messenger": {
"close": "Fermer", "close": "Fermer",
"unauthorized": "Non autorisé" "unauthorized": "Non autorisé"
},
"manager_list": {
"title": "Liste ",
"subtitle": "",
"username": "Nom d'utilisateur",
"role": "Rôle",
"tracker_admin": "Administrateur",
"actions": "Actions",
"unauthorized": "Vou n'êtes pas authorisé à accéder à cette page",
"promote": "Promouvoir",
"demote": "Rétrograder",
"register_time": "Date d'inscription"
} }
} }

View File

@ -17,7 +17,7 @@ body {
padding-left: 15px; padding-left: 15px;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
margin-top: 15px; margin-top: 30px;
} }
@media (min-width: 576px) { @media (min-width: 576px) {
@ -86,3 +86,11 @@ pre {
.hidden { .hidden {
display: none !important; display: none !important;
} }
.unauthorized {
text-align: center;
color: #616161;
min-height: 3em;
margin-top: 2em !important;
}