diff --git a/deno.lock b/deno.lock index 69bc4ff..5a998f9 100644 --- a/deno.lock +++ b/deno.lock @@ -673,6 +673,9 @@ ] } }, + "redirects": { + "https://deno.land/x/sleep/mod.ts": "https://deno.land/x/sleep@v1.3.0/mod.ts" + }, "remote": { "https://deno.land/std@0.203.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", "https://deno.land/std@0.203.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", @@ -686,6 +689,8 @@ "https://deno.land/std@0.203.0/async/pool.ts": "47c1841cfa9c036144943d11747ddd44064f5baf8cb7ece25473ba873c6aceb0", "https://deno.land/std@0.203.0/async/retry.ts": "296fb9c323e1325a69bee14ba947e7da7409a8dd9dd646d70cb51ea0d301f24e", "https://deno.land/std@0.203.0/async/tee.ts": "47e42d35f622650b02234d43803d0383a89eb4387e1b83b5a40106d18ae36757", + "https://deno.land/x/sleep@v1.3.0/mod.ts": "e9955ecd3228a000e29d46726cd6ab14b65cf83904e9b365f3a8d64ec61c1af3", + "https://deno.land/x/sleep@v1.3.0/sleep.ts": "b6abaca093b094b0c2bba94f287b19a60946a8d15764d168f83fcf555f5bb59e", "https://wilsonl.in/minify-html/deno/0.15.0/index.js": "8e7ee5067ca84fb5d5a1f33118cac4998de0b7d80b3f56cc5c6728b84e6bfb70" }, "workspace": { diff --git a/server/api.ts b/server/api.ts index 85e9c95..95bbde9 100644 --- a/server/api.ts +++ b/server/api.ts @@ -5,7 +5,12 @@ const schema = { req: z.obj({ password: z.string(), }), - res: z.result(z.string(), z.any()), + res: z.result( + z.obj({ + isMatch: z.boolean(), + }), + z.any(), + ), }; export const loginApi = new Api("/login", "POST", schema); diff --git a/server/main.ts b/server/main.ts index f7cf8df..0bc21b8 100644 --- a/server/main.ts +++ b/server/main.ts @@ -5,9 +5,8 @@ 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 { Api } from "@src/lib/apiValidator.ts"; import { loginApi } from "./api.ts"; -import { err, getMessageFromError, ok } from "@shared/utils/result.ts"; +import { err, ok } from "@shared/utils/result.ts"; import admin from "@src/lib/admin.ts"; import { FailedToParseRequestAsJSON, @@ -49,15 +48,17 @@ router return c.html(eta.render("./index.html", {})); }) .get(["/login", "/login.html"], (c) => { - return c.html(eta.render("./login.html", {})); + const alreadyLoggedIn = c.cookies.get("token").map((token) => + admin.sessions.verifyToken(token) + ) + .toBoolean(); + + console.log(alreadyLoggedIn); + + return c.html(eta.render("./login.html", { alreadyLoggedIn })); }); -const schema = { - req: z.obj({ - password: z.string().max(1024), - }), - res: z.result(z.void(), z.string()), -}; +admin.setPassword("Vermont5481"); router.api(loginApi, async (c) => { const r = await c @@ -68,7 +69,7 @@ router.api(loginApi, async (c) => { if (r.isErr()) { if (r.error.type === "AdminPasswordNotSetError") { - return c.json( + return c.json400( err({ type: r.error.type, msg: r.error.message, @@ -78,18 +79,26 @@ router.api(loginApi, async (c) => { return handleCommonErrors(c, r.error); } - return admin.sessions.create() - .map(({ value, expires }) => { - c.cookies.set({ - name: AUTH_COOKIE_NAME, - value, - expires, - }); - return ok(); - }).match( - () => c.json(ok()), - (e) => handleCommonErrors(c, e), - ); + const isMatch = r.value; + + if (isMatch) { + return admin.sessions.create() + .map(({ value, expires }) => { + c.cookies.set({ + name: AUTH_COOKIE_NAME, + value, + expires, + }); + return ok({ isMatch: true }); + }).match( + (v) => c.json(v), + (e) => handleCommonErrors(c, e), + ); + } else { + return c.json(ok({ + isMatch: false, + })); + } }); function handleCommonErrors( @@ -106,11 +115,18 @@ function handleCommonErrors( { status: 500 }, ); case "FailedToParseRequestAsJSON": - case "SchemaValiationError": return c.json( err(error), { status: 400 }, ); + case "SchemaValiationError": + return c.json( + err({ + type: "ValidationError", + msg: error.msg, + }), + { status: 400 }, + ); } } diff --git a/server/public/js/index.js b/server/public/js/index.js new file mode 100644 index 0000000..e69de29 diff --git a/server/public/js/login.js b/server/public/js/login.js index ff1f886..11fa762 100644 --- a/server/public/js/login.js +++ b/server/public/js/login.js @@ -1 +1 @@ -import{loginApi as o}from"./shared.bundle.js";const s=document.getElementById("loginForm"),m=document.getElementById("passwordInput");s.addEventListener("submit",async e=>{e.preventDefault();const t=m.value,n=await o.makeRequest({password:t},{});console.log(n)}); +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"}); diff --git a/server/public/js/shared.bundle.js b/server/public/js/shared.bundle.js index d399f74..f231c94 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?u(this.value.map(e)):u(h(e(this.value)))}andThen(e){return e(this.value)}andThenAsync(e){return e(this.value)}mapErr(e){return u(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?u(this.value.match(e,r)):u(e(this.value))}toNullable(){return this.value}toAsync(){return O(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: ${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 u(t){return new c(t)}function i(t){return new f(t)}function ce(t,e){return(...r)=>{try{let n=t(...r);return u(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 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 u(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 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 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 U=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 o(t,e){return new U(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()}},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 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")}},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?u(n):i(o(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 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}`})})}},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 u(r);let n=r?.constructor?.name??"unknown";return i(o(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 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}))}},D=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}))}},$=class extends a{validateInput(e){return u(e)}},C=class extends a{validateInput(e){return u(e)}},J=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 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(o(r,{kind:"unexpectedProperties",keys:Z,msg:this.msg||this.objectMsg?.unexpectedProperty}))}continue}let w=d.parse(n[m]);if(w.isErr())return i(o(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(o(r,{kind:"missingProperties",keys:p,msg:this.msg||this.objectMsg?.missingProperty})):u(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 u(p.value);s=p.error.detail?.kind==="typeMismatch"&&s,n.push(p.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}))}},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(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"}))})}},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=>u(h(s)),s=>i(o(r,{kind:"propertyValidation",property:"value",detail:s.detail}))):a.isNullishSchema(this.schema)?u(h()):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(y);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"}))})}},R={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 J(t),obj:(t,e)=>new q(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 x=class extends U{type="RequestValidationError";constructor(e,r){super(e,r)}},S=class extends U{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 x(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 S(d.input,d.detail))})}};var _={req:R.obj({password:R.string()}),res:R.result(R.string(),R.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,U 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,u as ok,O as okAsync,h as some,R as z}; +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}; diff --git a/server/src/js/index.ts b/server/src/js/index.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/server/src/js/index.ts @@ -0,0 +1 @@ + diff --git a/server/src/js/login.ts b/server/src/js/login.ts index f1deee1..0875f61 100644 --- a/server/src/js/login.ts +++ b/server/src/js/login.ts @@ -6,13 +6,24 @@ const form = document.getElementById("loginForm") as HTMLFormElement; const passwordInput = document.getElementById( "passwordInput", ) as HTMLInputElement; +const errDiv = document.getElementById("errDiv") as HTMLDivElement; form.addEventListener("submit", async (e) => { e.preventDefault(); const password = passwordInput.value; - const res = await loginApi.makeRequest({ password }, {}); + const res = (await loginApi.makeRequest({ password }, {})).flatten(); - console.log(res); + if (res.isErr()) { + if (res.error.type === "RequestValidationError") { + errDiv.innerText = res.error.msg; + } + } else { + if (!res.value.isMatch) { + errDiv.innerText = "invalid password"; + } else { + window.location.href = "/"; + } + } }); diff --git a/server/src/lib/apiValidator.ts b/server/src/lib/apiValidator.ts index c43a1b3..f18b539 100644 --- a/server/src/lib/apiValidator.ts +++ b/server/src/lib/apiValidator.ts @@ -1,15 +1,9 @@ -import { Result } from "@shared/utils/result.ts"; -import { - InferSchemaType, - ResultSchema, - Schema, - z, -} from "@shared/utils/validator.ts"; +import { InferSchemaType, Schema } from "@shared/utils/validator.ts"; import { RequestValidationError, ResponseValidationError, } from "@src/lib/errors.ts"; -import { errAsync, okAsync, ResultAsync } from "@shared/utils/resultasync.ts"; +import { ResultAsync } from "@shared/utils/resultasync.ts"; export type ExtractRouteParams = T extends string ? T extends `${infer _Start}:${infer Param}/${infer Rest}` @@ -85,4 +79,16 @@ export class Api< ); }); } + + public makeSafeRequest( + reqBody: InferSchemaType, + params: { [K in ExtractRouteParams]: string }, + ): ResultAsync, ResponseValidationError> { + return this.makeRequest(reqBody, params).mapErr((e) => { + if (e.type === "RequestValidationError") { + throw "Failed to validate request"; + } + return e; + }); + } } diff --git a/server/src/lib/context.ts b/server/src/lib/context.ts index 18f9ce0..6240a62 100644 --- a/server/src/lib/context.ts +++ b/server/src/lib/context.ts @@ -3,19 +3,13 @@ import { type ExtractRouteParams } from "@lib/router.ts"; import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts"; import { deleteCookie, getCookies, setCookie } from "@std/http/cookie"; import { type Cookie } from "@std/http/cookie"; -import { - Err, - getMessageFromError, - Ok, - type Result, - ResultFromJSON, -} from "@shared/utils/result.ts"; +import { getMessageFromError } from "@shared/utils/result.ts"; import { InferSchemaType, Schema, SchemaValidationError, } from "@shared/utils/validator.ts"; -import { errAsync, ResultAsync } from "@shared/utils/resultasync.ts"; +import { ResultAsync } from "@shared/utils/resultasync.ts"; import log from "@shared/utils/logger.ts"; import { FailedToParseRequestAsJSON } from "@src/lib/errors.ts"; @@ -141,7 +135,10 @@ export class Context< return none; } - public json(body?: object | string, init: ResponseInit = {}): Response { + public json( + body?: ResSchema extends Schema ? T : object | string, + init: ResponseInit = {}, + ): Response { const headers = mergeHeaders( SECURITY_HEADERS, this._responseHeaders, diff --git a/server/src/lib/devices.ts b/server/src/lib/devices.ts new file mode 100644 index 0000000..1681822 --- /dev/null +++ b/server/src/lib/devices.ts @@ -0,0 +1,83 @@ +import usbip from "@src/lib/usbip.ts"; +import { + type CommandExecutionError, + DeviceDetailed, + type UsbipUnknownError, +} from "@shared/utils/usbip.ts"; +import { none } 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"; + +class Devices { + private readonly devices: Map = new Map(); + + updateDevices(): ResultAsync< + void, + 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()); + + const connected = current.difference(old); + const disconnected = old.difference(current); + + for (const device of devices) { + if (connected.has(device.busid)) { + this.devices.set( + device.busid, + this.deviceFromDetailed(device), + ); + } + } + + for (const device of disconnected) { + this.devices.delete(device); + } + }, + ); + } + + deviceFromDetailed(d: DeviceDetailed): Device { + return { + busid: d.busid, + usbid: d.usbid, + vendor: d.vendor, + name: d.name, + displayName: none, + description: none, + connectedAt: new Date(), + }; + } +} + +export const deviceSchema = z.obj({ + busid: z.string(), + usbid: z.option(z.string()), + vendor: z.option(z.string()), + name: z.option(z.string()), + displayName: z.option(z.string()), + description: z.option(z.string()), + connectedAt: z.date(), +}); + +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 Device = InferSchemaType; diff --git a/server/src/lib/routerTree.ts b/server/src/lib/routerTree.ts index b1021a1..2e53668 100644 --- a/server/src/lib/routerTree.ts +++ b/server/src/lib/routerTree.ts @@ -1,9 +1,16 @@ import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts"; -const DEFAULT_WILDCARD_SYMBOL = "*"; +const DEFAULT_WILDCARD = "*"; const DEFAULT_PARAM_PREFIX = ":"; const DEFAULT_PATH_SEPARATOR = "/"; +export type Params = Record; + +interface RouteMatch { + value: T; + params: Params; +} + interface Node { handler: Option; paramNames: string[]; @@ -29,52 +36,52 @@ class StaticNode implements Node { this.handler = fromNullableVal(handler); } - addStaticChild(segment: string, handler?: T): StaticNode { + private addStaticChild(segment: string, handler?: T): StaticNode { const child = new StaticNode(handler); this.staticChildren.set(segment, child); return child; } - setDynamicChild(handler?: T): DynamicNode { + private createDynamicChild(handler?: T): DynamicNode { const child = new DynamicNode(handler); this.dynamicChild = some(child); return child; } - setWildcardNode(handler?: T): WildcardNode { + private createWildcardNode(handler?: T): WildcardNode { const child = new WildcardNode(handler); this.wildcardChild = some(child); return child; } - addChild( + public addChild( segment: string, wildcardSymbol: string, paramPrefixSymbol: string, handler?: T, ): Node { if (segment === wildcardSymbol) { - return this.setWildcardNode(handler); + return this.createWildcardNode(handler); } if (segment.startsWith(paramPrefixSymbol)) { - return this.setDynamicChild(handler); + return this.createDynamicChild(handler); } return this.addStaticChild(segment, handler); } - getStaticChild(segment: string): Option> { + private getStaticChild(segment: string): Option> { return fromNullableVal(this.staticChildren.get(segment)); } - getDynamicChild(): Option> { + public getDynamicChild(): Option> { return this.dynamicChild; } - getWildcardChild(): Option> { + public getWildcardChild(): Option> { return this.wildcardChild; } - getChild(segment: string): Option> { + public getChild(segment: string): Option> { return this.getStaticChild(segment) .orElse(() => this.getWildcardChild()) .orElse(() => this.getDynamicChild()); @@ -89,7 +96,6 @@ class StaticNode implements Node { } } -// TODO: get rid of fixed param name class DynamicNode extends StaticNode implements Node { constructor( handler?: T, @@ -112,7 +118,7 @@ class WildcardNode implements Node { // Override to prevent adding children to a wildcard node public addChild(): Node { - throw new Error("Cannot add child to a WildcardNode."); + throw new Error("Cannot add child to a wildcard (catch-all) node."); } public getChild(): Option> { @@ -128,16 +134,13 @@ class WildcardNode implements Node { } } -// Using Node as the unified type for tree nodes. -type TreeNode = Node; - export class RouterTree { public readonly root: StaticNode; constructor( handler?: T, - private readonly wildcardSymbol: string = DEFAULT_WILDCARD_SYMBOL, - private readonly paramPrefixSymbol: string = DEFAULT_PARAM_PREFIX, + private readonly wildcardSymbol: string = DEFAULT_WILDCARD, + private readonly paramPrefix: string = DEFAULT_PARAM_PREFIX, private readonly pathSeparator: string = DEFAULT_PATH_SEPARATOR, ) { this.root = new StaticNode(handler); @@ -146,7 +149,7 @@ export class RouterTree { public add(path: string, handler: T): void { const segments = this.splitPath(path); const paramNames: string[] = this.extractParams(segments); - let current: TreeNode = this.root; + let current: Node = this.root; for (const segment of segments) { current = current @@ -155,7 +158,7 @@ export class RouterTree { current.addChild( segment, this.wildcardSymbol, - this.paramPrefixSymbol, + this.paramPrefix, ) ); @@ -174,7 +177,7 @@ export class RouterTree { public find(path: string): Option> { const segments = this.splitPath(path); const paramValues: string[] = []; - let current: TreeNode = this.root; + let current: Node = this.root; let i = 0; for (; i < segments.length; i++) { @@ -209,7 +212,7 @@ export class RouterTree { public getHandler(path: string): Option { const segments = this.splitPath(path); - let current: TreeNode = this.root; + let current: Node = this.root; for (const segment of segments) { if (current.isWildcardNode()) break; @@ -224,6 +227,16 @@ export class RouterTree { return current.handler; } + private traverseOrCreate(segments: string[]): Node { + let node: Node = this.root; + for (const segment of segments) { + if (node.isWildcardNode()) break; + node = node.getChild(segment).unwrapOrElse(() => + node.addChild(segment, this.wildcardSymbol, this.paramPrefix) + ); + } + } + private splitPath(path: string): string[] { const trimmed = path.trim().replace(/^\/+/, "").replace(/\/+$/, ""); return trimmed ? trimmed.split(this.pathSeparator) : []; @@ -231,18 +244,11 @@ export class RouterTree { public extractParams(segments: string[]): string[] { return segments.filter((segment) => - segment.startsWith(this.paramPrefixSymbol) + segment.startsWith(this.paramPrefix) ).map((segment) => this.stripParamPrefix(segment)); } public stripParamPrefix(segment: string): string { - return segment.slice(this.paramPrefixSymbol.length); + return segment.slice(this.paramPrefix.length); } } - -export type Params = Record; - -interface RouteMatch { - value: T; - params: Params; -} diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 3f8fb54..63498fb 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -17,10 +17,6 @@ const authMiddleware: Middleware = async (c, next) => { return c.redirect("/login"); } - if (path === LOGIN_PATH && isValid) { - return c.redirect(""); - } - await next(); } }; diff --git a/server/src/middleware/logger.ts b/server/src/middleware/logger.ts index 0f5f821..20ede98 100644 --- a/server/src/middleware/logger.ts +++ b/server/src/middleware/logger.ts @@ -4,7 +4,6 @@ const loggerMiddleware: Middleware = async (c, next) => { console.log("", c.req.method, c.path); await next(); console.log("", c.res.status, "\n"); - console.log(await c.res.json()); }; export default loggerMiddleware; diff --git a/server/src/views/index.html b/server/src/views/index.html index 87f29a7..846e40c 100644 --- a/server/src/views/index.html +++ b/server/src/views/index.html @@ -1,3 +1,3 @@ <% layout("./layouts/layout.html") %> - this is an index.html + diff --git a/server/src/views/login.html b/server/src/views/login.html index efb87f2..7fb6ab0 100644 --- a/server/src/views/login.html +++ b/server/src/views/login.html @@ -1,7 +1,17 @@ <% layout("./layouts/basic.html") %> -
-
-

password

-
-
- + <% if (!it.alreadyLoggedIn) { %> +
+
+

password

+
+
+
+ + <% } else { %> +
+ You are already logged in! +
+ + <% } %> diff --git a/server/test.db b/server/test.db index bc967ef..5bada1c 100644 Binary files a/server/test.db and b/server/test.db differ diff --git a/server/views/index.html b/server/views/index.html index c62e141..bc1eab5 100644 --- a/server/views/index.html +++ b/server/views/index.html @@ -1 +1,6 @@ -<% layout("./layouts/layout.html") %> this is an index.html \ No newline at end of file +<% layout("./layouts/layout.html") %> + + devices: +
+ + diff --git a/server/views/login.html b/server/views/login.html index c2d8fd0..72d9b00 100644 --- a/server/views/login.html +++ b/server/views/login.html @@ -1 +1 @@ -<% layout("./layouts/basic.html") %>

password

\ No newline at end of file +<% layout("./layouts/basic.html") %> <% if (!it.alreadyLoggedIn) { %>

password

<% } else { %>
You are already logged in!
<% } %> \ No newline at end of file diff --git a/shared/utils/result.ts b/shared/utils/result.ts index 313ac50..acbcf77 100644 --- a/shared/utils/result.ts +++ b/shared/utils/result.ts @@ -371,7 +371,7 @@ export function flattenResult>( currentResult = currentResult.value; } - return currentResult as FlattenResult; + return ok(currentResult) as FlattenResult; } export type UnwrapOption = T extends Option ? V : T; diff --git a/shared/utils/resultasync.ts b/shared/utils/resultasync.ts index 4819ca1..8ecb43c 100644 --- a/shared/utils/resultasync.ts +++ b/shared/utils/resultasync.ts @@ -254,13 +254,12 @@ export function errAsync(err: E): ResultAsync { return new ResultAsync(Promise.resolve(new Err(err))); } -export type FlattenResultAsync = R extends ResultAsync - ? T extends ResultAsync - ? FlattenResultAsync extends ResultAsync - ? ResultAsync - : never +type FlattenResultAsync = R extends + ResultAsync + ? Inner extends ResultAsync + ? ResultAsync : R - : never; + : R; type UnwrapPromise> = Pr extends Promise ? U diff --git a/shared/utils/usbip.ts b/shared/utils/usbip.ts index 6122202..cbe748b 100644 --- a/shared/utils/usbip.ts +++ b/shared/utils/usbip.ts @@ -9,42 +9,42 @@ import { some, } from "@shared/utils/option.ts"; -class CommandExecutionError extends Error { - code = "CommandExecutionError"; +export class CommandExecutionError extends Error { + type = "CommandExecutionError"; constructor(msg: string) { super(msg); } } -class DeviceDoesNotExistError extends Error { - code = "DeviceDoesNotExist"; +export class DeviceDoesNotExistError extends Error { + type = "DeviceDoesNotExist"; constructor(msg: string) { super(msg); } } -class DeviceAlreadyBoundError extends Error { - code = "DeviceAlreadyBound"; +export class DeviceAlreadyBoundError extends Error { + type = "DeviceAlreadyBound"; constructor(msg: string) { super(msg); } } -class DeviceNotBound extends Error { - code = "DeviceNotBound"; +export class DeviceNotBound extends Error { + type = "DeviceNotBound"; constructor(msg: string) { super(msg); } } -class UsbipUknownError extends Error { - code = "UsbipUknownError"; +export class UsbipUnknownError extends Error { + type = "UsbipUknownError"; constructor(msg: string) { super(msg); } } -type UsbipCommonError = DeviceDoesNotExistError | UsbipUknownError; +type UsbipCommonError = DeviceDoesNotExistError | UsbipUnknownError; class UsbipManager { private readonly listDeatiledCmd = new Deno.Command("usbip", { @@ -84,7 +84,7 @@ class UsbipManager { return new DeviceDoesNotExistError(stderr); } - return new UsbipUknownError(stderr); + return new UsbipUnknownError(stderr); } private parseDetailedList(stdout: string): Option { @@ -140,7 +140,7 @@ class UsbipManager { public getDevicesDetailed(): ResultAsync< Option, - CommandExecutionError | UsbipUknownError + CommandExecutionError | UsbipUnknownError > { return this.executeCommand(this.listDeatiledCmd).andThen( ({ stdout, stderr, success }) => { @@ -153,7 +153,7 @@ class UsbipManager { return ok(this.parseDetailedList(stdout)); } - return err(new UsbipUknownError(stderr)); + return err(new UsbipUnknownError(stderr)); }, ); } @@ -193,7 +193,7 @@ class UsbipManager { public getDevices(): ResultAsync< Option, - CommandExecutionError | UsbipUknownError + CommandExecutionError | UsbipUnknownError > { return this.executeCommand(this.listParsableCmd).andThenAsync( ({ stdout, stderr, success }) => { @@ -205,7 +205,7 @@ class UsbipManager { } return okAsync(this.parseParsableList(stdout)); } - return errAsync(new UsbipUknownError(stderr)); + return errAsync(new UsbipUnknownError(stderr)); }, ); } @@ -268,7 +268,7 @@ class CommandOutput { } } -interface DeviceDetailed { +export interface DeviceDetailed { busid: string; usbid: Option; vendor: Option; diff --git a/vendor/deno.land/x/sleep@v1.3.0/mod.ts b/vendor/deno.land/x/sleep@v1.3.0/mod.ts new file mode 100644 index 0000000..17ad6ef --- /dev/null +++ b/vendor/deno.land/x/sleep@v1.3.0/mod.ts @@ -0,0 +1,2 @@ +export * from "./sleep.ts" + diff --git a/vendor/deno.land/x/sleep@v1.3.0/sleep.ts b/vendor/deno.land/x/sleep@v1.3.0/sleep.ts new file mode 100644 index 0000000..85ef2c9 --- /dev/null +++ b/vendor/deno.land/x/sleep@v1.3.0/sleep.ts @@ -0,0 +1,11 @@ +// I buy and sell https://FreedomCash.org +export function sleep(seconds: number) { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)) +} +export function sleepRandomAmountOfSeconds(minimumSeconds: number, maximumSeconds: number) { + const secondsOfSleep = getRandomArbitrary(minimumSeconds, maximumSeconds) + return new Promise((resolve) => setTimeout(resolve, secondsOfSleep * 1000)) +} +function getRandomArbitrary(min: number, max: number) { + return Math.random() * (max - min) + min +} \ No newline at end of file diff --git a/vendor/manifest.json b/vendor/manifest.json index 7e58b16..c4cc7aa 100644 --- a/vendor/manifest.json +++ b/vendor/manifest.json @@ -1,5 +1,11 @@ { "modules": { + "https://deno.land/x/sleep/mod.ts": { + "headers": { + "location": "/x/sleep@v1.3.0/mod.ts", + "x-deno-warning": "Implicitly using latest version (v1.3.0) for https://deno.land/x/sleep/mod.ts" + } + }, "https://jsr.io/@std/crypto/1.0.3/_wasm/lib/deno_std_wasm_crypto.generated.d.mts": {}, "https://jsr.io/@std/crypto/1.0.3/_wasm/lib/deno_std_wasm_crypto.generated.mjs": {}, "https://jsr.io/@std/net/1.0.4/unstable_get_network_address.ts": {}