diff --git a/server/api.ts b/server/api.ts index 95bbde9..339d8f3 100644 --- a/server/api.ts +++ b/server/api.ts @@ -1,16 +1,68 @@ import { Api } from "@src/lib/apiValidator.ts"; -import { z } from "@shared/utils/validator.ts"; +import { createValidationError, z } from "@shared/utils/validator.ts"; +import { + adminPasswordAlreadySetErrorSchema, + adminPasswordNotSetErrorSchema, + failedToParseRequestAsJSONErrorSchema, + invalidPasswordErrorSchema, + passwordsMustMatchErrorSchema, + queryExecutionErrorSchema, + requestValidationErrorSchema, + tooManyRequestsErrorSchema, +} from "@src/lib/errors.ts"; -const schema = { +const loginApiSchema = { req: z.obj({ password: z.string(), }), res: z.result( - z.obj({ - isMatch: z.boolean(), - }), - z.any(), + z.void(), + z.union([ + adminPasswordNotSetErrorSchema, + queryExecutionErrorSchema, + failedToParseRequestAsJSONErrorSchema, + requestValidationErrorSchema, + tooManyRequestsErrorSchema, + invalidPasswordErrorSchema, + ]), ), }; -export const loginApi = new Api("/login", "POST", schema); +export const loginApi = new Api("/login", "POST", loginApiSchema); + +const passwordSetupApiSchema = { + req: z.obj({ + password: z.string().min( + 10, + "Password must be at least 10 characters long", + ).regex( + /^[a-zA-Z0-9]+$/, + "Password must consist of lower or upper case latin letters and numbers", + ), + passwordRepeat: z.string(), + }).addCheck((v) => { + if (v.passwordRepeat !== v.password) { + return createValidationError(v, { + kind: "general", + msg: "Passwords must match", + }); + } + }), + res: z.result( + z.void(), + z.union([ + passwordsMustMatchErrorSchema, + adminPasswordAlreadySetErrorSchema, + queryExecutionErrorSchema, + failedToParseRequestAsJSONErrorSchema, + requestValidationErrorSchema, + tooManyRequestsErrorSchema, + ]), + ), +}; + +export const passwordSetupApi = new Api( + "/setup", + "POST", + passwordSetupApiSchema, +); diff --git a/server/deno.json b/server/deno.json index 26126b6..85fbad4 100644 --- a/server/deno.json +++ b/server/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "dev": "deno run --allow-read --allow-write --allow-sys --allow-env --allow-run ./autoBundler.ts & deno serve --allow-read --allow-write --allow-sys --allow-env --allow-ffi --watch -R main.ts" + "dev": "deno run --allow-read --allow-write --allow-sys --allow-env --allow-run ./autoBundler.ts & deno serve --allow-read --allow-write --allow-sys --allow-env --allow-ffi --watch --allow-run -R main.ts" }, "imports": { "@db/sqlite": "jsr:@db/sqlite@^0.12.0", diff --git a/server/main.ts b/server/main.ts index 0bc21b8..0c7b19e 100644 --- a/server/main.ts +++ b/server/main.ts @@ -5,21 +5,26 @@ import rateLimitMiddleware from "@src/middleware/rateLimiter.ts"; import authMiddleware from "@src/middleware/auth.ts"; import loggerMiddleware from "@src/middleware/logger.ts"; import { SchemaValidationError, z } from "@shared/utils/validator.ts"; -import { loginApi } from "./api.ts"; +import { loginApi, passwordSetupApi } from "./api.ts"; import { err, ok } from "@shared/utils/result.ts"; import admin from "@src/lib/admin.ts"; -import { - FailedToParseRequestAsJSON, - QueryExecutionError, -} from "@src/lib/errors.ts"; import { Context } from "@src/lib/context.ts"; +import { + FailedToParseRequestAsJSONError, + invalidPasswordError, + passwordsMustMatchError, + QueryExecutionError, + queryExecutionError, + RequestValidationError, +} from "@src/lib/errors.ts"; +import devices from "@src/lib/devices.ts"; const AUTH_COOKIE_NAME = "token"; const router = new HttpRouter(); const views = Deno.cwd() + "/views/"; -const eta = new Eta({ views }); +export const eta = new Eta({ views }); router.use(loggerMiddleware); router.use(rateLimitMiddleware); @@ -45,9 +50,21 @@ router.get("/public/*", async (c) => { router .get(["", "/index.html"], (c) => { + console.log(devices.list()); + return c.html(eta.render("./index.html", {})); }) .get(["/login", "/login.html"], (c) => { + const isSet = admin.isPasswordSet(); + + if (isSet.isErr()) { + return c.html(eta.render("./internal_error.html", {})); + } + + if (!isSet.value) { + return c.redirect("/setup"); + } + const alreadyLoggedIn = c.cookies.get("token").map((token) => admin.sessions.verifyToken(token) ) @@ -56,10 +73,21 @@ router console.log(alreadyLoggedIn); return c.html(eta.render("./login.html", { alreadyLoggedIn })); + }) + .get("/setup", (c) => { + return admin.isPasswordSet() + .match( + (isSet) => { + if (isSet) { + return c.redirect("/login"); + } else { + return c.html(eta.render("./setup.html", {})); + } + }, + (e) => c.html(eta.render("./internal_error.html", {})), + ); }); -admin.setPassword("Vermont5481"); - router.api(loginApi, async (c) => { const r = await c .parseBody() @@ -72,7 +100,7 @@ router.api(loginApi, async (c) => { return c.json400( err({ type: r.error.type, - msg: r.error.message, + info: r.error.info, }), ); } @@ -89,42 +117,56 @@ router.api(loginApi, async (c) => { value, expires, }); - return ok({ isMatch: true }); + return ok(); }).match( (v) => c.json(v), (e) => handleCommonErrors(c, e), ); } else { - return c.json(ok({ - isMatch: false, - })); + return c.json(err(invalidPasswordError("Invalid login or password"))); } }); +router.api(passwordSetupApi, async (c) => { + const r = await c.parseBody(); + + if (r.isErr()) { + return handleCommonErrors(c, r.error); + } + + const v = r.value; + + if (v.password !== v.passwordRepeat) { + return c.json400(err(passwordsMustMatchError("Passwords must match"))); + } + + return admin.setPassword(v.password).match( + () => c.json(ok()), + (e) => c.json400(err(e)), + ); +}); + function handleCommonErrors( c: Context, error: | QueryExecutionError - | FailedToParseRequestAsJSON - | SchemaValidationError, + | FailedToParseRequestAsJSONError + | RequestValidationError, ): Response { switch (error.type) { case "QueryExecutionError": return c.json( - err(new QueryExecutionError("Server failed to execute query")), + err(queryExecutionError("Server failed to execute query")), { status: 500 }, ); - case "FailedToParseRequestAsJSON": + case "FailedToParseRequestAsJSONError": return c.json( err(error), { status: 400 }, ); - case "SchemaValiationError": + case "RequestValidationError": return c.json( - err({ - type: "ValidationError", - msg: error.msg, - }), + err(error), { status: 400 }, ); } diff --git a/server/public/js/login.js b/server/public/js/login.js index 11fa762..4fdf68c 100644 --- a/server/public/js/login.js +++ b/server/public/js/login.js @@ -1 +1 @@ -import{loginApi as s}from"./shared.bundle.js";const o=document.getElementById("loginForm"),i=document.getElementById("passwordInput"),t=document.getElementById("errDiv");o.addEventListener("submit",async n=>{n.preventDefault();const r=i.value,e=(await s.makeRequest({password:r},{})).flatten();e.isErr()?e.error.type==="RequestValidationError"&&(t.innerText=e.error.msg):e.value.isMatch?window.location.href="/":t.innerText="invalid password"}); +import{loginApi as o}from"./shared.bundle.js";const r=document.getElementById("loginForm"),s=document.getElementById("passwordInput"),i=document.getElementById("errDiv");r.addEventListener("submit",async t=>{t.preventDefault();const n=s.value,e=(await o.makeRequest({password:n},{})).flatten();e.isErr()?i.innerText=e.error.info:window.location.href="/"}); diff --git a/server/public/js/setup.js b/server/public/js/setup.js new file mode 100644 index 0000000..c8b459d --- /dev/null +++ b/server/public/js/setup.js @@ -0,0 +1 @@ +import{passwordSetupApi as o}from"./shared.bundle.js";const r=document.getElementById("passwordSetupForm"),a=document.getElementById("passwordInput"),p=document.getElementById("passwordRepeatInput"),d=document.getElementById("errDiv");r.addEventListener("submit",async t=>{t.preventDefault();const n=a.value,s=p.value,e=(await o.makeRequest({password:n,passwordRepeat:s},{})).flatten();e.isErr()?d.innerText=e.error.info:window.location.href="/login"}); diff --git a/server/public/js/shared.bundle.js b/server/public/js/shared.bundle.js index f231c94..cfbfd2b 100644 --- a/server/public/js/shared.bundle.js +++ b/server/public/js/shared.bundle.js @@ -1 +1 @@ -var v=class t{constructor(e){this._promise=e;this._promise=e}static fromPromise(e,r){let n=e.then(s=>new c(s)).catch(s=>new f(r(s)));return new t(n)}static fromSafePromise(e){let r=e.then(n=>new c(n));return new t(r)}static fromThrowable(e,r){return(...n)=>t.fromPromise(e(n),s=>r?r(s):s)}async unwrap(){let e=await this._promise;if(e.isErr())throw e.error;return e.value}async match(e,r){let n=await this._promise;return n.isErr()?r(n.error):e(n.value)}map(e){return new t(this._promise.then(r=>r.isErr()?new f(r.error):new c(e(r.value))))}mapAsync(e){return new t(this._promise.then(async r=>r.isErr()?g(r.error):new c(await e(r.value))))}mapErr(e){return new t(this._promise.then(r=>r.isErr()?new f(e(r.error)):new c(r.value)))}mapErrAsync(e){return new t(this._promise.then(async r=>r.isErr()?g(await e(r.error)):new c(r.value)))}andThen(e){return new t(this._promise.then(r=>r.isErr()?g(r.error):e(r.value).toAsync()))}andThenAsync(e){return new t(this._promise.then(r=>r.isErr()?g(r.error):e(r.value)))}nullableToOption(){return this.map(e=>e?h(e):y)}flatten(){return new t(this._promise.then(e=>e.flatten()))}flattenOption(e){return new t(this._promise.then(r=>r.flattenOption(e)))}flattenOptionOrDefault(e){return new t(this._promise.then(r=>r.flattenOptionOrDefault(e)))}matchOption(e,r){return new t(this._promise.then(n=>n.matchOption(e,r)))}matchOptionAndFlatten(e,r){return new t(this._promise.then(n=>n.matchOptionAndFlatten(e,r)))}then(e,r){return this._promise.then(e,r)}};function O(t){return new v(Promise.resolve(new c(t)))}function g(t){return new v(Promise.resolve(new f(t)))}var c=class t{constructor(e){this.value=e;this.value=e,Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}tag="ok";isErr(){return!1}ifErr(e){return this}isErrOrNone(){return this.value instanceof T}isOk(){return!0}ifOk(e){return e(this.value),this}unwrap(){return this.value}unwrapOr(e){return this.value}unwrapOrElse(e){return this.value}unwrapErr(){return y}match(e,r){return e(this.value)}map(e){let r=e(this.value);return new t(r)}mapAsync(e){return v.fromSafePromise(e(this.value))}mapOption(e){return this.value instanceof T||this.value instanceof E?o(this.value.map(e)):o(h(e(this.value)))}andThen(e){return e(this.value)}andThenAsync(e){return e(this.value)}mapErr(e){return o(this.value)}mapErrAsync(e){return O(this.value)}flatten(){return Y(this)}flattenOption(e){return this.value instanceof T||this.value instanceof E?this.value.okOrElse(e):new t(this.value)}flattenOptionOr(e){return this.value instanceof T||this.value instanceof E?this.value.unwrapOr(e):new t(this.value)}matchOption(e,r){return this.value instanceof T||this.value instanceof E?o(this.value.match(e,r)):o(e(this.value))}toNullable(){return this.value}toAsync(){return O(this.value)}void(){return o()}toJSON(){return{tag:"ok",value:this.value}}},f=class t{constructor(e){this.error=e;this.error=e,Object.defineProperties(this,{tag:{writable:!1,configurable:!1,enumerable:!1}})}tag="err";isErr(){return!0}ifErr(e){return e(this.error),this}isOk(){return!1}ifOk(e){return this}isErrOrNone(){return!0}unwrap(){let e=`Tried to unwrap error: ${X(this.error)}`;throw new Error(e)}unwrapOr(e){return e}unwrapOrElse(e){return e()}unwrapErr(){return h(this.error)}match(e,r){return r(this.error)}map(e){return new t(this.error)}mapAsync(e){return g(this.error)}mapErr(e){return new t(e(this.error))}mapErrAsync(e){return v.fromPromise(new Promise(()=>{throw""}),()=>e(this.error))}mapOption(e){return i(this.error)}andThen(e){return new t(this.error)}andThenAsync(e){return new t(this.error).toAsync()}flatten(){return Y(this)}flattenOption(e){return new t(this.error)}flattenOptionOr(e){return new t(this.error)}matchOption(e,r){return i(this.error)}toNullable(){return null}toAsync(){return g(this.error)}void(){return i(this.error)}toJSON(){return{tag:"err",error:this.error}}};function o(t){return new c(t)}function i(t){return new f(t)}function ce(t,e){return(...r)=>{try{let n=t(...r);return o(n)}catch(n){return i(e?e(n):n)}}}function X(t){if(t instanceof Error)return t.message?t.message:"code"in t&&typeof t.code=="string"?t.code:"An unknown error occurred";if(typeof t=="string")return t;if(typeof t=="object"&&t!==null&&"message"in t){let e=t;return typeof e.message=="string"?e.message:String(e.message)}return"An unknown error occurred"}function Y(t){let e=t;for(;e instanceof c;)e=e.value;return o(e)}var k=class extends Error{constructor(e){super(`Failed to parse ${e} as result`)}};function pe(t){let e;if(typeof t=="string")try{e=JSON.parse(t)}catch(n){return i(new k(X(n)))}else e=t;if(typeof e!="object"||e===null)return i(new k("Expected an object but received type ${typeof data}."));let r=e;if("tag"in r)switch(r.tag){case"ok":{if("value"in r)return o(r.value);break}case"err":{if("error"in r)return i(r.error);break}}return i(new k("Object does not contain 'tag' and 'value' or 'error' property"))}var E=class t{constructor(e){this.value=e;Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}tag="some";isSome(){return!0}ifSome(e){return e(this.value),this}isNone(){return!1}ifNone(e){return this}map(e){return new t(e(this.value))}flatMap(e){return e(this.value)}andThen(e){return e(this.value)}unwrap(){return this.value}unwrapOr(e){return this.value}unwrapOrElse(e){return this.value}or(e){return this}orElse(e){return this}match(e,r){return e(this.value)}toString(){return`Some(${this.value})`}toJSON(){return{tag:"some",value:this.value}}toNullable(){return this.value}toBoolean(){return!0}okOrElse(e){return o(this.value)}},T=class t{tag="none";constructor(){Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}isSome(){return!1}ifSome(e){return this}isNone(){return!0}ifNone(e){return e(),this}map(e){return new t}andThen(e){return y}flatMap(e){return new t}unwrap(){throw new Error("Tried to unwrap a non-existent value")}unwrapOr(e){return e}unwrapOrElse(e){return e()}or(e){return e}orElse(e){return e()}match(e,r){return r()}toString(){return"None"}toJSON(){return{tag:"none"}}toNullable(){return null}toBoolean(){return!1}okOrElse(e){return i(e())}};function h(t){return new E(t)}var y=new T;function Te(t){return t?h(t):y}var R=class t extends Error{constructor(r,n){super(t.getBestMsg(n)||"Schema validation error");this.input=r;this.detail=n;this.name="SchemaValidationError"}type="SchemaValiationError";format(){return{input:this.input,error:t.formatDetail(this.detail)}}get msg(){return t.getBestMsg(this.detail)}static getBestMsg(r){switch(r.kind){case"typeMismatch":case"unexpectedProperties":case"missingProperties":case"general":case"unionValidation":return t.formatMsg(r);case"propertyValidation":case"arrayElement":return r.msg||t.getBestMsg(r.detail);default:return"Unknown error"}}static formatDetail(r){switch(r.kind){case"general":case"typeMismatch":return t.formatMsg(r);case"propertyValidation":return{[r.property]:r.msg||this.formatDetail(r.detail)};case"unexpectedProperties":case"missingProperties":{let n=r.msg||(r.kind==="unexpectedProperties"?"Property is not allowed in a strict schema object":"Property is required, but missing");return r.keys.reduce((s,l)=>(s[l]=n,s),{})}case"arrayElement":{let n={};return r.msg&&(n.msg=r.msg),n[`index_${r.index}`]=this.formatDetail(r.detail),n}case"unionValidation":{let n=r.details?.map(s=>this.formatDetail(s));return r.msg&&n.unshift("Msg: "+r.msg),n}default:return"Unknown error type"}}static formatMsg(r){if(r.msg||r.kind==="general")return r.msg||"Unknown error";switch(r.kind){case"typeMismatch":return`Expected ${r.expected}, but received ${r.received}`;case"unexpectedProperties":return`Properties not allowed: ${r.keys.join(", ")}`;case"missingProperties":return`Missing required properties: ${r.keys.join(", ")}`;case"unionValidation":return"Input did not match any union member";default:return"Unknown error"}}};function u(t,e){return new R(t,e)}var a=class{constructor(e){this.msg=e}checks=[];parse(e){return this.validateInput(e).andThen(r=>this.applyValidationChecks(r))}static validatePrimitive(e,r,n){let s=typeof e;return s===r?o(e):i(u(e,{kind:"typeMismatch",expected:r,received:s,msg:n}))}addCheck(e){return this.checks.push(e),this}applyValidationChecks(e){for(let r of this.checks){let n=r(e);if(n)return i(n)}return o(e)}static isNullishSchema(e){return e.parse(null).isOk()||e.parse(void 0).isOk()}},A=class t extends a{static emailRegex=/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;static ipRegex=/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;validateInput(e){return a.validatePrimitive(e,"string",this.msg)}max(e,r){return this.addCheck(n=>{if(n.length>e)return u(n,{kind:"general",msg:r||`String must be at most ${e} characters long`})})}min(e,r){return this.addCheck(n=>{if(n.length{if(!e.test(n))return u(n,{kind:"general",msg:r||`String must match pattern ${String(e)}`})})}email(e){return this.regex(t.emailRegex,e||"String must be a valid email address")}ip(e){return this.regex(t.ipRegex,e||"String must be a valid ip address")}},P=class extends a{constructor(r,n){super(n);this.literal=r}validateInput(r){return a.validatePrimitive(r,"string",this.msg).andThen(n=>n===this.literal?o(n):i(u(r,{kind:"typeMismatch",expected:this.literal,received:n,msg:this.msg})))}},V=class extends a{validateInput(e){return a.validatePrimitive(e,"number",this.msg)}gt(e,r){return this.addCheck(n=>{if(n<=e)return u(n,{kind:"general",msg:r||`Number must be greater than ${e}`})})}gte(e,r){return this.addCheck(n=>{if(n{if(n>=e)return u(n,{kind:"general",msg:r||`Number must be less than ${e}`})})}lte(e,r){return this.addCheck(n=>{if(n>e)return u(n,{kind:"general",msg:r||`Number must be less than or equal to ${e}`})})}int(e){return this.addCheck(r=>{if(!Number.isInteger(r))return u(r,{kind:"general",msg:e||"Number must be an integer"})})}positive(e){return this.gt(0,e||"Number must be positive")}nonnegative(e){return this.gte(0,e||"Number must be nonnegative")}negative(e){return this.lt(0,e||"Number must be negative")}nonpositive(e){return this.lte(0,e||"Number must be nonpositive")}finite(e){return this.addCheck(r=>{if(!Number.isFinite(r))return u(r,{kind:"general",msg:e||"Number must be an integer"})})}safe(e){return this.addCheck(r=>{if(!Number.isSafeInteger(r))return u(r,{kind:"general",msg:e||"Number must be an integer"})})}multipleOf(e,r){return this.addCheck(n=>{if(n%e!==0)return u(n,{kind:"general",msg:r||`Number must be a multiple of ${e}`})})}},I=class extends a{validateInput(e){return a.validatePrimitive(e,"bigint",this.msg)}},F=class extends a{validateInput(e){return a.validatePrimitive(e,"boolean",this.msg)}},N=class extends a{validateInput(e){return a.validatePrimitive(e,"object",this.msg).andThen(r=>{if(r instanceof Date)return o(r);let n=r?.constructor?.name??"unknown";return i(u(e,{kind:"typeMismatch",expected:"Date instance",received:n,msg:this.msg}))})}},M=class extends a{validateInput(e){return a.validatePrimitive(e,"symbol",this.msg)}},B=class extends a{validateInput(e){return a.validatePrimitive(e,"undefined",this.msg)}},j=class extends a{validateInput(e){if(e===null)return o(e);let r=typeof e=="object"?e?.constructor?.name??"unknown":typeof e;return i(u(e,{kind:"typeMismatch",expected:"null",received:r,msg:this.msg}))}},D=class extends a{validateInput(e){if(e==null)return o();let r=typeof e=="object"?e?.constructor?.name??"unknown":typeof e;return i(u(e,{kind:"typeMismatch",expected:"void (undefined/null)",received:r,msg:this.msg}))}},$=class extends a{validateInput(e){return o(e)}},C=class extends a{validateInput(e){return o(e)}},q=class extends a{validateInput(e){return i(u(e,{kind:"typeMismatch",expected:"never",received:typeof e,msg:this.msg}))}},J=class extends a{constructor(r,n){let s,l;typeof n=="string"?s=n:typeof n=="object"&&(l=n);super(s);this.shape=r;this.objectMsg=l}strictMode=!1;objectMsg;validateInput(r){return a.validatePrimitive(r,"object",this.msg||this.objectMsg?.mismatch).andThen(n=>{if(n===null)return i(u(r,{kind:"typeMismatch",expected:"Non-null object",received:"null",msg:this.msg||this.objectMsg?.nullObject}));let s={},l=new Set(Object.keys(this.shape));for(let m of Object.keys(n)){let d=this.shape[m];if(d===void 0){if(this.strictMode){let Z=new Set(Object.keys(n)).difference(new Set(Object.keys(this.shape))).keys().toArray();return i(u(r,{kind:"unexpectedProperties",keys:Z,msg:this.msg||this.objectMsg?.unexpectedProperty}))}continue}let w=d.parse(n[m]);if(w.isErr())return i(u(r,{kind:"propertyValidation",property:m,detail:w.error.detail,msg:this.msg||this.objectMsg?.propertyValidation}));l.delete(m),s[m]=w.value}let p=l.keys().filter(m=>!a.isNullishSchema(this.shape[m])).toArray();return p.length>0?i(u(r,{kind:"missingProperties",keys:p,msg:this.msg||this.objectMsg?.missingProperty})):o(s)})}strict(){return this.strictMode=!0,this}},L=class t extends a{constructor(r,n){let s,l;typeof n=="string"?s=n:typeof n=="object"&&(l=n);super(s);this.schemas=r;this.unionMsg=l}unionMsg;static getTypeFromSchemaName(r){switch(r){case"StringSchema":case"LiteralSchema":return"string";case"NumberSchema":return"number";case"BigintSchema":return"bigint";case"BooleanSchema":return"boolean";case"UndefinedSchema":return"undefined";case"SymbolSchema":return"symbol";default:return"object"}}validateInput(r){let n=[],s=!0;for(let l of this.schemas){let p=l.parse(r);if(p.isOk())return o(p.value);s=p.error.detail?.kind==="typeMismatch"&&s,n.push(p.error.detail)}return s?i(u(r,{kind:"typeMismatch",expected:this.schemas.map(l=>t.getTypeFromSchemaName(l.constructor.name)).join(" | "),received:typeof r,msg:this.msg||this.unionMsg?.mismatch})):i(u(r,{kind:"unionValidation",msg:this.msg||this.unionMsg?.unionValidation||"Input did not match any union member",details:n}))}},K=class extends a{constructor(r,n){let s,l;typeof n=="string"?s=n:typeof n=="object"&&(l=n);super(s);this.schema=r;this.arrayMsg=l}arrayMsg;validateInput(r){if(!Array.isArray(r))return i(u(r,{kind:"typeMismatch",expected:"Array",received:"Non-array",msg:this.msg||this.arrayMsg?.mismatch}));for(let n=0;n{if("tag"in n)switch(n.tag){case"ok":return"value"in n?this.okSchema.parse(n.value).match(s=>o(o(s)),s=>i(u(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):a.isNullishSchema(this.okSchema)?o(o()):i(u(r,{kind:"missingProperties",keys:["value"],msg:"If tag is set to 'ok', than result must contain a 'value' property"}));case"err":return"error"in n?this.errSchema.parse(n.error).match(s=>o(i(s)),s=>i(u(r,{kind:"propertyValidation",property:"error",detail:s.detail}))):a.isNullishSchema(this.errSchema)?o(i()):i(u(r,{kind:"missingProperties",keys:["error"],msg:"If tag is set to 'err', than result must contain a 'error' property"}));default:return i(u(r,{kind:"propertyValidation",property:"tag",detail:{kind:"typeMismatch",expected:"'ok' or 'err'",received:`'${n.tag}'`}}))}else return i(u(r,{kind:"missingProperties",keys:["tag"],msg:"Result must contain a tag property"}))})}},Q=class extends a{constructor(r){super();this.schema=r}validateInput(r){return a.validatePrimitive(r,"object").andThen(n=>{if("tag"in n)switch(n.tag){case"some":return"value"in n?this.schema.parse(n.value).match(s=>o(h(s)),s=>i(u(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):a.isNullishSchema(this.schema)?o(h()):i(u(r,{kind:"missingProperties",keys:["value"],msg:"If tag is set to 'some', than option must contain a 'value' property"}));case"none":return o(y);default:return i(u(r,{kind:"propertyValidation",property:"tag",detail:{kind:"typeMismatch",expected:"'some' or 'none'",received:`'${n.tag}'`}}))}else return i(u(r,{kind:"missingProperties",keys:["tag"],msg:"Option must contain a tag property"}))})}},U={string:t=>new A(t),literal:(t,e)=>new P(t,e),number:t=>new V(t),bigint:t=>new I(t),boolean:t=>new F(t),date:t=>new N(t),symbol:t=>new M(t),undefined:t=>new B(t),null:t=>new j(t),void:t=>new D(t),any:t=>new $(t),unknown:t=>new C(t),never:t=>new q(t),obj:(t,e)=>new J(t,e),union:(t,e)=>new L(t,e),array:(t,e)=>new K(t,e),optional:(t,e)=>new z(t,e),nullable:(t,e)=>new W(t,e),nullish:(t,e)=>new G(t,e),result:(t,e)=>new H(t,e),option:t=>new Q(t)};var S=class extends R{type="RequestValidationError";constructor(e,r){super(e,r)}},x=class extends R{type="ResponseValidationError";constructor(e,r){super(e,r)}};var b=class{constructor(e,r,n){this.path=e;this.method=r;this.schema=n;this.pathSplitted=e.split("/"),this.paramIndexes=this.pathSplitted.reduce((s,l,p)=>(l.startsWith(":")&&(s[l.slice(1)]=p),s),{})}pathSplitted;paramIndexes;makeRequest(e,r){return this.schema.req.parse(e).toAsync().mapErr(n=>new S(n.input,n.detail)).andThenAsync(async n=>{let s=this.pathSplitted;for(let[d,w]of Object.entries(r))s[this.paramIndexes[d]]=w;let l=s.join("/"),m=await(await fetch(l,{method:this.method,headers:{"Content-Type":"application/json"},body:JSON.stringify(n)})).json();return this.schema.res.parse(m).toAsync().map(d=>d).mapErr(d=>new x(d.input,d.detail))})}makeSafeRequest(e,r){return this.makeRequest(e,r).mapErr(n=>{if(n.type==="RequestValidationError")throw"Failed to validate request";return n})}};var _={req:U.obj({password:U.string()}),res:U.result(U.obj({isMatch:U.boolean()}),U.any())},Be=new b("/login","POST",_);export{a as BaseSchema,f as Err,T as None,c as Ok,Q as OptionSchema,v as ResultAsync,pe as ResultFromJSON,H as ResultSchema,R as SchemaValidationError,E as Some,i as err,g as errAsync,Y as flattenResult,Te as fromNullableVal,ce as fromThrowable,X as getMessageFromError,Be as loginApi,y as none,o as ok,O as okAsync,h as some,U as z}; +var U=class t{constructor(e){this._promise=e;this._promise=e}static fromPromise(e,r){let n=e.then(s=>new m(s)).catch(s=>new f(r(s)));return new t(n)}static fromSafePromise(e){let r=e.then(n=>new m(n));return new t(r)}static fromThrowable(e,r){return(...n)=>t.fromPromise(e(n),s=>r?r(s):s)}async unwrap(){let e=await this._promise;if(e.isErr())throw e.error;return e.value}async match(e,r){let n=await this._promise;return n.isErr()?r(n.error):e(n.value)}map(e){return new t(this._promise.then(r=>r.isErr()?new f(r.error):new m(e(r.value))))}mapAsync(e){return new t(this._promise.then(async r=>r.isErr()?R(r.error):new m(await e(r.value))))}mapErr(e){return new t(this._promise.then(r=>r.isErr()?new f(e(r.error)):new m(r.value)))}mapErrAsync(e){return new t(this._promise.then(async r=>r.isErr()?R(await e(r.error)):new m(r.value)))}andThen(e){return new t(this._promise.then(r=>r.isErr()?R(r.error):e(r.value).toAsync()))}andThenAsync(e){return new t(this._promise.then(r=>r.isErr()?R(r.error):e(r.value)))}nullableToOption(){return this.map(e=>e?y(e):v)}flatten(){return new t(this._promise.then(e=>e.flatten()))}flattenOption(e){return new t(this._promise.then(r=>r.flattenOption(e)))}flattenOptionOrDefault(e){return new t(this._promise.then(r=>r.flattenOptionOrDefault(e)))}matchOption(e,r){return new t(this._promise.then(n=>n.matchOption(e,r)))}matchOptionAndFlatten(e,r){return new t(this._promise.then(n=>n.matchOptionAndFlatten(e,r)))}then(e,r){return this._promise.then(e,r)}};function I(t){return new U(Promise.resolve(new m(t)))}function R(t){return new U(Promise.resolve(new f(t)))}var m=class t{constructor(e){this.value=e;this.value=e,Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}tag="ok";isErr(){return!1}ifErr(e){return this}isErrOrNone(){return this.value instanceof T}isOk(){return!0}ifOk(e){return e(this.value),this}unwrap(){return this.value}unwrapOr(e){return this.value}unwrapOrElse(e){return this.value}unwrapErr(){return v}match(e,r){return e(this.value)}map(e){let r=e(this.value);return new t(r)}mapAsync(e){return U.fromSafePromise(e(this.value))}mapOption(e){return this.value instanceof T||this.value instanceof S?u(this.value.map(e)):u(y(e(this.value)))}andThen(e){return e(this.value)}andThenAsync(e){return e(this.value)}mapErr(e){return u(this.value)}mapErrAsync(e){return I(this.value)}flatten(){return ie(this)}flattenOption(e){return this.value instanceof T||this.value instanceof S?this.value.okOrElse(e):new t(this.value)}flattenOptionOr(e){return this.value instanceof T||this.value instanceof S?this.value.unwrapOr(e):new t(this.value)}matchOption(e,r){return this.value instanceof T||this.value instanceof S?u(this.value.match(e,r)):u(e(this.value))}toNullable(){return this.value}toAsync(){return I(this.value)}void(){return u()}toJSON(){return{tag:"ok",value:this.value}}},f=class t{constructor(e){this.error=e;this.error=e,Object.defineProperties(this,{tag:{writable:!1,configurable:!1,enumerable:!1}})}tag="err";isErr(){return!0}ifErr(e){return e(this.error),this}isOk(){return!1}ifOk(e){return this}isErrOrNone(){return!0}unwrap(){let e=`Tried to unwrap error: ${se(this.error)}`;throw new Error(e)}unwrapOr(e){return e}unwrapOrElse(e){return e()}unwrapErr(){return y(this.error)}match(e,r){return r(this.error)}map(e){return new t(this.error)}mapAsync(e){return R(this.error)}mapErr(e){return new t(e(this.error))}mapErrAsync(e){return U.fromPromise(new Promise(()=>{throw""}),()=>e(this.error))}mapOption(e){return i(this.error)}andThen(e){return new t(this.error)}andThenAsync(e){return new t(this.error).toAsync()}flatten(){return ie(this)}flattenOption(e){return new t(this.error)}flattenOptionOr(e){return new t(this.error)}matchOption(e,r){return i(this.error)}toNullable(){return null}toAsync(){return R(this.error)}void(){return i(this.error)}toJSON(){return{tag:"err",error:this.error}}};function u(t){return new m(t)}function i(t){return new f(t)}function Oe(t,e){return(...r)=>{try{let n=t(...r);return u(n)}catch(n){return i(e?e(n):n)}}}function se(t){if(t instanceof Error)return t.message?t.message:"code"in t&&typeof t.code=="string"?t.code:"An unknown error occurred";if(typeof t=="string")return t;if(typeof t=="object"&&t!==null&&"message"in t){let e=t;return typeof e.message=="string"?e.message:String(e.message)}return"An unknown error occurred"}function ie(t){let e=t;for(;e instanceof m&&(e.value instanceof m||e.value instanceof f);)e=e.value;return e}var x=class extends Error{constructor(e){super(`Failed to parse ${e} as result`)}};function be(t){let e;if(typeof t=="string")try{e=JSON.parse(t)}catch(n){return i(new x(se(n)))}else e=t;if(typeof e!="object"||e===null)return i(new x("Expected an object but received type ${typeof data}."));let r=e;if("tag"in r)switch(r.tag){case"ok":{if("value"in r)return u(r.value);break}case"err":{if("error"in r)return i(r.error);break}}return i(new x("Object does not contain 'tag' and 'value' or 'error' property"))}var S=class t{constructor(e){this.value=e;Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}tag="some";isSome(){return!0}ifSome(e){return e(this.value),this}isNone(){return!1}ifNone(e){return this}map(e){return new t(e(this.value))}flatMap(e){return e(this.value)}andThen(e){return e(this.value)}unwrap(){return this.value}unwrapOr(e){return this.value}unwrapOrElse(e){return this.value}or(e){return this}orElse(e){return this}match(e,r){return e(this.value)}toString(){return`Some(${this.value})`}toJSON(){return{tag:"some",value:this.value}}toNullable(){return this.value}toBoolean(){return!0}okOrElse(e){return u(this.value)}},T=class t{tag="none";constructor(){Object.defineProperties(this,{tag:{writable:!1,enumerable:!1}})}isSome(){return!1}ifSome(e){return this}isNone(){return!0}ifNone(e){return e(),this}map(e){return new t}andThen(e){return v}flatMap(e){return new t}unwrap(){throw new Error("Tried to unwrap a non-existent value")}unwrapOr(e){return e}unwrapOrElse(e){return e()}or(e){return e}orElse(e){return e()}match(e,r){return r()}toString(){return"None"}toJSON(){return{tag:"none"}}toNullable(){return null}toBoolean(){return!1}okOrElse(e){return i(e())}};function y(t){return new S(t)}var v=new T;function Ve(t){return t?y(t):v}var V=class t extends Error{constructor(r,n){super(t.getBestMsg(n)||"Schema validation error");this.input=r;this.detail=n;this.name="SchemaValidationError"}type="SchemaValiationError";format(){return{input:this.input,error:t.formatDetail(this.detail)}}get info(){return t.getBestMsg(this.detail)}static getBestMsg(r){switch(r.kind){case"typeMismatch":case"unexpectedProperties":case"missingProperties":case"general":case"unionValidation":return t.formatMsg(r);case"propertyValidation":case"arrayElement":return r.msg||t.getBestMsg(r.detail);default:return"Unknown error"}}static formatDetail(r){switch(r.kind){case"general":case"typeMismatch":return t.formatMsg(r);case"propertyValidation":return{[r.property]:r.msg||this.formatDetail(r.detail)};case"unexpectedProperties":case"missingProperties":{let n=r.msg||(r.kind==="unexpectedProperties"?"Property is not allowed in a strict schema object":"Property is required, but missing");return r.keys.reduce((s,l)=>(s[l]=n,s),{})}case"arrayElement":{let n={};return r.msg&&(n.msg=r.msg),n[`index_${r.index}`]=this.formatDetail(r.detail),n}case"unionValidation":{let n=r.details?.map(s=>this.formatDetail(s));return r.msg&&n.unshift("Msg: "+r.msg),n}default:return"Unknown error type"}}static formatMsg(r){if(r.msg||r.kind==="general")return r.msg||"Unknown error";switch(r.kind){case"typeMismatch":return`Expected ${r.expected}, but received ${r.received}`;case"unexpectedProperties":return`Properties not allowed: ${r.keys.join(", ")}`;case"missingProperties":return`Missing required properties: ${r.keys.join(", ")}`;case"unionValidation":return"Input did not match any union member";default:return"Unknown error"}}};function o(t,e){return new V(t,e)}var a=class{constructor(e){this.msg=e}checks=[];parse(e){return this.validateInput(e).andThen(r=>this.applyValidationChecks(r))}static validatePrimitive(e,r,n){let s=typeof e;return s===r?u(e):i(o(e,{kind:"typeMismatch",expected:r,received:s,msg:n}))}addCheck(e){return this.checks.push(e),this}applyValidationChecks(e){for(let r of this.checks){let n=r(e);if(n)return i(n)}return u(e)}static isNullishSchema(e){return e.parse(null).isOk()||e.parse(void 0).isOk()}},F=class t extends a{static emailRegex=/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;static ipRegex=/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;validateInput(e){return a.validatePrimitive(e,"string",this.msg)}max(e,r){return this.addCheck(n=>{if(n.length>e)return o(n,{kind:"general",msg:r||`String must be at most ${e} characters long`})})}min(e,r){return this.addCheck(n=>{if(n.length{if(!e.test(n))return o(n,{kind:"general",msg:r||`String must match pattern ${String(e)}`})})}email(e){return this.regex(t.emailRegex,e||"String must be a valid email address")}ip(e){return this.regex(t.ipRegex,e||"String must be a valid ip address")}},N=class extends a{constructor(r,n){super(n);this.literal=r}validateInput(r){return a.validatePrimitive(r,"string",this.msg).andThen(n=>n===this.literal?u(n):i(o(r,{kind:"typeMismatch",expected:this.literal,received:n,msg:this.msg})))}},M=class extends a{validateInput(e){return a.validatePrimitive(e,"number",this.msg)}gt(e,r){return this.addCheck(n=>{if(n<=e)return o(n,{kind:"general",msg:r||`Number must be greater than ${e}`})})}gte(e,r){return this.addCheck(n=>{if(n{if(n>=e)return o(n,{kind:"general",msg:r||`Number must be less than ${e}`})})}lte(e,r){return this.addCheck(n=>{if(n>e)return o(n,{kind:"general",msg:r||`Number must be less than or equal to ${e}`})})}int(e){return this.addCheck(r=>{if(!Number.isInteger(r))return o(r,{kind:"general",msg:e||"Number must be an integer"})})}positive(e){return this.gt(0,e||"Number must be positive")}nonnegative(e){return this.gte(0,e||"Number must be nonnegative")}negative(e){return this.lt(0,e||"Number must be negative")}nonpositive(e){return this.lte(0,e||"Number must be nonpositive")}finite(e){return this.addCheck(r=>{if(!Number.isFinite(r))return o(r,{kind:"general",msg:e||"Number must be an integer"})})}safe(e){return this.addCheck(r=>{if(!Number.isSafeInteger(r))return o(r,{kind:"general",msg:e||"Number must be an integer"})})}multipleOf(e,r){return this.addCheck(n=>{if(n%e!==0)return o(n,{kind:"general",msg:r||`Number must be a multiple of ${e}`})})}},j=class extends a{validateInput(e){return a.validatePrimitive(e,"bigint",this.msg)}},B=class extends a{validateInput(e){return a.validatePrimitive(e,"boolean",this.msg)}},q=class extends a{validateInput(e){return a.validatePrimitive(e,"object",this.msg).andThen(r=>{if(r instanceof Date)return u(r);let n=r?.constructor?.name??"unknown";return i(o(e,{kind:"typeMismatch",expected:"Date instance",received:n,msg:this.msg}))})}},D=class extends a{validateInput(e){return a.validatePrimitive(e,"symbol",this.msg)}},$=class extends a{validateInput(e){return a.validatePrimitive(e,"undefined",this.msg)}},J=class extends a{validateInput(e){if(e===null)return u(e);let r=typeof e=="object"?e?.constructor?.name??"unknown":typeof e;return i(o(e,{kind:"typeMismatch",expected:"null",received:r,msg:this.msg}))}},C=class extends a{validateInput(e){if(e==null)return u();let r=typeof e=="object"?e?.constructor?.name??"unknown":typeof e;return i(o(e,{kind:"typeMismatch",expected:"void (undefined/null)",received:r,msg:this.msg}))}},K=class extends a{validateInput(e){return u(e)}},L=class extends a{validateInput(e){return u(e)}},z=class extends a{validateInput(e){return i(o(e,{kind:"typeMismatch",expected:"never",received:typeof e,msg:this.msg}))}},Q=class extends a{constructor(r,n){let s,l;typeof n=="string"?s=n:typeof n=="object"&&(l=n);super(s);this.shape=r;this.objectMsg=l}strictMode=!1;objectMsg;validateInput(r){return a.validatePrimitive(r,"object",this.msg||this.objectMsg?.mismatch).andThen(n=>{if(n===null)return i(o(r,{kind:"typeMismatch",expected:"Non-null object",received:"null",msg:this.msg||this.objectMsg?.nullObject}));let s={},l=new Set(Object.keys(this.shape));for(let E of Object.keys(n)){let g=this.shape[E];if(g===void 0){if(this.strictMode){let ue=new Set(Object.keys(n)).difference(new Set(Object.keys(this.shape))).keys().toArray();return i(o(r,{kind:"unexpectedProperties",keys:ue,msg:this.msg||this.objectMsg?.unexpectedProperty}))}continue}let w=g.parse(n[E]);if(w.isErr())return i(o(r,{kind:"propertyValidation",property:E,detail:w.error.detail,msg:this.msg||this.objectMsg?.propertyValidation}));l.delete(E),s[E]=w.value}let h=l.keys().filter(E=>!a.isNullishSchema(this.shape[E])).toArray();return h.length>0?i(o(r,{kind:"missingProperties",keys:h,msg:this.msg||this.objectMsg?.missingProperty})):u(s)})}strict(){return this.strictMode=!0,this}pick(r){let n={};for(let s of Object.keys(r))r[s]&&(n[s]=this.shape[s]);return c.obj(n)}},W=class t extends a{constructor(r,n){let s,l;typeof n=="string"?s=n:typeof n=="object"&&(l=n);super(s);this.schemas=r;this.unionMsg=l}unionMsg;static getTypeFromSchemaName(r){switch(r){case"StringSchema":case"LiteralSchema":return"string";case"NumberSchema":return"number";case"BigintSchema":return"bigint";case"BooleanSchema":return"boolean";case"UndefinedSchema":return"undefined";case"SymbolSchema":return"symbol";default:return"object"}}validateInput(r){let n=[],s=!0;for(let l of this.schemas){let h=l.parse(r);if(h.isOk())return u(h.value);s=h.error.detail?.kind==="typeMismatch"&&s,n.push(h.error.detail)}return s?i(o(r,{kind:"typeMismatch",expected:this.schemas.map(l=>t.getTypeFromSchemaName(l.constructor.name)).join(" | "),received:typeof r,msg:this.msg||this.unionMsg?.mismatch})):i(o(r,{kind:"unionValidation",msg:this.msg||this.unionMsg?.unionValidation||"Input did not match any union member",details:n}))}},Z=class extends a{constructor(r,n){let s,l;typeof n=="string"?s=n:typeof n=="object"&&(l=n);super(s);this.schema=r;this.arrayMsg=l}arrayMsg;validateInput(r){if(!Array.isArray(r))return i(o(r,{kind:"typeMismatch",expected:"Array",received:"Non-array",msg:this.msg||this.arrayMsg?.mismatch}));for(let n=0;n{if("tag"in n)switch(n.tag){case"ok":return"value"in n?this.okSchema.parse(n.value).match(s=>u(u(s)),s=>i(o(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):a.isNullishSchema(this.okSchema)?u(u()):i(o(r,{kind:"missingProperties",keys:["value"],msg:"If tag is set to 'ok', than result must contain a 'value' property"}));case"err":return"error"in n?this.errSchema.parse(n.error).match(s=>u(i(s)),s=>i(o(r,{kind:"propertyValidation",property:"error",detail:s.detail}))):a.isNullishSchema(this.errSchema)?u(i()):i(o(r,{kind:"missingProperties",keys:["error"],msg:"If tag is set to 'err', than result must contain a 'error' property"}));default:return i(o(r,{kind:"propertyValidation",property:"tag",detail:{kind:"typeMismatch",expected:"'ok' or 'err'",received:`'${n.tag}'`}}))}else return i(o(r,{kind:"missingProperties",keys:["tag"],msg:"Result must contain a tag property"}))})}},_=class extends a{constructor(r){super();this.schema=r}validateInput(r){return a.validatePrimitive(r,"object").andThen(n=>{if("tag"in n)switch(n.tag){case"some":return"value"in n?this.schema.parse(n.value).match(s=>u(y(s)),s=>i(o(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):a.isNullishSchema(this.schema)?u(y()):i(o(r,{kind:"missingProperties",keys:["value"],msg:"If tag is set to 'some', than option must contain a 'value' property"}));case"none":return u(v);default:return i(o(r,{kind:"propertyValidation",property:"tag",detail:{kind:"typeMismatch",expected:"'some' or 'none'",received:`'${n.tag}'`}}))}else return i(o(r,{kind:"missingProperties",keys:["tag"],msg:"Option must contain a tag property"}))})}},c={string:t=>new F(t),literal:(t,e)=>new N(t,e),number:t=>new M(t),bigint:t=>new j(t),boolean:t=>new B(t),date:t=>new q(t),symbol:t=>new D(t),undefined:t=>new $(t),null:t=>new J(t),void:t=>new C(t),any:t=>new K(t),unknown:t=>new L(t),never:t=>new z(t),obj:(t,e)=>new Q(t,e),union:(t,e)=>new W(t,e),array:(t,e)=>new Z(t,e),optional:(t,e)=>new G(t,e),nullable:(t,e)=>new H(t,e),nullish:(t,e)=>new X(t,e),result:(t,e)=>new Y(t,e),option:t=>new _(t)};function p(t,e){return c.obj({type:c.literal(t),info:e??c.string()})}function d(t){return e=>({type:t.shape.type.literal,info:e})}var O=p("QueryExecutionError"),er=d(O),le=p("NoAdminEntryError"),rr=d(le),ce=p("FailedToReadFileError"),tr=d(ce),pe=p("InvalidSyntaxError"),nr=d(pe),de=p("InvalidPathError"),sr=d(de),ee=p("AdminPasswordNotSetError"),ir=d(ee),b=p("RequestValidationError"),ae=d(b),me=p("ResponseValidationError"),oe=d(me),A=p("FailedToParseRequestAsJSONError"),ar=d(A),P=p("TooManyRequestsError"),or=d(P),he=p("UnauthorizedError"),ur=d(he),re=p("InvalidPasswordError"),lr=d(re),te=p("AdminPasswordAlreadySetError"),cr=d(te),ne=p("PasswordsMustMatchError"),pr=d(ne);var k=class{constructor(e,r,n){this.path=e;this.method=r;this.schema=n;this.pathSplitted=e.split("/"),this.paramIndexes=this.pathSplitted.reduce((s,l,h)=>(l.startsWith(":")&&(s[l.slice(1)]=h),s),{})}pathSplitted;paramIndexes;makeRequest(e,r){return this.schema.req.parse(e).toAsync().mapErr(n=>ae(n.info)).andThenAsync(async n=>{let s=this.pathSplitted;for(let[g,w]of Object.entries(r))s[this.paramIndexes[g]]=w;let l=s.join("/"),E=await(await fetch(l,{method:this.method,headers:{"Content-Type":"application/json",Accept:"application/json; charset=utf-8"},body:JSON.stringify(n)})).json();return this.schema.res.parse(E).toAsync().map(g=>g).mapErr(g=>oe(g.info))})}makeSafeRequest(e,r){return this.makeRequest(e,r).mapErr(n=>{if(n.type==="RequestValidationError")throw"Failed to validate request";return n})}};var Ee={req:c.obj({password:c.string()}),res:c.result(c.void(),c.union([ee,O,A,b,P,re]))},vr=new k("/login","POST",Ee),fe={req:c.obj({password:c.string().min(10,"Password must be at least 10 characters long").regex(/^[a-zA-Z0-9]+$/,"Password must consist of lower or upper case latin letters and numbers"),passwordRepeat:c.string()}).addCheck(t=>{if(t.passwordRepeat!==t.password)return o(t,{kind:"general",msg:"Passwords must match"})}),res:c.result(c.void(),c.union([ne,te,O,A,b,P]))},Sr=new k("/setup","POST",fe);export{a as BaseSchema,f as Err,N as LiteralSchema,T as None,Q as ObjectSchema,m as Ok,_ as OptionSchema,U as ResultAsync,be as ResultFromJSON,Y as ResultSchema,V as SchemaValidationError,S as Some,F as StringSchema,o as createValidationError,i as err,R as errAsync,ie as flattenResult,Ve as fromNullableVal,Oe as fromThrowable,se as getMessageFromError,vr as loginApi,v as none,u as ok,I as okAsync,Sr as passwordSetupApi,y as some,c as z}; diff --git a/server/src/js/login.ts b/server/src/js/login.ts index 0875f61..4919719 100644 --- a/server/src/js/login.ts +++ b/server/src/js/login.ts @@ -16,14 +16,8 @@ form.addEventListener("submit", async (e) => { const res = (await loginApi.makeRequest({ password }, {})).flatten(); if (res.isErr()) { - if (res.error.type === "RequestValidationError") { - errDiv.innerText = res.error.msg; - } + errDiv.innerText = res.error.info; } else { - if (!res.value.isMatch) { - errDiv.innerText = "invalid password"; - } else { - window.location.href = "/"; - } + window.location.href = "/"; } }); diff --git a/server/src/js/setup.ts b/server/src/js/setup.ts new file mode 100644 index 0000000..53574db --- /dev/null +++ b/server/src/js/setup.ts @@ -0,0 +1,29 @@ +/// + +import { passwordSetupApi } from "./shared.bundle.ts"; + +const form = document.getElementById("passwordSetupForm") as HTMLFormElement; +const passwordInput = document.getElementById( + "passwordInput", +) as HTMLInputElement; +const passwordRepeatInput = document.getElementById( + "passwordRepeatInput", +) as HTMLInputElement; +const errDiv = document.getElementById("errDiv") as HTMLDivElement; + +form.addEventListener("submit", async (e) => { + e.preventDefault(); + + const password = passwordInput.value; + const passwordRepeat = passwordRepeatInput.value; + + const res = + (await passwordSetupApi.makeRequest({ password, passwordRepeat }, {})) + .flatten(); + + if (res.isErr()) { + errDiv.innerText = res.error.info; + } else { + window.location.href = "/login"; + } +}); diff --git a/server/src/lib/admin.ts b/server/src/lib/admin.ts index 39ac111..2f3b370 100644 --- a/server/src/lib/admin.ts +++ b/server/src/lib/admin.ts @@ -1,7 +1,11 @@ import { Option, some } from "@shared/utils/option.ts"; import db from "@lib/db/index.ts"; import { ok, Result } from "@shared/utils/result.ts"; -import { AdminPasswordNotSetError, QueryExecutionError } from "@lib/errors.ts"; +import { + AdminPasswordNotSetError, + adminPasswordNotSetError, + QueryExecutionError, +} from "@lib/errors.ts"; import { AdminRaw, AdminSessionRaw } from "@lib/db/types/index.ts"; import { generateRandomString, passwd } from "@lib/utils.ts"; import { errAsync, ResultAsync } from "@shared/utils/resultasync.ts"; @@ -52,7 +56,7 @@ class Admin { const result = this.getPasswordHash().flattenOption( () => { log.warn("Tried to verify password when it is not set"); - return new AdminPasswordNotSetError( + return adminPasswordNotSetError( "Admin password is not set", ); }, diff --git a/server/src/lib/apiValidator.ts b/server/src/lib/apiValidator.ts index e28e5a4..7280a3a 100644 --- a/server/src/lib/apiValidator.ts +++ b/server/src/lib/apiValidator.ts @@ -19,7 +19,7 @@ export type ExtractRouteParams = T extends string : never; type ApiError = - | SchemaValidationError + | RequestValidationError | ResponseValidationError; export class Api< @@ -57,7 +57,7 @@ export class Api< return this.schema.req .parse(reqBody) .toAsync() - .mapErr((e) => requestValidationError(e.msg)) + .mapErr((e) => requestValidationError(e.info)) .andThenAsync(async (data) => { const pathSplitted = this.pathSplitted; for (const [key, value] of Object.entries(params)) { @@ -71,6 +71,7 @@ export class Api< method: this.method, headers: { "Content-Type": "application/json", + "Accept": "application/json; charset=utf-8", }, body: JSON.stringify(data), }, @@ -80,7 +81,7 @@ export class Api< return this.schema.res.parse(resBody).toAsync() .map((v) => v as InferSchemaType) - .mapErr((e) => responseValidationError(e.msg)); + .mapErr((e) => responseValidationError(e.info)); }); } diff --git a/server/src/lib/context.ts b/server/src/lib/context.ts index 7d935c1..7505f78 100644 --- a/server/src/lib/context.ts +++ b/server/src/lib/context.ts @@ -15,6 +15,8 @@ import { FailedToParseRequestAsJSONError, failedToParseRequestAsJSONError, } from "@src/lib/errors.ts"; +import { RequestValidationError } from "@src/lib/errors.ts"; +import { requestValidationError } from "@src/lib/errors.ts"; // https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html const SECURITY_HEADERS: Headers = new Headers({ @@ -86,7 +88,7 @@ export class Context< } public setParams( - params: Params, + params: Params, ): Context { const ctx = new Context( this.req, @@ -98,12 +100,13 @@ export class Context< ctx._port = this._port; ctx._cookies = this._cookies; ctx.res = this.res; + ctx.schema = this.schema; return ctx as Context; } public parseBody(): ResultAsync< InferSchemaType, - SchemaValidationError | FailedToParseRequestAsJSONError + RequestValidationError | FailedToParseRequestAsJSONError > { return ResultAsync .fromPromise( @@ -115,7 +118,9 @@ export class Context< return ok(data); } - return this.schema?.req.parse(data); + return this.schema?.req.parse(data).mapErr((e) => + requestValidationError(e.info) + ); }); } @@ -138,6 +143,21 @@ export class Context< return none; } + matchPreferredType( + html: () => Response, + json: () => Response, + other: () => Response, + ): Response { + switch (this.preferredType.unwrapOr("other")) { + case "json": + return json(); + case "html": + return html(); + case "other": + return other(); + } + } + get hostname(): Option { if (this._hostname) return some(this._hostname); const remoteAddr = this.info.remoteAddr; diff --git a/server/src/lib/devices.ts b/server/src/lib/devices.ts index 1681822..fa97464 100644 --- a/server/src/lib/devices.ts +++ b/server/src/lib/devices.ts @@ -1,49 +1,89 @@ -import usbip from "@src/lib/usbip.ts"; -import { - type CommandExecutionError, +import usbip, { + CommandExecutionError, DeviceDetailed, - type UsbipUnknownError, -} from "@shared/utils/usbip.ts"; -import { none } from "@shared/utils/option.ts"; + DeviceDoesNotExistError, + deviceDoesNotExistError, + UsbipUnknownError, +} from "@src/lib/usbip.ts"; +import { none, Option, some } from "@shared/utils/option.ts"; import { InferSchemaType, z } from "@shared/utils/validator.ts"; import log from "@shared/utils/logger.ts"; -import { errAsync, ResultAsync } from "@shared/utils/resultasync.ts"; +import { ResultAsync } from "@shared/utils/resultasync.ts"; +import { err, Ok, ok, Result } from "@shared/utils/result.ts"; + +type FailedToAccessDevices = CommandExecutionError | UsbipUnknownError; class Devices { - private readonly devices: Map = new Map(); - - updateDevices(): ResultAsync< - void, + private devices: Result< + Map, CommandExecutionError | UsbipUnknownError - > { - return usbip.getDevicesDetailed().mapErr((e) => { - log.error("Failed to update devices!"); - return e; - }).map((d) => d.unwrapOr([])).map( - (devices) => { - const current = new Set(devices.map((d) => d.busid)); - const old = new Set(this.devices.keys()); + > = ok(new Map()); - const connected = current.difference(old); - const disconnected = old.difference(current); + public update( + busid: string, + update: Partial, + ): Result { + return this.devices.andThen((devices) => { + const device = devices.get(busid); - for (const device of devices) { - if (connected.has(device.busid)) { - this.devices.set( - device.busid, - this.deviceFromDetailed(device), - ); - } - } + if (device === undefined) { + return err( + deviceDoesNotExistError( + `Device with busid ${busid} does not exist`, + ), + ); + } - for (const device of disconnected) { - this.devices.delete(device); - } - }, - ); + for (const key of Object.keys(update)) { + device[key as keyof typeof update] = + update[key as keyof typeof update] || none; + } + + return ok(); + }); } - deviceFromDetailed(d: DeviceDetailed): Device { + public updateDevices(): ResultAsync< + void, + FailedToAccessDevices + > { + return usbip.getDevicesDetailed() + .mapErr((e) => { + log.error("Failed to update devices!"); + this.devices = err(e); + return e; + }) + .map((d) => d.unwrapOr([] as DeviceDetailed[])) + .map( + (devices) => { + const current = new Set(devices.map((d) => d.busid)); + const old = new Set( + this.devices.unwrapOrElse(() => { + this.devices = ok(new Map()); + return this.devices.unwrap(); + }).keys(), + ); + + const connected = current.difference(old); + const disconnected = old.difference(current); + + for (const device of devices) { + if (connected.has(device.busid)) { + this.devices.unwrap().set( + device.busid, + this.deviceFromDetailed(device), + ); + } + } + + for (const device of disconnected) { + this.devices.unwrap().delete(device); + } + }, + ); + } + + private deviceFromDetailed(d: DeviceDetailed): Device { return { busid: d.busid, usbid: d.usbid, @@ -54,6 +94,12 @@ class Devices { connectedAt: new Date(), }; } + + public list(): Result, FailedToAccessDevices> { + return this.devices.map((devices) => devices.values().toArray()).map(( + devices, + ) => devices.length > 0 ? some(devices) : none); + } } export const deviceSchema = z.obj({ @@ -64,20 +110,19 @@ export const deviceSchema = z.obj({ displayName: z.option(z.string()), description: z.option(z.string()), connectedAt: z.date(), +}).strict(); + +export const deviceMutablesSchema = deviceSchema.pick({ + displayName: true, + description: true, }); -const test = new Devices(); - -await test.updateDevices(); - -console.log(test); - -import { sleep } from "https://deno.land/x/sleep/mod.ts"; - -await sleep(5); - -await test.updateDevices(); - -console.log(test); +export type DeviceMutables = InferSchemaType; export type Device = InferSchemaType; + +const devices = new Devices(); + +devices.updateDevices(); + +export default devices; diff --git a/server/src/lib/errors.ts b/server/src/lib/errors.ts index 089b353..1a890b8 100644 --- a/server/src/lib/errors.ts +++ b/server/src/lib/errors.ts @@ -1,4 +1,4 @@ -import { InferSchemaType } from "@shared/utils/validator.ts"; +import { InferSchemaType, z } from "@shared/utils/validator.ts"; import { createErrorFactory, defineError } from "@shared/utils/errors.ts"; export const queryExecutionErrorSchema = defineError( @@ -72,3 +72,47 @@ export const failedToParseRequestAsJSONError = createErrorFactory( export type FailedToParseRequestAsJSONError = InferSchemaType< typeof failedToParseRequestAsJSONErrorSchema >; + +export const tooManyRequestsErrorSchema = defineError( + "TooManyRequestsError", +); +export const tooManyRequestsError = createErrorFactory( + tooManyRequestsErrorSchema, +); +export type TooManyRequestsError = InferSchemaType< + typeof tooManyRequestsErrorSchema +>; + +export const unauthorizedErrorSchema = defineError( + "UnauthorizedError", +); +export const unauthorizedError = createErrorFactory(unauthorizedErrorSchema); +export type UnauthorizedError = InferSchemaType; + +export const invalidPasswordErrorSchema = defineError("InvalidPasswordError"); +export const invalidPasswordError = createErrorFactory( + invalidPasswordErrorSchema, +); +export type InvalidPasswordError = InferSchemaType< + typeof invalidPasswordErrorSchema +>; + +export const adminPasswordAlreadySetErrorSchema = defineError( + "AdminPasswordAlreadySetError", +); +export const adminPasswordAlreadySetError = createErrorFactory( + adminPasswordAlreadySetErrorSchema, +); +export type AdminPasswordAlreadySetError = InferSchemaType< + typeof adminPasswordAlreadySetErrorSchema +>; + +export const passwordsMustMatchErrorSchema = defineError( + "PasswordsMustMatchError", +); +export const passwordsMustMatchError = createErrorFactory( + passwordsMustMatchErrorSchema, +); +export type PasswordsMustMatchError = InferSchemaType< + typeof passwordsMustMatchErrorSchema +>; diff --git a/server/src/lib/router.ts b/server/src/lib/router.ts index 10159a1..6e7309d 100644 --- a/server/src/lib/router.ts +++ b/server/src/lib/router.ts @@ -51,32 +51,33 @@ class HttpRouter { return this; } - public add( + public add< + S extends string, + ReqSchema extends Schema = Schema, + ResSchema extends Schema = Schema, + >( path: S, method: string, - handler: RequestHandler, - schema?: { - res: Schema; - req: Schema; - }, + handler: RequestHandler, + schema?: { req: ReqSchema; res: ResSchema }, ): HttpRouter; - public add( + + public add< + S extends string, + ReqSchema extends Schema = Schema, + ResSchema extends Schema = Schema, + >( path: S[], method: string, - handler: RequestHandler, - schema?: { - res: Schema; - req: Schema; - }, + handler: RequestHandler, + schema?: { req: ReqSchema; res: ResSchema }, ): HttpRouter; + public add( path: string | string[], method: string, handler: RequestHandler, - schema?: { - res: Schema; - req: Schema; - }, + schema?: { req: Schema; res: Schema }, ): HttpRouter { const paths = Array.isArray(path) ? path : [path]; diff --git a/server/src/lib/usbip.ts b/server/src/lib/usbip.ts index 70c29a8..977d5c7 100644 --- a/server/src/lib/usbip.ts +++ b/server/src/lib/usbip.ts @@ -1,4 +1,291 @@ -import UsbipManager from "@shared/utils/usbip.ts"; +import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts"; +import { err, getMessageFromError, ok } from "@shared/utils/result.ts"; +import { errAsync } from "@shared/utils/index.ts"; +import log from "@shared/utils/logger.ts"; +import { + fromNullableVal, + none, + type Option, + some, +} from "@shared/utils/option.ts"; +import { createErrorFactory, defineError } from "@shared/utils/errors.ts"; +import { InferSchemaType } from "@shared/utils/validator.ts"; + +export const commandExecutionErrorSchema = defineError("CommandExecutionError"); +export const commandExecutionError = createErrorFactory( + commandExecutionErrorSchema, +); +export type CommandExecutionError = InferSchemaType< + typeof commandExecutionErrorSchema +>; + +export const deviceDoesNotExistErrorSchema = defineError( + "DeviceDoesNotExistError", +); +export const deviceDoesNotExistError = createErrorFactory( + deviceDoesNotExistErrorSchema, +); +export type DeviceDoesNotExistError = InferSchemaType< + typeof deviceDoesNotExistErrorSchema +>; + +export const deviceAlreadyBoundErrorSchema = defineError( + "DeviceAlreadyBoundError", +); +export const deviceAlreadyBoundError = createErrorFactory( + deviceAlreadyBoundErrorSchema, +); +export type DeviceAlreadyBoundError = InferSchemaType< + typeof deviceAlreadyBoundErrorSchema +>; + +export const deviceNotBoundErrorSchema = defineError("DeviceNotBoundError"); +export const deviceNotBoundError = createErrorFactory( + deviceNotBoundErrorSchema, +); +export type DeviceNotBoundError = InferSchemaType< + typeof deviceNotBoundErrorSchema +>; + +export const usbipUnknownErrorSchema = defineError("UsbipUnknownError"); +export const usbipUnknownError = createErrorFactory(usbipUnknownErrorSchema); +export type UsbipUnknownError = InferSchemaType; + +type UsbipCommonError = DeviceDoesNotExistError | UsbipUnknownError; + +class UsbipManager { + private readonly listDeatiledCmd = new Deno.Command("usbip", { + args: ["list", "-l"], + }); + private readonly listParsableCmd = new Deno.Command("usbip", { + args: ["list", "-pl"], + }); + private readonly decoder = new TextDecoder(); + + private readonly usbidRegex = /[0-9abcdef]{4}:[0-9abcdef]{4}/; + private readonly busidRegex = + /(?:[0-9]+(?:\.[0-9]+)*-)*[0-9]+(?:\.[0-9]+)*/; + + private executeCommand( + cmd: Deno.Command, + ): ResultAsync { + const promise = cmd.output(); + + return ResultAsync.fromPromise( + promise, + (e) => commandExecutionError(getMessageFromError(e)), + ) + .map(({ stdout, stderr, code }) => + new CommandOutput( + this.decoder.decode(stdout).trim(), + this.decoder.decode(stderr).trim(), + code, + ) + ); + } + + private handleCommonErrors(stderr: string): UsbipCommonError { + if ( + stderr.includes("device with the specified bus ID does not exist") + ) { + return deviceDoesNotExistError(stderr); + } + + return usbipUnknownError(stderr); + } + + private parseDetailedList(stdout: string): Option { + const devices: DeviceDetailed[] = []; + + const deviceEntries = stdout.trim().split("\n\n"); + + for (const deviceEntry of deviceEntries) { + const busid = deviceEntry.match(this.busidRegex)?.shift(); + + if (!busid) { + log.error( + `Failed to parse busid of a device:\n ${deviceEntry}`, + ); + continue; + } + + const usbid = fromNullableVal( + deviceEntry.match(this.usbidRegex)?.shift(), + ); + + const [_, line2] = deviceEntry.split("\n"); + + const [vendorVal, nameVal] = line2 + ? line2.split(" : ").map((s) => s.trim()) + : [undefined, undefined]; + + const vendor = fromNullableVal(vendorVal); + const name = nameVal + ? some( + nameVal.replace( + usbid.isSome() ? usbid.value : this.usbidRegex, + "", + ).replace("()", "") + .trim(), + ) + : none; + + [["usbid", usbid], ["vendor", vendor], ["name", name]].filter((v) => + (v[1] as Option).isNone() + ).map((v) => log.warn(`Failed to parse ${v[0]}:\n ${deviceEntry}`)); + + devices.push({ + busid, + usbid, + vendor, + name, + }); + } + + return devices.length > 0 ? some(devices) : none; + } + + public getDevicesDetailed(): ResultAsync< + Option, + CommandExecutionError | UsbipUnknownError + > { + return this.executeCommand(this.listDeatiledCmd).andThen( + ({ stdout, stderr, success }) => { + if (success) { + if (stderr) { + log.warn( + `usbip list -l succeeded but encountered an error: ${stderr}`, + ); + } + return ok(this.parseDetailedList(stdout)); + } + + return err(usbipUnknownError(stderr)); + }, + ); + } + + private parseParsableList(stdout: string): Option { + const devices: Device[] = []; + + const devicesEntries = stdout.trim().split("\n"); + + for (const deviceEntry of devicesEntries) { + const [busid, usbid] = deviceEntry + .slice(0, -1) + .split("#") + .map((v) => v.split("=")[1].trim() || undefined); + + if (!busid) { + log.error( + `Failed to parse busid of a device:\n ${deviceEntry}`, + ); + continue; + } + + if (!usbid) { + log.warn( + `Failed to parse usbid of a device:\n ${deviceEntry}`, + ); + } + + devices.push({ + busid, + usbid: fromNullableVal(usbid), + }); + } + + return devices.length > 0 ? some(devices) : none; + } + + public getDevices(): ResultAsync< + Option, + CommandExecutionError | UsbipUnknownError + > { + return this.executeCommand(this.listParsableCmd).andThenAsync( + ({ stdout, stderr, success }) => { + if (success) { + if (stderr) { + log.warn( + `usbip list -lp succeeded but encountered an error: ${stderr}`, + ); + } + return okAsync(this.parseParsableList(stdout)); + } + return errAsync(usbipUnknownError(stderr)); + }, + ); + } + + public bindDevice( + busid: string, + ): ResultAsync< + string, + UsbipCommonError | DeviceAlreadyBoundError | CommandExecutionError + > { + const cmd = new Deno.Command("usbip", { args: ["bind", "-b", busid] }); + + return this.executeCommand(cmd).andThen( + ({ stderr, success }) => { + if (success) { + return ok(stderr.trim() || "Device bound successfully"); + } + + if (stderr.includes("is already bound to usbip-host")) { + return err(deviceAlreadyBoundError(stderr)); + } + + return err(this.handleCommonErrors(stderr)); + }, + ); + } + + public unbindDevice( + busid: string, + ): ResultAsync< + string, + CommandExecutionError | DeviceNotBoundError | UsbipCommonError + > { + const cmd = new Deno.Command("usbip", { + args: ["unbind", "-b", busid], + }); + + return this.executeCommand(cmd).andThen(({ stderr, success }) => { + if (success) { + return ok(stderr.trim() || "Device unbound successfully"); + } + + if (stderr.includes("device is not bound to usbip-host driver")) { + return err(deviceNotBoundError(stderr)); + } + + return err(this.handleCommonErrors(stderr)); + }); + } +} + +class CommandOutput { + constructor( + public readonly stdout: string, + public readonly stderr: string, + public readonly code: number, + ) {} + get success(): boolean { + return this.code === 0; + } +} + +export interface DeviceDetailed { + busid: string; + usbid: Option; + vendor: Option; + name: Option; +} + +interface Device { + busid: string; + usbid: Option; +} const usbip = new UsbipManager(); diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 63498fb..56437d2 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -1,24 +1,60 @@ import { Middleware } from "@lib/router.ts"; import admin from "@lib/admin.ts"; +import { + queryExecutionError, + tooManyRequestsError, + unauthorizedError, +} from "@src/lib/errors.ts"; +import { err, ok } from "@shared/utils/result.ts"; +import { eta } from "../../main.ts"; const LOGIN_PATH = "/login"; +const SETUP_PATH = "/setup"; const authMiddleware: Middleware = async (c, next) => { const token = c.cookies.get("token"); const isValid = token - .map((token) => admin.sessions.verifyToken(token)) - .toBoolean(); + .map((token) => admin.sessions.verifyToken(token)).match( + (r) => r, + () => ok(false), + ); + + if (isValid.isErr()) { + return c.matchPreferredType( + () => c.html(eta.render("./internal_error.html", {})), + () => + c.json( + err(queryExecutionError("Server failed to execute query")), + ), + () => new Response("500 Internal server error", { status: 500 }), + ); + } + const path = c.path; - if (path.startsWith("/public")) { - await next(); - } else { - if (path !== LOGIN_PATH && !isValid) { - return c.redirect("/login"); + if ( + !isValid.value && !path.startsWith("/public") && path !== LOGIN_PATH && + path !== SETUP_PATH + ) { + if (!isValid.value) { + c.cookies.delete("token"); } - await next(); + if (c.preferredType.isNone()) { + return new Response("401 unautorized", { status: 401 }); + } + + switch (c.preferredType.value) { + case "json": + return c.json( + err(unauthorizedError("Unauthorized")), + { status: 401 }, + ); + case "html": + return c.redirect("/login"); + } } + await next(); }; export default authMiddleware; diff --git a/server/src/middleware/rateLimiter.ts b/server/src/middleware/rateLimiter.ts index 6b11cfd..e944037 100644 --- a/server/src/middleware/rateLimiter.ts +++ b/server/src/middleware/rateLimiter.ts @@ -1,5 +1,7 @@ import { Middleware } from "@lib/router.ts"; import log from "@shared/utils/logger.ts"; +import { err } from "@shared/utils/result.ts"; +import { tooManyRequestsError } from "@src/lib/errors.ts"; const requestCounts: Partial< Record @@ -31,9 +33,7 @@ const rateLimitMiddleware: Middleware = async (c, next) => { } case "json": { return c.json( - { - err: "429 Too Many Requests", - }, + err(tooManyRequestsError("Too many request")), { status: 429, }, diff --git a/server/src/views/internal_error.html b/server/src/views/internal_error.html new file mode 100644 index 0000000..6a7731d --- /dev/null +++ b/server/src/views/internal_error.html @@ -0,0 +1,3 @@ +<% layout("./layouts/layout.html") %> + + Internal error occurred diff --git a/server/src/views/setup.html b/server/src/views/setup.html new file mode 100644 index 0000000..b8831f8 --- /dev/null +++ b/server/src/views/setup.html @@ -0,0 +1,10 @@ +<% layout("./layouts/basic.html") %> +
+
+

password

+

password repeat

+
+
+
+ diff --git a/server/test.db b/server/test.db index 8981435..1b91532 100644 Binary files a/server/test.db and b/server/test.db differ diff --git a/server/views/internal_error.html b/server/views/internal_error.html new file mode 100644 index 0000000..0c31090 --- /dev/null +++ b/server/views/internal_error.html @@ -0,0 +1 @@ +<% layout("./layouts/layout.html") %> Internal error occurred \ No newline at end of file diff --git a/server/views/setup.html b/server/views/setup.html new file mode 100644 index 0000000..27770bc --- /dev/null +++ b/server/views/setup.html @@ -0,0 +1 @@ +<% layout("./layouts/basic.html") %>

password

password repeat

\ No newline at end of file diff --git a/shared/utils/errors.ts b/shared/utils/errors.ts new file mode 100644 index 0000000..20f30bc --- /dev/null +++ b/shared/utils/errors.ts @@ -0,0 +1,38 @@ +import { + InferSchemaType, + LiteralSchema, + ObjectSchema, + Schema, + StringSchema, + z, +} from "@shared/utils/validator.ts"; + +type ErrorDefinition> = ObjectSchema< + { type: LiteralSchema; info: I } +>; + +export function defineError< + T extends string, + I extends Schema = StringSchema, +>( + type: T, + info?: I, +): ErrorDefinition { + return z.obj({ + type: z.literal(type), + info: (info ?? z.string()) as I, + }); +} +export function createErrorFactory< + T extends string, + I extends Schema, +>( + errorDefinition: ErrorDefinition, +): (info: InferSchemaType) => InferSchemaType> { + return (info: InferSchemaType) => { + return { + type: errorDefinition.shape.type.literal, + info, + }; + }; +} diff --git a/shared/utils/result.ts b/shared/utils/result.ts index acbcf77..404ac30 100644 --- a/shared/utils/result.ts +++ b/shared/utils/result.ts @@ -367,11 +367,15 @@ export function flattenResult>( ): FlattenResult { let currentResult = nestedResult; - while (currentResult instanceof Ok) { - currentResult = currentResult.value; + while ( + currentResult instanceof Ok && + (currentResult.value instanceof Ok || + currentResult.value instanceof Err) + ) { + currentResult = currentResult.value as R; } - return ok(currentResult) as FlattenResult; + return currentResult as FlattenResult; } export type UnwrapOption = T extends Option ? V : T; diff --git a/shared/utils/validator.ts b/shared/utils/validator.ts index 81ae29c..f5f26d9 100644 --- a/shared/utils/validator.ts +++ b/shared/utils/validator.ts @@ -54,7 +54,7 @@ export class SchemaValidationError extends Error { }; } - get msg(): string { + get info(): string { return SchemaValidationError.getBestMsg(this.detail); } @@ -136,14 +136,14 @@ export class SchemaValidationError extends Error { case "missingProperties": return `Missing required properties: ${detail.keys.join(", ")}`; case "unionValidation": - return `Input did not match any union member`; + return `Input did not match any union member`; default: return "Unknown error"; } } } -function createValidationError( +export function createValidationError( input: unknown, error: ValidationErrorDetail, ) { @@ -285,7 +285,7 @@ export class StringSchema extends BaseSchema { } } -class LiteralSchema extends BaseSchema { +export class LiteralSchema extends BaseSchema { constructor( public readonly literal: L, msg?: string, @@ -645,7 +645,7 @@ class NeverSchema extends BaseSchema { } } -class ObjectSchema>> +export class ObjectSchema>> extends BaseSchema<{ [K in keyof S]: InferSchemaType }> { private strictMode: boolean = false; private objectMsg?; @@ -768,12 +768,41 @@ class ObjectSchema>> }); } - strict(): this { + public strict(): this { this.strictMode = true; return this; } + + public pick< + P extends Partial< + Record, boolean> + >, + >( + keys: P, + ): ObjectPick { + const o: Record> = {}; + + for (const key of Object.keys(keys)) { + if (keys[key as keyof P]) { + o[key] = this.shape[key]; + } + } + + return z.obj(o) as unknown as ObjectPick; + } } +type PickedKeys = { + [K in keyof T]: T[K] extends true ? K : never; +}[keyof T]; + +type ObjectPick< + O extends ObjectSchema, + P extends Partial, boolean>>, +> = O extends ObjectSchema + ? ObjectSchema<{ [K in PickedKeys

& keyof T]: T[K] }> + : never; + type InferUnionSchemaType[]> = U[number] extends Schema ? T : never;