diff --git a/app/handlers_licensing.go b/app/handlers_licensing.go index b96e2ad..e7cd34a 100644 --- a/app/handlers_licensing.go +++ b/app/handlers_licensing.go @@ -36,6 +36,128 @@ func (app *application) HandlerApiGetLicensingRecord(w http.ResponseWriter, r *h app.writeJSON(w, http.StatusOK, license) } +func (app *application) ParseLicenseFormData(r *http.Request) (license models.License, err error) { + err = r.ParseMultipartForm(32 << 20) + if err != nil { + app.errorLog.Println(fmt.Errorf("cannot parse the data, probably the attachment is too big - %s", err)) + return license, err + } + + license = models.License{ + UUID: r.PostFormValue("uuid"), + Category: r.PostFormValue("category"), + Name: r.PostFormValue("name"), + Number: r.PostFormValue("number"), + Issued: r.PostFormValue("issued"), + ValidFrom: r.PostFormValue("valid_from"), + ValidUntil: r.PostFormValue("valid_until"), + Remarks: r.PostFormValue("remarks"), + } + + // check attached file + file, header, err := r.FormFile("document") + if err != nil { + if !strings.Contains(err.Error(), "no such file") { + return license, err + } + } else { + defer file.Close() + license.DocumentName = header.Filename + + // read file + bs, err := io.ReadAll(file) + if err != nil { + return license, err + } + license.Document = bs + } + + return license, nil +} + +// HandlerLicensingRecordSave is a handler for creating or updating license record +func (app *application) HandlerApiNewLicensingRecord(w http.ResponseWriter, r *http.Request) { + + err := r.ParseMultipartForm(32 << 20) + if err != nil { + app.errorLog.Println(fmt.Errorf("cannot parse the data, probably the attachment is too big - %s", err)) + app.handleError(w, err) + return + } + + license := models.License{ + UUID: r.PostFormValue("uuid"), + Category: r.PostFormValue("category"), + Name: r.PostFormValue("name"), + Number: r.PostFormValue("number"), + Issued: r.PostFormValue("issued"), + ValidFrom: r.PostFormValue("valid_from"), + ValidUntil: r.PostFormValue("valid_until"), + Remarks: r.PostFormValue("remarks"), + } + + // check attached file + file, header, err := r.FormFile("document") + if err != nil { + if !strings.Contains(err.Error(), "no such file") { + app.handleError(w, err) + return + } + } else { + defer file.Close() + license.DocumentName = header.Filename + + // read file + bs, err := io.ReadAll(file) + if err != nil { + app.handleError(w, err) + return + } + license.Document = bs + } + + // new record + uuid, err := uuid.NewRandom() + if err != nil { + app.handleError(w, err) + return + } + + license.UUID = uuid.String() + + err = app.db.InsertLicenseRecord(license) + if err != nil { + app.handleError(w, err) + return + } + + var response models.JSONResponse + response.OK = true + response.Message = "New License Record created" + response.Data = license.UUID + + app.writeJSON(w, http.StatusOK, response) +} + +// HandlerApiUpdateLicensingRecord is a handler for updating license record +func (app *application) HandlerApiUpdateLicensingRecord(w http.ResponseWriter, r *http.Request) { + license, err := app.ParseLicenseFormData(r) + if err != nil { + app.handleError(w, err) + return + } + + // just update the license record + err = app.db.UpdateLicenseRecord(license) + if err != nil { + app.handleError(w, err) + return + + } + + app.writeOkResponse(w, "License Record has been updated") +} + ////////////////////////////////////////////// ///////////////////////////////////////////// @@ -58,25 +180,6 @@ func (app *application) HandlerLicensingDownload(w http.ResponseWriter, r *http. } } -// HandlerLicensingRecordNew is a handler for creating a new license record -func (app *application) HandlerLicensingRecordNew(w http.ResponseWriter, r *http.Request) { - - var license models.License - - categories, err := app.db.GetLicensesCategory() - if err != nil { - app.errorLog.Println(err) - } - - data := make(map[string]interface{}) - data["license"] = license - data["categories"] = categories - - if err := app.renderTemplate(w, r, "license-record", &templateData{Data: data}); err != nil { - app.errorLog.Println(err) - } -} - // HandlerLicensingRecordDelete is a handler for deleting license record func (app *application) HandlerLicensingRecordDelete(w http.ResponseWriter, r *http.Request) { diff --git a/app/routes.go b/app/routes.go index 4d4b2c6..b3ca9bb 100644 --- a/app/routes.go +++ b/app/routes.go @@ -162,6 +162,8 @@ func (app *application) routes() *chi.Mux { r.Route("/licensing", func(r chi.Router) { r.Get("/list", app.HandlerApiGetLicensingRecords) r.With(middleware.Compress(5)).Get("/{uuid}", app.HandlerApiGetLicensingRecord) + r.Post("/new", app.HandlerApiNewLicensingRecord) + r.Put("/{uuid}", app.HandlerApiUpdateLicensingRecord) }) // attachments @@ -248,7 +250,6 @@ func (app *application) routes() *chi.Mux { r.Get(APIMapData, app.HandlerMapData) // documents - r.Get(APILicensingNew, app.HandlerLicensingRecordNew) r.Get(APILicensingDownloadUUID, app.HandlerLicensingDownload) r.Post(APILicensingSave, app.HandlerLicensingRecordSave) diff --git a/app/ui/package-lock.json b/app/ui/package-lock.json index 8db50c1..f8f3fbc 100644 --- a/app/ui/package-lock.json +++ b/app/ui/package-lock.json @@ -19,6 +19,7 @@ "arc": "^0.1.4", "dayjs": "^1.11.13", "export-to-csv": "^1.4.0", + "file-type": "^20.0.0", "material-react-table": "^3.0.3", "ol": "^10.3.1", "react": "^18.3.1", @@ -1945,6 +1946,30 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.6.tgz", + "integrity": "sha512-SdR/i05U7Xhnsq36iyIq/ZiGGw4PKzw4ww3bOq80Pjj4wyXpqyTcgrgdDdGlcatnlvzNJx8CQw3hp6QZvkUwhA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.7", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@toolpad/core": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@toolpad/core/-/core-0.11.0.tgz", @@ -3391,6 +3416,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3404,6 +3435,24 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.0.0.tgz", + "integrity": "sha512-w8Z+QqWtEPIfyoPx9lDhzR52UjY5PfZunJ6lmH48oCR2gVbV52Aaw2bVtbi7P4EAlSpjn8xmNDiRAieYaabEIQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.0.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -3797,6 +3846,26 @@ "node": ">=16.17.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4949,6 +5018,19 @@ "pbf": "bin/pbf" } }, + "node_modules/peek-readable": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.3.1.tgz", + "integrity": "sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5670,6 +5752,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.0.1.tgz", + "integrity": "sha512-7OOJepVlvlcgjW/fLNCsIqpNleAoi1y0LTRWGnOpABOSpRmw+65HvnruoOCnjpaQ1efnlYpQ/JwHKuaombnuXQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.3.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -5738,6 +5837,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5829,6 +5945,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/app/ui/package.json b/app/ui/package.json index c94fbbe..99d297f 100644 --- a/app/ui/package.json +++ b/app/ui/package.json @@ -21,6 +21,7 @@ "arc": "^0.1.4", "dayjs": "^1.11.13", "export-to-csv": "^1.4.0", + "file-type": "^20.0.0", "material-react-table": "^3.0.3", "ol": "^10.3.1", "react": "^18.3.1", diff --git a/app/ui/src/components/LicenseRecord/LicensePreview.jsx b/app/ui/src/components/LicenseRecord/LicensePreview.jsx new file mode 100644 index 0000000..53b860d --- /dev/null +++ b/app/ui/src/components/LicenseRecord/LicensePreview.jsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from 'react'; +import { fileTypeFromBuffer } from 'file-type'; +// MUI UI elements +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +// Custom components and libraries +import CardHeader from '../UIElements/CardHeader'; + +export const decodeBase64 = async (base64String) => { + const binaryString = atob(base64String); + const uint8Array = new Uint8Array( + binaryString.split('').map((char) => char.charCodeAt(0)) + ); + + const fileType = await fileTypeFromBuffer(uint8Array); + return fileType?.mime || 'unknown'; +}; + +export const LicensePreview = ({ license }) => { + const [fileType, setFileType] = useState("unknown"); + const [previewUrl, setPreviewUrl] = useState(null); + + useEffect(() => { + const loadDocument = async () => { + if (!license?.document) { + setFileType("unknown"); + setPreviewUrl(null); + return; + } + + if (license.document instanceof File) { + // Handle File type (user-selected file) + const file = license.document; + const mimeType = file.type || "unknown"; // Get the MIME type from the file + const dataUrl = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.readAsDataURL(file); // Generate data URL for the file + }); + setFileType(mimeType); + setPreviewUrl(dataUrl); + } else { + // Handle base64-encoded document + const mimeType = await decodeBase64(license.document); + const dataUrl = `data:${mimeType};base64,${license.document}`; + setFileType(mimeType); + setPreviewUrl(dataUrl); + } + }; + + loadDocument(); + }, [license?.document]); + + const renderPreview = () => { + if (!license.document) { + return
No document attached
; + } + + if (fileType.startsWith("image/")) { + return ( +Preview not available for this file type: {fileType}
; + } + }; + + return ( +