Skip to content
Snippets Groups Projects
Commit 1689c695 authored by Nicolas PAGNY's avatar Nicolas PAGNY
Browse files

Add client Simplified interface and apiclient

parent 653b95bf
No related branches found
No related tags found
No related merge requests found
package adminserver
import (
"net/http"
"forge.grandlyon.com/npernoud/glcpro/internal/apiclient"
)
func CreateAdminServer() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/clients", apiclient.ProcessClients)
return mux
}
package adminserver
import (
"net/http"
"os"
"testing"
"forge.grandlyon.com/npernoud/glcpro/pkg/tester"
)
var (
noH map[string]string
)
func TestUserServer(t *testing.T) {
h := CreateAdminServer()
do := tester.CreateHandlerTester(t, h)
do("GET", "/clients", noH, "", http.StatusOK, "")
// Remove the database
os.Remove("./glcpro.db")
}
......@@ -2,53 +2,55 @@ package apiclient
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"forge.grandlyon.com/npernoud/glcpro/internal/client"
)
// User represents a logged user
type Client struct {
ID string `json:"id,omitempty"`
Login string `json:"login"`
func ProcessClients(w http.ResponseWriter, r *http.Request) {
switch method := r.Method; method {
case "GET":
SendClients(w, r)
case "PUT":
AddClient(w, r)
case "POST":
UpdateClient(w, r)
case "DELETE":
DeleteClient(w, r)
default:
http.Error(w, "method not allowed", http.StatusBadRequest)
}
}
// TokenData represents the data held into a token
type TokenData struct {
Client
URL string `json:"url,omitempty"`
func SendClients(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(client.GetClients())
}
func CreateApiclientServer() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Handler login OK", http.StatusNotImplemented)
})
func AddClient(w http.ResponseWriter, r *http.Request) {
var newClient client.Client
json.NewDecoder(r.Body).Decode(&newClient)
client.Create(newClient.Id_client, newClient.Secret)
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Handler logout OK", http.StatusNotImplemented)
})
SendClients(w, r)
}
//return user data
mux.HandleFunc("/whoAmI", func(w http.ResponseWriter, r *http.Request) {
user, err := getTokenData(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(user)
fmt.Print(user)
})
func UpdateClient(w http.ResponseWriter, r *http.Request) {
var clientToUpdate client.Client
json.NewDecoder(r.Body).Decode(&clientToUpdate)
client.Update(clientToUpdate.ID, clientToUpdate.Id_client, clientToUpdate.Secret)
return mux
SendClients(w, r)
}
func getTokenData(r *http.Request) (TokenData, error) {
client, ok := r.Context().Value(0).(TokenData)
if !ok {
return client, errors.New("user could not be get form context")
func DeleteClient(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
id, err := strconv.Atoi(query.Get("id"))
if err != nil {
http.Error(w, "id not valid", http.StatusBadRequest)
}
return client, nil
client.Delete(id)
SendClients(w, r)
}
package apiclient
import (
"net/http"
"testing"
"forge.grandlyon.com/npernoud/glcpro/pkg/tester"
)
var (
noH map[string]string
)
func TestUserServer(t *testing.T) {
h := CreateApiclientServer()
do := tester.CreateHandlerTester(t, h)
do("GET", "/login", noH, "", http.StatusNotImplemented, "Handler login OK")
do("GET", "/logout", noH, "", http.StatusNotImplemented, "Handler logout OK")
do("GET", "/whoAmI", noH, "", http.StatusBadRequest, "")
}
......@@ -4,9 +4,10 @@ import (
"fmt"
"net/http"
"forge.grandlyon.com/npernoud/glcpro/internal/apiclient"
"forge.grandlyon.com/npernoud/glcpro/internal/adminserver"
"forge.grandlyon.com/npernoud/glcpro/internal/matcher"
"forge.grandlyon.com/npernoud/glcpro/internal/oidcserver"
"forge.grandlyon.com/npernoud/glcpro/internal/userserver"
"forge.grandlyon.com/npernoud/glcpro/pkg/common"
"forge.grandlyon.com/npernoud/glcpro/pkg/middlewares"
)
......@@ -14,15 +15,17 @@ import (
// CreateRootMux creates a RootMux
func CreateRootMux(staticDir string) *http.ServeMux {
// Create the main handler
mainMux := http.NewServeMux()
apiMux := http.NewServeMux()
apiMux.HandleFunc("/healthcheck", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "OK")
})
mainMux := http.NewServeMux()
mainMux.Handle("/api/", http.StripPrefix("/api", apiMux))
mainMux.Handle("/api/oidc/", middlewares.Cors(http.StripPrefix("/api/oidc", oidcserver.CreateOIDCServer())))
mainMux.Handle("/api/matcher/", middlewares.Cors(http.StripPrefix("/api/matcher", matcher.CreateMatcherServer())))
mainMux.Handle("/api/apiclient/", middlewares.Cors(http.StripPrefix("/api/apiclient", apiclient.CreateApiclientServer())))
mainMux.Handle("/api/user/", middlewares.Cors(http.StripPrefix("/api/users", userserver.CreateUserServer())))
mainMux.Handle("/api/admin/", middlewares.Cors(http.StripPrefix("/api/admin", adminserver.CreateAdminServer())))
// Serve static files falling back to serving index.html
mainMux.Handle("/", middlewares.NoCache(http.FileServer(&common.FallBackWrapper{Assets: http.Dir(staticDir)})))
// Put it together into the main handler
......
package main
import (
"bytes"
"io/ioutil"
"net/http"
"os"
"os/signal"
......@@ -20,6 +22,15 @@ var (
debugMode = common.BoolValueFromEnv("DEBUG_MODE", false) // Debug mode, disable Let's Encrypt, enable CORS and more logging
)
func MiddlewareKeepBody(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
readBody, _ := ioutil.ReadAll((r.Body))
newBody := ioutil.NopCloser(bytes.NewBuffer(readBody))
r.Body = newBody
next.ServeHTTP(w, r)
})
}
func main() {
// Initialize logger
if logFile != "" {
......@@ -51,6 +62,6 @@ func main() {
log.Logger.Fatal(http.ListenAndServe(":"+strconv.Itoa(httpPort), log.Middleware(rootMux)))
} else {
log.Logger.Fatal(http.ListenAndServe(":"+strconv.Itoa(httpPort), rootMux))
log.Logger.Fatal(http.ListenAndServe(":"+strconv.Itoa(httpPort), MiddlewareKeepBody(rootMux)))
}
}
// Imports
import { AnimateCSS, RandomString } from "/services/common/common.js";
import { Delete } from "/services/common/delete.js";
import { HandleError } from "/services/common/errors.js";
export async function mount(where, client) {
const clientsComponent = new Clients(client);
await clientsComponent.mount(where);
}
class Clients {
constructor(client) {
this.current_client = client;
}
// DOM elements
clientID;
id_client_field;
secret_field;
// local variables
clients;
async mount(mountpoint) {
document.getElementById(mountpoint).innerHTML = /* HTML */ `
<div class="table-container">
<table class="table is-bordered is-narrow is-hoverable is-fullwidth">
<thead>
<tr class="is-selected">
<th>Id_client</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="clients"></tbody>
</table>
</div>
<button id="clients-new" class="button is-primary">
<span class="icon is-small">
<i class="fas fa-plus"></i>
</span>
</button>
<div class="modal" id="clients-modal">
<div class="modal-background"></div>
<div class="modal-card" id="clients-modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add/Edit client</p>
<button
class="delete"
aria-label="close"
id="clients-modal-close"
></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">Id_client</label>
<div class="control">
<input
class="input"
type="text"
id="clients-modal-id_client"
/>
</div>
</div>
<label class="label">Secret</label>
<div class="field has-addons">
<div class="control is-expanded">
<input
class="input"
type="text"
id="clients-modal-secret"
placeholder="Leave empty to keep current secret"
/>
</div>
<div class="control">
<button id="clients-modal-secret-generate" class="button">
<span class="icon is-small">
<i class="fas fa-dice"></i>
</span>
</button>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button id="clients-modal-save" class="button is-success">
Save changes
</button>
<button id="clients-modal-cancel" class="button">Cancel</button>
</footer>
</div>
</div>
`;
this.registerModalFields();
await this.firstShowClients();
}
cleanClient(client) {
let props = ["secret"];
for (const prop of props) {
client[prop] = client[prop] === undefined ? "" : client[prop];
}
}
clientTemplate(client) {
this.cleanClient(client);
return /* HTML */ `
<p id="clients-client-hidden-ID" hidden>${client.ID}<p/>
<tr id="clients-client-${client.ID}">
<th>${client.Id_client}</th>
<td>
<a
id="clients-client-edit-${client.ID}"
class="button is-link is-small"
>
<span>Edit</span>
<span class="icon is-small">
<i class="fas fa-pen"></i>
</span>
</a>
<a
id="clients-client-delete-${client.ID}"
class="button is-danger is-small"
>
<span>Delete</span>
<span class="icon is-small">
<i class="fas fa-times"></i>
</span>
</a>
</td>
</tr>
`;
}
displayClients() {
this.clients.sort((a, b) => parseInt(a.id) - parseInt(b.id));
const markup = this.clients
.map((client) => this.clientTemplate(client))
.join("");
document.getElementById("clients").innerHTML = markup;
this.clients.map((client) => {
document
.getElementById(`clients-client-edit-${client.ID}`)
.addEventListener("click", () => {
this.editClient(client);
});
document
.getElementById(`clients-client-delete-${client.ID}`)
.addEventListener("click", () => {
new Delete(() => {
this.deleteClient(client);
}, client.login);
});
});
}
async firstShowClients() {
try {
const response = await fetch("/api/admin/clients", {
method: "GET",
});
if (response.status !== 200) {
throw new Error(
`Clients could not be fetched (status ${response.status})`
);
}
this.clients = await response.json();
this.displayClients();
} catch (e) {
HandleError(e);
}
}
async deleteClient(client) {
try {
const response = await fetch("/api/admin/clients?id=" + client.ID, {
method: "DELETE",
});
if (response.status !== 200) {
throw new Error(
`Client could not be deleted (status ${response.status})`
);
}
document.getElementById(`clients-client-${client.ID}`).remove();
} catch (e) {
HandleError(e);
}
}
registerModalFields() {
this.clientID = document.getElementById("clients-client-hidden-ID")
this.id_client_field = document.getElementById("clients-modal-id_client");
this.secret_field = document.getElementById("clients-modal-secret");
document
.getElementById(`clients-modal-close`)
.addEventListener("click", () => {
this.toggleModal();
});
document
.getElementById(`clients-modal-cancel`)
.addEventListener("click", () => {
this.toggleModal();
});
document
.getElementById(`clients-modal-save`)
.addEventListener("click", () => {
this.postClient();
});
document.getElementById(`clients-new`).addEventListener("click", () => {
this.newClient();
});
document
.getElementById(`clients-modal-secret-generate`)
.addEventListener("click", () => {
this.secret_field.value = RandomString(48);
});
}
async editClient(client) {
this.cleanClient(client);
this.clientID = client.ID
this.id_client_field.value = client.Id_client;
this.toggleModal();
}
async newClient() {
this.id_client_field.value = "";
this.secret_field.value = RandomString(48);
this.toggleModal();
}
async postClient() {
var methodToUse = ""
if (this.clientID == "") {
methodToUse = "PUT"
}else{
methodToUse = "POST"
}
try {
const response = await fetch("/api/admin/clients", {
method: methodToUse,
header: new Headers({
"XSRF-Token": "application/json"
}),
body: JSON.stringify({
ID: this.clientID,
Id_client: this.id_client_field.value,
Secret: this.secret_field.value,
}),
});
if (response.status !== 200) {
throw new Error(
`Clients could not be updated (status ${response.status})`
);
}
this.clients = await response.json();
this.displayClients();
} catch (e) {
HandleError(e);
}
this.toggleModal();
}
async toggleModal() {
const modal = document.getElementById("clients-modal");
const card = document.getElementById("clients-modal-card");
if (modal.classList.contains("is-active")) {
AnimateCSS(modal, "fadeOut");
await AnimateCSS(card, "zoomOut");
modal.classList.remove("is-active");
} else {
modal.classList.add("is-active");
AnimateCSS(modal, "fadeIn");
AnimateCSS(card, "zoomIn");
}
}
}
......@@ -2,6 +2,7 @@ import * as Auth from "/components/auth/auth.js";
import * as Matcher from "/components/matcher/matcher.js";
import * as Info from "/components/info/info.js";
import * as Login from "./components/login/login.js";
import * as Clients from "./components/clients/clients.js";
const mountPoint = document.getElementById("main");
let user = {};
......@@ -47,6 +48,11 @@ async function navigate() {
await Login.mount("main");
})
break;
case "/clients":
load(mountPoint, async function() {
await Clients.mount("main");
})
break;
default:
location.pathname = "auth";
}
......
export const AnimateCSS = (element, animation, prefix = "animate__") =>
console.log("")
// We create a Promise and return it
new Promise((resolve, reject) => {
const animationName = `${prefix}${animation}`;
element.classList.add(`${prefix}animated`, animationName);
// When the animation ends, we clean the classes and resolve the Promise
function handleAnimationEnd() {
element.classList.remove(`${prefix}animated`, animationName);
element.removeEventListener("animationend", handleAnimationEnd);
resolve("Animation ended");
}
element.addEventListener("animationend", handleAnimationEnd);
});
export let GID = (obj, id) => {
return document.getElementById(obj.prefix + id);
};
......
// Imports
import { AnimateCSS } from "/services/common/common.js";
export class Delete {
constructor(okFunction, what) {
let deleteModal = document.createElement("div");
deleteModal.classList.add("modal", "animate__animated", "animate__fadeIn", "is-active");
deleteModal.innerHTML = /* HTML */ `
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<div class="field">
<label class="label">Confirm deletion of <i>${what}</i> ?</label>
</div>
<div class="field is-grouped">
<div class="control">
<button id="delete-ok" class="button is-danger">
<span class="icon"><i class="fas fa-check"></i></span><span>Delete</span>
</button>
</div>
<div class="control">
<button id="delete-cancel" class="button">
<span class="icon"><i class="fas fa-times-circle"></i></span><span>Cancel</span>
</button>
</div>
</div>
</div>
</div>
`;
const deleteOK = deleteModal.querySelector("#delete-ok");
const deleteCancel = deleteModal.querySelector("#delete-cancel");
const toggleButtons = () => {
deleteOK.classList.toggle("is-loading");
deleteOK.disabled = !deleteOK.disabled;
deleteCancel.disabled = !deleteCancel.disabled;
};
deleteOK.addEventListener("click", async () => {
toggleButtons();
await okFunction();
toggleButtons();
await AnimateCSS(deleteModal, "fadeOut");
deleteModal.parentNode.removeChild(deleteModal);
});
deleteCancel.addEventListener("click", async () => {
await AnimateCSS(deleteModal, "fadeOut");
deleteModal.parentNode.removeChild(deleteModal);
});
document.body.appendChild(deleteModal);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment