Compare commits
11 Commits
3d42e30061
...
6d721886d0
Author | SHA1 | Date | |
---|---|---|---|
6d721886d0 | |||
96ddd2c51b | |||
0666c74068 | |||
e10c903b52 | |||
88fecb793c | |||
12c11c9a27 | |||
e32046ffec | |||
095bd30f62 | |||
f21040d507 | |||
5f2bc7b5fa | |||
aa10241d57 |
72
Cargo.lock
generated
72
Cargo.lock
generated
@ -171,18 +171,20 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"serde",
|
"serde",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
"tokio 1.30.0",
|
"tokio 1.30.0",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-jwt-session"
|
name = "actix-jwt-session"
|
||||||
version = "0.1.1"
|
version = "1.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"cookie 0.17.0",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@ -193,7 +195,9 @@ dependencies = [
|
|||||||
"redis-async-pool",
|
"redis-async-pool",
|
||||||
"ring",
|
"ring",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time 0.3.28",
|
||||||
"tokio 1.30.0",
|
"tokio 1.30.0",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
@ -315,7 +319,7 @@ dependencies = [
|
|||||||
"derive_more",
|
"derive_more",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -365,7 +369,7 @@ dependencies = [
|
|||||||
"bytes 1.1.0",
|
"bytes 1.1.0",
|
||||||
"bytestring",
|
"bytestring",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"cookie",
|
"cookie 0.16.0",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -383,7 +387,7 @@ dependencies = [
|
|||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"socket2 0.4.9",
|
"socket2 0.4.9",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -895,6 +899,9 @@ name = "bitflags"
|
|||||||
version = "2.3.3"
|
version = "2.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
|
checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitvec"
|
name = "bitvec"
|
||||||
@ -1282,7 +1289,17 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"sha2",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
|
||||||
|
dependencies = [
|
||||||
|
"time 0.3.28",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2534,7 +2551,7 @@ dependencies = [
|
|||||||
"oswilno-view",
|
"oswilno-view",
|
||||||
"redis",
|
"redis",
|
||||||
"redis-async-pool",
|
"redis-async-pool",
|
||||||
"ron 0.8.0",
|
"ron 0.8.1",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -2630,7 +2647,7 @@ dependencies = [
|
|||||||
"ring",
|
"ring",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
"tokio 1.30.0",
|
"tokio 1.30.0",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
@ -3278,13 +3295,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ron"
|
name = "ron"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff"
|
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.0",
|
"base64 0.21.2",
|
||||||
"bitflags 1.3.2",
|
"bitflags 2.3.3",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3412,7 +3430,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
@ -3475,7 +3493,7 @@ dependencies = [
|
|||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"sea-query-derive",
|
"sea-query-derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3491,7 +3509,7 @@ dependencies = [
|
|||||||
"sea-query",
|
"sea-query",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3567,18 +3585,18 @@ checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.183"
|
version = "1.0.188"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c"
|
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.183"
|
version = "1.0.188"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816"
|
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3587,9 +3605,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.104"
|
version = "1.0.105"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
|
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa 1.0.2",
|
"itoa 1.0.2",
|
||||||
"ryu",
|
"ryu",
|
||||||
@ -3702,7 +3720,7 @@ dependencies = [
|
|||||||
"num-bigint",
|
"num-bigint",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3840,7 +3858,7 @@ dependencies = [
|
|||||||
"sqlx-rt",
|
"sqlx-rt",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time 0.3.25",
|
"time 0.3.28",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
@ -4018,9 +4036,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.25"
|
version = "0.3.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea"
|
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa 1.0.2",
|
"itoa 1.0.2",
|
||||||
@ -4037,9 +4055,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.11"
|
version = "0.2.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd"
|
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
@ -8,3 +8,4 @@ members = [
|
|||||||
'./crates/oswilno-actix-admin',
|
'./crates/oswilno-actix-admin',
|
||||||
'./crates/actix-jwt-session',
|
'./crates/actix-jwt-session',
|
||||||
]
|
]
|
||||||
|
resolver = "2"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
(()=>{var a=Object.create;var n=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var u=Object.getPrototypeOf,m=Object.prototype.hasOwnProperty;var p=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,s)=>(typeof require<"u"?require:t)[s]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var f=(e,t,s,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of h(t))!m.call(e,o)&&o!==s&&n(e,o,{get:()=>t[o],enumerable:!(r=d(t,o))||r.enumerable});return e};var y=(e,t,s)=>(s=e!=null?a(u(e)):{},f(t||!e||!e.__esModule?n(s,"default",{value:e,enumerable:!0}):s,e));customElements.define("oswilno-price",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){let e=this.shadowRoot,t=parseInt(this.getAttribute("price"));isNaN(t)&&(t=0);let s=parseInt(this.getAttribute("multiplier")),r=t,o=0;isNaN(s)||(r=Math.floor(t/s),o=t%s);let l=this.getAttribute("currency")||"PLN";e.innerHTML=`<style>:host{display:block;}</style><div>${r}.${o>=10?o:o+"0"} ${l}</div>`}});var i=e=>{let t="";for(let r=0;r<document.styleSheets.length;r++){let o=document.styleSheets[r];for(let l=0;l<o.rules.length;l++)t+=o.rules[l].cssText}let s=new CSSStyleSheet;s.replaceSync(t),e.adoptedStyleSheets=[s]};customElements.define("oswilno-error",class extends HTMLElement{constructor(){super();let e=this.attachShadow({mode:"open"});e.innerHTML=`
|
(()=>{var h=Object.create;var c=Object.defineProperty;var m=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var p=Object.getPrototypeOf,f=Object.prototype.hasOwnProperty;var g=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,s)=>(typeof require<"u"?require:t)[s]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var S=(e,t,s,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of u(t))!f.call(e,r)&&r!==s&&c(e,r,{get:()=>t[r],enumerable:!(o=m(t,r))||o.enumerable});return e};var y=(e,t,s)=>(s=e!=null?h(p(e)):{},S(t||!e||!e.__esModule?c(s,"default",{value:e,enumerable:!0}):s,e));customElements.define("oswilno-price",class extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){let e=this.shadowRoot,t=parseInt(this.getAttribute("price"));isNaN(t)&&(t=0);let s=parseInt(this.getAttribute("multiplier")),o=t,r=0;isNaN(s)||(o=Math.floor(t/s),r=t%s);let l=this.getAttribute("currency")||"PLN";e.innerHTML=`<style>:host{display:block;}</style><div>${o}.${r>=10?r:r+"0"} ${l}</div>`}});var a=e=>{let t="";for(let o=0;o<document.styleSheets.length;o++){let r=document.styleSheets[o];for(let l=0;l<r.rules.length;l++)t+=r.rules[l].cssText}let s=new CSSStyleSheet;s.replaceSync(t),e.adoptedStyleSheets=[s]};customElements.define("oswilno-error",class extends HTMLElement{constructor(){super();let e=this.attachShadow({mode:"open"});e.innerHTML=`
|
||||||
<style>:host{display:block;}</style>
|
<style>:host{display:block;}</style>
|
||||||
<div class="flex bg-red-100 rounded-lg p-4 mb-4 text-sm text-red-700">
|
<div class="flex bg-red-100 rounded-lg p-4 mb-4 text-sm text-red-700">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<div>
|
<div>
|
||||||
`,i(e)}});import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");var c=document.body;c.addEventListener("htmx:beforeOnLoad",function(e){let t=e.detail,s=t.xhr,r=s.status,o=t.successful;if(console.log(o,s.getResponseHeader("Authorization")),r===200){let l=s.getResponseHeader("Authorization");l&&localStorage.setItem("jwt",l.replace(/^Bearer /i,""))}else r===401&&localStorage.removeItem("jwt");(r===422||r===400)&&(t.shouldSwap=!0,t.isError=!1)});c.addEventListener("htmx:configRequest",function(e){localStorage.getItem("jwt")&&(e.detail.headers.Authorization="Bearer "+(localStorage.getItem("jwt")||""))});})();
|
`,a(e)}});import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");var n="Authorization",E="ACX-Authorization",i="ACX-Refresh",d=document.body;d.addEventListener("htmx:beforeOnLoad",function(e){let t=e.detail,s=t.xhr,o=s.status,r=t.successful;if(o===200){let l=s.getResponseHeader(n);l&&(console.log(s),localStorage.setItem("jwt",l.replace(/^Bearer /i,""))),s.getResponseHeader(i)&&localStorage.setItem("refresh",l.replace(/^Bearer /i,""))}else o===401&&localStorage.removeItem("jwt");(o===422||o===400)&&(t.shouldSwap=!0,t.isError=!1)});d.addEventListener("htmx:configRequest",function(e){localStorage.getItem("jwt")&&(e.detail.headers[E]="Bearer "+(localStorage.getItem("jwt")||""),e.detail.headers[n]="Bearer "+(localStorage.getItem("jwt")||""),e.detail.headers[i]=localStorage.getItem("refresh")||"")});})();
|
||||||
//# sourceMappingURL=build.js.map
|
//# sourceMappingURL=build.js.map
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"sources": ["../crates/web-assets/assets/elements/oswilno-price.js", "../crates/web-assets/assets/css.js", "../crates/web-assets/assets/elements/oswilno-error.js", "../crates/web-assets/assets/app.js"],
|
"sources": ["../crates/web-assets/assets/elements/oswilno-price.js", "../crates/web-assets/assets/css.js", "../crates/web-assets/assets/elements/oswilno-error.js", "../crates/web-assets/assets/app.js"],
|
||||||
"sourcesContent": ["customElements.define('oswilno-price', class extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.attachShadow({ mode: 'open' });\n\t}\n\tconnectedCallback() {\n\t\tlet shadow = this.shadowRoot;\n\t\tlet price = parseInt(this.getAttribute('price'));\n\t\tif (isNaN(price)) price = 0;\n\t\tconst multiplier = parseInt(this.getAttribute('multiplier'));\n\t\tlet major = price;\n\t\tlet minor = 0;\n\t\tif (!isNaN(multiplier)) {\n\t\t\tmajor = Math.floor(price / multiplier);\n\t\t\tminor = price % multiplier;\n\t\t}\n\t\tconst currency = this.getAttribute('currency') || 'PLN';\n\t\tshadow.innerHTML = `<style>:host{display:block;}</style><div>${major}.${minor >= 10 ? minor : minor + '0'} ${ currency }</div>`;\n\t}\n});\n", "export const copyCss = (shadow) => {\n\tlet css = '';\n\tfor (let i = 0; i < document.styleSheets.length; i++) {\n\t\tconst styleSheet = document.styleSheets[i];\n\t\tfor (let j = 0; j < styleSheet.rules.length; j++) {\n\t\t\tcss += styleSheet.rules[j].cssText;\t\n\t\t}\n\t}\n\tconst sheet = new CSSStyleSheet();\n\tsheet.replaceSync(css);\n\tshadow.adoptedStyleSheets = [sheet];\n};\n", "import { copyCss } from \"../css.js\";\n\ncustomElements.define('oswilno-error', class extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tconst shadow = this.attachShadow({ mode: 'open' });\n\t\tshadow.innerHTML = `\n\t\t\t<style>:host{display:block;}</style>\n\t\t\t<div class=\"flex bg-red-100 rounded-lg p-4 mb-4 text-sm text-red-700\">\n\t\t\t\t<slot></slot>\n\t\t\t<div>\n\t\t`;\n\t\tcopyCss(shadow);\n\t}\n});\n", "import './elements/oswilno-price.js';\nimport './elements/oswilno-error.js';\nimport(\"https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js\");\n\nconst body = document.body;\nbody.addEventListener('htmx:beforeOnLoad', function (evt) {\n\tconst detail = evt.detail;\n\tconst xhr = detail.xhr;\n\tconst status = xhr.status;\n\tconst successful = detail.successful;\n\n\tconsole.log(successful, xhr.getResponseHeader('Authorization'));\n\tif (status === 200) {\n\t\tconst bearer = xhr.getResponseHeader('Authorization');\n\t\tif (bearer) {\n\t\t\tlocalStorage.setItem('jwt', bearer.replace(/^Bearer /i, ''));\n\t\t}\n\t} else if (status === 401) {\n\t\tlocalStorage.removeItem('jwt');\n\t}\n\tif (status === 422 || status === 400) {\n\t\tdetail.shouldSwap = true;\n\t\tdetail.isError = false;\n\t}\n});\nbody.addEventListener('htmx:configRequest', function (evt) {\n\tif (localStorage.getItem('jwt')) {\n\t\tevt.detail.headers.Authorization = 'Bearer ' + (localStorage.getItem('jwt') || '');\n\t}\n});\n\n"],
|
"sourcesContent": ["customElements.define('oswilno-price', class extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.attachShadow({ mode: 'open' });\n\t}\n\tconnectedCallback() {\n\t\tlet shadow = this.shadowRoot;\n\t\tlet price = parseInt(this.getAttribute('price'));\n\t\tif (isNaN(price)) price = 0;\n\t\tconst multiplier = parseInt(this.getAttribute('multiplier'));\n\t\tlet major = price;\n\t\tlet minor = 0;\n\t\tif (!isNaN(multiplier)) {\n\t\t\tmajor = Math.floor(price / multiplier);\n\t\t\tminor = price % multiplier;\n\t\t}\n\t\tconst currency = this.getAttribute('currency') || 'PLN';\n\t\tshadow.innerHTML = `<style>:host{display:block;}</style><div>${major}.${minor >= 10 ? minor : minor + '0'} ${ currency }</div>`;\n\t}\n});\n", "export const copyCss = (shadow) => {\n\tlet css = '';\n\tfor (let i = 0; i < document.styleSheets.length; i++) {\n\t\tconst styleSheet = document.styleSheets[i];\n\t\tfor (let j = 0; j < styleSheet.rules.length; j++) {\n\t\t\tcss += styleSheet.rules[j].cssText;\t\n\t\t}\n\t}\n\tconst sheet = new CSSStyleSheet();\n\tsheet.replaceSync(css);\n\tshadow.adoptedStyleSheets = [sheet];\n};\n", "import { copyCss } from \"../css.js\";\n\ncustomElements.define('oswilno-error', class extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tconst shadow = this.attachShadow({ mode: 'open' });\n\t\tshadow.innerHTML = `\n\t\t\t<style>:host{display:block;}</style>\n\t\t\t<div class=\"flex bg-red-100 rounded-lg p-4 mb-4 text-sm text-red-700\">\n\t\t\t\t<slot></slot>\n\t\t\t<div>\n\t\t`;\n\t\tcopyCss(shadow);\n\t}\n});\n", "import './elements/oswilno-price.js';\nimport './elements/oswilno-error.js';\nimport(\"https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js\");\n\nconst READ_AUTH_HEADER = 'Authorization';\nconst AUTH_HEADER = 'ACX-Authorization';\nconst REFRESH_HEADER = 'ACX-Refresh';\nconst body = document.body;\nbody.addEventListener('htmx:beforeOnLoad', function (evt) {\n\tconst detail = evt.detail;\n\tconst xhr = detail.xhr;\n\tconst status = xhr.status;\n\tconst successful = detail.successful;\n\n\tif (status === 200) {\n\t\tconst bearer = xhr.getResponseHeader(READ_AUTH_HEADER);\n\t\tif (bearer) {\n\t\t\tconsole.log(xhr);\n\t\t\tlocalStorage.setItem('jwt', bearer.replace(/^Bearer /i, ''));\n\t\t}\n\t\tconst refresh = xhr.getResponseHeader(REFRESH_HEADER);\n\t\tif (refresh) {\n\t\t\tlocalStorage.setItem('refresh', bearer.replace(/^Bearer /i, ''));\n\t\t}\n\t} else if (status === 401) {\n\t\tlocalStorage.removeItem('jwt');\n\t}\n\tif (status === 422 || status === 400) {\n\t\tdetail.shouldSwap = true;\n\t\tdetail.isError = false;\n\t}\n});\nbody.addEventListener('htmx:configRequest', function (evt) {\n\tif (localStorage.getItem('jwt')) {\n\t\tevt.detail.headers[AUTH_HEADER] = 'Bearer ' + (localStorage.getItem('jwt') || '');\n\t\tevt.detail.headers[READ_AUTH_HEADER] = 'Bearer ' + (localStorage.getItem('jwt') || '');\n\t\tevt.detail.headers[REFRESH_HEADER] = (localStorage.getItem('refresh') || '');\n\t}\n});\n\n"],
|
||||||
"mappings": "0sBAAA,eAAe,OAAO,gBAAiB,cAAc,WAAY,CAChE,aAAc,CACb,MAAM,EACN,KAAK,aAAa,CAAE,KAAM,MAAO,CAAC,CACnC,CACA,mBAAoB,CACnB,IAAIA,EAAS,KAAK,WACdC,EAAQ,SAAS,KAAK,aAAa,OAAO,CAAC,EAC3C,MAAMA,CAAK,IAAGA,EAAQ,GAC1B,IAAMC,EAAa,SAAS,KAAK,aAAa,YAAY,CAAC,EACvDC,EAAQF,EACRG,EAAQ,EACP,MAAMF,CAAU,IACpBC,EAAQ,KAAK,MAAMF,EAAQC,CAAU,EACrCE,EAAQH,EAAQC,GAEjB,IAAMG,EAAW,KAAK,aAAa,UAAU,GAAK,MAClDL,EAAO,UAAY,4CAA4CG,CAAK,IAAIC,GAAS,GAAKA,EAAQA,EAAQ,GAAG,IAAKC,CAAS,QACxH,CACD,CAAC,ECnBM,IAAMC,EAAWC,GAAW,CAClC,IAAIC,EAAM,GACV,QAASC,EAAI,EAAGA,EAAI,SAAS,YAAY,OAAQA,IAAK,CACrD,IAAMC,EAAa,SAAS,YAAYD,CAAC,EACzC,QAASE,EAAI,EAAGA,EAAID,EAAW,MAAM,OAAQC,IAC5CH,GAAOE,EAAW,MAAMC,CAAC,EAAE,OAE7B,CACA,IAAMC,EAAQ,IAAI,cAClBA,EAAM,YAAYJ,CAAG,EACrBD,EAAO,mBAAqB,CAACK,CAAK,CACnC,ECTA,eAAe,OAAO,gBAAiB,cAAc,WAAY,CAChE,aAAc,CACb,MAAM,EACN,IAAMC,EAAS,KAAK,aAAa,CAAE,KAAM,MAAO,CAAC,EACjDA,EAAO,UAAY;AAAA;AAAA;AAAA;AAAA;AAAA,IAMnBC,EAAQD,CAAM,CACf,CACD,CAAC,ECZD,OAAO,mDAAmD,EAE1D,IAAME,EAAO,SAAS,KACtBA,EAAK,iBAAiB,oBAAqB,SAAUC,EAAK,CACzD,IAAMC,EAASD,EAAI,OACbE,EAAMD,EAAO,IACbE,EAASD,EAAI,OACbE,EAAaH,EAAO,WAG1B,GADA,QAAQ,IAAIG,EAAYF,EAAI,kBAAkB,eAAe,CAAC,EAC1DC,IAAW,IAAK,CACnB,IAAME,EAASH,EAAI,kBAAkB,eAAe,EAChDG,GACH,aAAa,QAAQ,MAAOA,EAAO,QAAQ,YAAa,EAAE,CAAC,CAE7D,MAAWF,IAAW,KACrB,aAAa,WAAW,KAAK,GAE1BA,IAAW,KAAOA,IAAW,OAChCF,EAAO,WAAa,GACpBA,EAAO,QAAU,GAEnB,CAAC,EACDF,EAAK,iBAAiB,qBAAsB,SAAUC,EAAK,CACtD,aAAa,QAAQ,KAAK,IAC7BA,EAAI,OAAO,QAAQ,cAAgB,WAAa,aAAa,QAAQ,KAAK,GAAK,IAEjF,CAAC",
|
"mappings": "0sBAAA,eAAe,OAAO,gBAAiB,cAAc,WAAY,CAChE,aAAc,CACb,MAAM,EACN,KAAK,aAAa,CAAE,KAAM,MAAO,CAAC,CACnC,CACA,mBAAoB,CACnB,IAAIA,EAAS,KAAK,WACdC,EAAQ,SAAS,KAAK,aAAa,OAAO,CAAC,EAC3C,MAAMA,CAAK,IAAGA,EAAQ,GAC1B,IAAMC,EAAa,SAAS,KAAK,aAAa,YAAY,CAAC,EACvDC,EAAQF,EACRG,EAAQ,EACP,MAAMF,CAAU,IACpBC,EAAQ,KAAK,MAAMF,EAAQC,CAAU,EACrCE,EAAQH,EAAQC,GAEjB,IAAMG,EAAW,KAAK,aAAa,UAAU,GAAK,MAClDL,EAAO,UAAY,4CAA4CG,CAAK,IAAIC,GAAS,GAAKA,EAAQA,EAAQ,GAAG,IAAKC,CAAS,QACxH,CACD,CAAC,ECnBM,IAAMC,EAAWC,GAAW,CAClC,IAAIC,EAAM,GACV,QAASC,EAAI,EAAGA,EAAI,SAAS,YAAY,OAAQA,IAAK,CACrD,IAAMC,EAAa,SAAS,YAAYD,CAAC,EACzC,QAASE,EAAI,EAAGA,EAAID,EAAW,MAAM,OAAQC,IAC5CH,GAAOE,EAAW,MAAMC,CAAC,EAAE,OAE7B,CACA,IAAMC,EAAQ,IAAI,cAClBA,EAAM,YAAYJ,CAAG,EACrBD,EAAO,mBAAqB,CAACK,CAAK,CACnC,ECTA,eAAe,OAAO,gBAAiB,cAAc,WAAY,CAChE,aAAc,CACb,MAAM,EACN,IAAMC,EAAS,KAAK,aAAa,CAAE,KAAM,MAAO,CAAC,EACjDA,EAAO,UAAY;AAAA;AAAA;AAAA;AAAA;AAAA,IAMnBC,EAAQD,CAAM,CACf,CACD,CAAC,ECZD,OAAO,mDAAmD,EAE1D,IAAME,EAAmB,gBACnBC,EAAc,oBACdC,EAAiB,cACjBC,EAAO,SAAS,KACtBA,EAAK,iBAAiB,oBAAqB,SAAUC,EAAK,CACzD,IAAMC,EAASD,EAAI,OACbE,EAAMD,EAAO,IACbE,EAASD,EAAI,OACbE,EAAaH,EAAO,WAE1B,GAAIE,IAAW,IAAK,CACnB,IAAME,EAASH,EAAI,kBAAkBN,CAAgB,EACjDS,IACH,QAAQ,IAAIH,CAAG,EACf,aAAa,QAAQ,MAAOG,EAAO,QAAQ,YAAa,EAAE,CAAC,GAE5CH,EAAI,kBAAkBJ,CAAc,GAEnD,aAAa,QAAQ,UAAWO,EAAO,QAAQ,YAAa,EAAE,CAAC,CAEjE,MAAWF,IAAW,KACrB,aAAa,WAAW,KAAK,GAE1BA,IAAW,KAAOA,IAAW,OAChCF,EAAO,WAAa,GACpBA,EAAO,QAAU,GAEnB,CAAC,EACDF,EAAK,iBAAiB,qBAAsB,SAAUC,EAAK,CACtD,aAAa,QAAQ,KAAK,IAC7BA,EAAI,OAAO,QAAQH,CAAW,EAAI,WAAa,aAAa,QAAQ,KAAK,GAAK,IAC9EG,EAAI,OAAO,QAAQJ,CAAgB,EAAI,WAAa,aAAa,QAAQ,KAAK,GAAK,IACnFI,EAAI,OAAO,QAAQF,CAAc,EAAK,aAAa,QAAQ,SAAS,GAAK,GAE3E,CAAC",
|
||||||
"names": ["shadow", "price", "multiplier", "major", "minor", "currency", "copyCss", "shadow", "css", "i", "styleSheet", "j", "sheet", "shadow", "copyCss", "body", "evt", "detail", "xhr", "status", "successful", "bearer"]
|
"names": ["shadow", "price", "multiplier", "major", "minor", "currency", "copyCss", "shadow", "css", "i", "styleSheet", "j", "sheet", "shadow", "copyCss", "READ_AUTH_HEADER", "AUTH_HEADER", "REFRESH_HEADER", "body", "evt", "detail", "xhr", "status", "successful", "bearer"]
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-jwt-session"
|
name = "actix-jwt-session"
|
||||||
version = "0.1.1"
|
version = "1.0.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Full featured JWT session managment for actix"
|
description = "Full featured JWT session managment for actix"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ['use-redis', 'use-tracing']
|
default = ['use-redis', 'use-tracing', 'panic-bad-ttl', 'hashing']
|
||||||
use-redis = ["redis", "redis-async-pool"]
|
use-redis = ["redis", "redis-async-pool"]
|
||||||
use-tracing = ['tracing']
|
use-tracing = ['tracing']
|
||||||
|
override-bad-ttl = []
|
||||||
|
panic-bad-ttl = []
|
||||||
|
hashing = ["argon2"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
@ -23,10 +26,14 @@ redis = { version = "0.17", optional = true }
|
|||||||
redis-async-pool = { version = "0.2.4", optional = true }
|
redis-async-pool = { version = "0.2.4", optional = true }
|
||||||
ring = "0.16.20"
|
ring = "0.16.20"
|
||||||
serde = { version = "1.0.183", features = ["derive"] }
|
serde = { version = "1.0.183", features = ["derive"] }
|
||||||
|
serde_json = "1.0.105"
|
||||||
thiserror = "1.0.44"
|
thiserror = "1.0.44"
|
||||||
tokio = { version = "1.30.0", features = ["full"] }
|
tokio = { version = "1.30.0", features = ["full"] }
|
||||||
tracing = { version = "0.1.37", optional = true }
|
tracing = { version = "0.1.37", optional = true }
|
||||||
uuid = { version = "1.4.1", features = ["v4", "serde"] }
|
uuid = { version = "1.4.1", features = ["v4", "serde"] }
|
||||||
|
argon2 = { version = "0.5.1", optional = true }
|
||||||
|
cookie = "0.17.0"
|
||||||
|
time = { version = "0.3.28", features = ["serde"] }
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "ensure_redis_flow"
|
name = "ensure_redis_flow"
|
||||||
|
@ -1,18 +1,151 @@
|
|||||||
General purpose JWT session validator for actix_web
|
![docs.rs](https://img.shields.io/docsrs/actix-jwt-session)
|
||||||
|
|
||||||
It’s designed to extract session using middleware and validate path simply by using extractors.
|
|
||||||
|
All in one creating session and session validation library for actix.
|
||||||
|
|
||||||
|
It's designed to extract session using middleware and validate endpoint simply by using actix-web extractors.
|
||||||
|
Currently you can extract tokens from Header or Cookie. It's possible to implement Path, Query
|
||||||
|
or Body using `[ServiceRequest::extract]` but you must have struct to which values will be
|
||||||
|
extracted so it's easy to do if you have your own fields.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct MyJsonBody {
|
||||||
|
jwt: Option<String>,
|
||||||
|
refresh: Option<String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To start with this library you need to create your own `AppClaims` structure and implement
|
||||||
|
`actix_jwt_session::Claims` trait for it.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Audience {
|
||||||
|
Web,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct Claims {
|
||||||
|
#[serde(rename = "exp")]
|
||||||
|
pub expiration_time: u64,
|
||||||
|
#[serde(rename = "iat")]
|
||||||
|
pub issues_at: usize,
|
||||||
|
/// Account login
|
||||||
|
#[serde(rename = "sub")]
|
||||||
|
pub subject: String,
|
||||||
|
#[serde(rename = "aud")]
|
||||||
|
pub audience: Audience,
|
||||||
|
#[serde(rename = "jti")]
|
||||||
|
pub jwt_id: uuid::Uuid,
|
||||||
|
#[serde(rename = "aci")]
|
||||||
|
pub account_id: i32,
|
||||||
|
#[serde(rename = "nbf")]
|
||||||
|
pub not_before: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix_jwt_session::Claims for Claims {
|
||||||
|
fn jti(&self) -> uuid::Uuid {
|
||||||
|
self.jwt_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subject(&self) -> &str {
|
||||||
|
&self.subject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Claims {
|
||||||
|
pub fn account_id(&self) -> i32 {
|
||||||
|
self.account_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you must create middleware factory with session storage. Currently there's adapter only
|
||||||
|
for redis so we will goes with it in this example.
|
||||||
|
|
||||||
|
* First create connection pool to redis using `redis_async_pool`.
|
||||||
|
* Next generate or load create jwt signing keys. They are required for creating JWT from
|
||||||
|
claims.
|
||||||
|
* Finally pass keys and algorithm to builder, pass pool and add some extractors
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::sync::Arc;
|
||||||
|
use actix_jwt_session::*;
|
||||||
|
|
||||||
|
async fn create<AppClaims: actix_jwt_session::Claims>() {
|
||||||
|
// create redis connection
|
||||||
|
let redis = {
|
||||||
|
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
||||||
|
RedisPool::new(
|
||||||
|
RedisConnectionManager::new(
|
||||||
|
redis::Client::open("redis://localhost:6379").expect("Fail to connect to redis"),
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
5,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// load or create new keys in `./config`
|
||||||
|
let keys = JwtSigningKeys::load_or_create();
|
||||||
|
|
||||||
|
// create new [SessionStorage] and [SessionMiddlewareFactory]
|
||||||
|
let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build(
|
||||||
|
Arc::new(keys.encoding_key),
|
||||||
|
Arc::new(keys.decoding_key),
|
||||||
|
Algorithm::EdDSA
|
||||||
|
)
|
||||||
|
// pass redis connection
|
||||||
|
.with_redis_pool(redis.clone())
|
||||||
|
// Check if header "Authorization" exists and contains Bearer with encoded JWT
|
||||||
|
.with_jwt_header("Authorization")
|
||||||
|
// Check if cookie "jwt" exists and contains encoded JWT
|
||||||
|
.with_jwt_cookie("acx-a")
|
||||||
|
.with_refresh_header("ACX-Refresh")
|
||||||
|
// Check if cookie "jwt" exists and contains encoded JWT
|
||||||
|
.with_refresh_cookie("acx-r")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see we have there [SessionMiddlewareBuilder::with_refresh_cookie] and [SessionMiddlewareBuilder::with_refresh_header]. Library uses
|
||||||
|
internal structure [RefreshToken] which is created and managed internally without any additional user work.
|
||||||
|
|
||||||
|
This will be used to extend JWT lifetime. This lifetime comes from 2 structures which describe
|
||||||
|
time to live. [JwtTtl] describes how long access token should be valid, [RefreshToken]
|
||||||
|
describes how long refresh token is valid. [SessionStorage] allows to extend livetime of both
|
||||||
|
with single call of [SessionStorage::refresh] and it will change time of creating tokens to
|
||||||
|
current time.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use actix_jwt_session::{JwtTtl, RefreshTtl, Duration};
|
||||||
|
|
||||||
|
fn example_ttl() {
|
||||||
|
let jwt_ttl = JwtTtl(Duration::days(14));
|
||||||
|
let refresh_ttl = RefreshTtl(Duration::days(3 * 31));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you just need to add those structures to [actix_web::App] using `.app_data` and `.wrap` and
|
||||||
|
you are ready to go. Bellow you have full example of usage.
|
||||||
|
|
||||||
Examples usage:
|
Examples usage:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use std::boxed::Box;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use actix_jwt_session::*;
|
use actix_jwt_session::*;
|
||||||
use actix_web::get;
|
use actix_web::{get, post};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::{Data, Json};
|
||||||
use actix_web::{HttpResponse, App, HttpServer};
|
use actix_web::{HttpResponse, App, HttpServer};
|
||||||
use ring::rand::SystemRandom;
|
|
||||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
|
||||||
use jsonwebtoken::*;
|
use jsonwebtoken::*;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
@ -30,93 +163,232 @@ async fn main() {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let keys = JwtSigningKeys::generate().unwrap();
|
let keys = JwtSigningKeys::load_or_create();
|
||||||
let factory = RedisMiddlewareFactory::<AppClaims>::new(
|
let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build(
|
||||||
Arc::new(keys.encoding_key),
|
Arc::new(keys.encoding_key),
|
||||||
Arc::new(keys.decoding_key),
|
Arc::new(keys.decoding_key),
|
||||||
Algorithm::EdDSA,
|
Algorithm::EdDSA
|
||||||
redis.clone(),
|
)
|
||||||
vec![
|
.with_redis_pool(redis.clone())
|
||||||
// Check if header "Authorization" exists and contains Bearer with encoded JWT
|
// Check if header "Authorization" exists and contains Bearer with encoded JWT
|
||||||
Box::new(HeaderExtractor::new("Authorization")),
|
.with_jwt_header(JWT_HEADER_NAME)
|
||||||
// Check if cookie "jwt" exists and contains encoded JWT
|
// Check if cookie JWT exists and contains encoded JWT
|
||||||
Box::new(CookieExtractor::new("jwt")),
|
.with_jwt_cookie(JWT_COOKIE_NAME)
|
||||||
]
|
.with_refresh_header(REFRESH_HEADER_NAME)
|
||||||
);
|
// Check if cookie JWT exists and contains encoded JWT
|
||||||
|
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
||||||
|
.finish();
|
||||||
|
let jwt_ttl = JwtTtl(Duration::days(14));
|
||||||
|
let refresh_ttl = RefreshTtl(Duration::days(3 * 31));
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let factory = factory.clone();
|
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(Data::new(factory.storage()))
|
.app_data(Data::new(storage.clone()))
|
||||||
.wrap(factory)
|
.app_data(Data::new( jwt_ttl ))
|
||||||
|
.app_data(Data::new( refresh_ttl ))
|
||||||
|
.wrap(factory.clone())
|
||||||
.app_data(Data::new(redis.clone()))
|
.app_data(Data::new(redis.clone()))
|
||||||
.service(storage_access)
|
|
||||||
.service(must_be_signed_in)
|
.service(must_be_signed_in)
|
||||||
.service(may_be_signed_in)
|
.service(may_be_signed_in)
|
||||||
|
.service(register)
|
||||||
|
.service(sign_in)
|
||||||
|
.service(sign_out)
|
||||||
|
.service(refresh_session)
|
||||||
|
.service(session_info)
|
||||||
|
.service(root)
|
||||||
})
|
})
|
||||||
.bind(("0.0.0.0", 8080)).unwrap()
|
.bind(("0.0.0.0", 8080)).unwrap()
|
||||||
.run()
|
.run()
|
||||||
.await.unwrap();
|
.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct AppClaims {
|
|
||||||
id: uuid::Uuid,
|
|
||||||
subject: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Claims for AppClaims {
|
|
||||||
fn jti(&self) -> uuid::Uuid { self.id }
|
|
||||||
fn subject(&self) -> &str { &self.subject }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct SessionData {
|
pub struct SessionData {
|
||||||
id: uuid::Uuid,
|
id: uuid::Uuid,
|
||||||
subject: String,
|
subject: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/access-storage")]
|
|
||||||
async fn storage_access(
|
|
||||||
session_store: Data<SessionStorage<AppClaims>>,
|
|
||||||
p: actix_web::web::Json<SessionData>,
|
|
||||||
) -> HttpResponse {
|
|
||||||
let p = p.into_inner();
|
|
||||||
session_store.store(AppClaims {
|
|
||||||
id: p.id,
|
|
||||||
subject: p.subject,
|
|
||||||
}, std::time::Duration::from_secs(60 * 60 * 24 * 14) ).await.unwrap();
|
|
||||||
HttpResponse::Ok().body("")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/authorized")]
|
#[get("/authorized")]
|
||||||
async fn must_be_signed_in(session: Authenticated<AppClaims>) -> HttpResponse {
|
async fn must_be_signed_in(session: Authenticated<AppClaims>) -> HttpResponse {
|
||||||
|
use crate::actix_jwt_session::Claims;
|
||||||
let jit = session.jti();
|
let jit = session.jti();
|
||||||
HttpResponse::Ok().body("")
|
HttpResponse::Ok().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/maybe-authorized")]
|
#[get("/maybe-authorized")]
|
||||||
async fn may_be_signed_in(session: MaybeAuthenticated<AppClaims>) -> HttpResponse {
|
async fn may_be_signed_in(session: MaybeAuthenticated<AppClaims>) -> HttpResponse {
|
||||||
if let Some(session) = session.into_option() {
|
if let Some(session) = session.into_option() {
|
||||||
}
|
}
|
||||||
HttpResponse::Ok().body("")
|
HttpResponse::Ok().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct JwtSigningKeys {
|
#[derive(Deserialize)]
|
||||||
encoding_key: EncodingKey,
|
struct SignUpPayload {
|
||||||
decoding_key: DecodingKey,
|
login: String,
|
||||||
|
password: String,
|
||||||
|
password_confirmation: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JwtSigningKeys {
|
#[post("/session/sign-up")]
|
||||||
fn generate() -> Result<Self, Box<dyn std::error::Error>> {
|
async fn register(payload: Json<SignUpPayload>) -> Result<HttpResponse, actix_web::Error> {
|
||||||
let doc = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?;
|
let payload = payload.into_inner();
|
||||||
let keypair = Ed25519KeyPair::from_pkcs8(doc.as_ref())?;
|
|
||||||
let encoding_key = EncodingKey::from_ed_der(doc.as_ref());
|
// Validate payload
|
||||||
let decoding_key = DecodingKey::from_ed_der(keypair.public_key().as_ref());
|
|
||||||
Ok(JwtSigningKeys {
|
// Save model and return HttpResponse
|
||||||
encoding_key,
|
let model = AccountModel {
|
||||||
decoding_key,
|
id: -1,
|
||||||
})
|
login: payload.login,
|
||||||
|
// Encrypt password before saving to database
|
||||||
|
pass_hash: Hashing::encrypt(&payload.password).unwrap(),
|
||||||
|
};
|
||||||
|
// Save model
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SignInPayload {
|
||||||
|
login: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/session/sign-in")]
|
||||||
|
async fn sign_in(
|
||||||
|
store: Data<SessionStorage>,
|
||||||
|
payload: Json<SignInPayload>,
|
||||||
|
jwt_ttl: Data<JwtTtl>,
|
||||||
|
refresh_ttl: Data<RefreshTtl>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let payload = payload.into_inner();
|
||||||
|
let store = store.into_inner();
|
||||||
|
let account: AccountModel = {
|
||||||
|
/* load account using login */
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
|
if let Err(e) = Hashing::verify(account.pass_hash.as_str(), payload.password.as_str()) {
|
||||||
|
return Ok(HttpResponse::Unauthorized().finish());
|
||||||
|
}
|
||||||
|
let claims = AppClaims {
|
||||||
|
issues_at: OffsetDateTime::now_utc().unix_timestamp() as usize,
|
||||||
|
subject: account.login.clone(),
|
||||||
|
expiration_time: jwt_ttl.0.as_seconds_f64() as u64,
|
||||||
|
audience: Audience::Web,
|
||||||
|
jwt_id: uuid::Uuid::new_v4(),
|
||||||
|
account_id: account.id,
|
||||||
|
not_before: 0,
|
||||||
|
};
|
||||||
|
let pair = store
|
||||||
|
.clone()
|
||||||
|
.store(claims, *jwt_ttl.into_inner(), *refresh_ttl.into_inner())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.append_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
||||||
|
.append_header((REFRESH_HEADER_NAME, pair.refresh.encode().unwrap()))
|
||||||
|
.finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/session/sign-out")]
|
||||||
|
async fn sign_out(store: Data<SessionStorage>, auth: Authenticated<AppClaims>) -> HttpResponse {
|
||||||
|
let store = store.into_inner();
|
||||||
|
store.erase::<AppClaims>(auth.jwt_id).await.unwrap();
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.append_header((JWT_HEADER_NAME, ""))
|
||||||
|
.append_header((REFRESH_HEADER_NAME, ""))
|
||||||
|
.cookie(
|
||||||
|
actix_web::cookie::Cookie::build(JWT_COOKIE_NAME, "")
|
||||||
|
.expires(OffsetDateTime::now_utc())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.cookie(
|
||||||
|
actix_web::cookie::Cookie::build(REFRESH_COOKIE_NAME, "")
|
||||||
|
.expires(OffsetDateTime::now_utc())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/session/info")]
|
||||||
|
async fn session_info(auth: Authenticated<AppClaims>) -> HttpResponse {
|
||||||
|
HttpResponse::Ok().json(&*auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/session/refresh")]
|
||||||
|
async fn refresh_session(
|
||||||
|
auth: Authenticated<RefreshToken>,
|
||||||
|
storage: Data<SessionStorage>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let storage = storage.into_inner();
|
||||||
|
storage.refresh(auth.refresh_jti).await.unwrap();
|
||||||
|
HttpResponse::Ok().json(&*auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn root() -> HttpResponse {
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Audience {
|
||||||
|
Web,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct AppClaims {
|
||||||
|
#[serde(rename = "exp")]
|
||||||
|
pub expiration_time: u64,
|
||||||
|
#[serde(rename = "iat")]
|
||||||
|
pub issues_at: usize,
|
||||||
|
/// Account login
|
||||||
|
#[serde(rename = "sub")]
|
||||||
|
pub subject: String,
|
||||||
|
#[serde(rename = "aud")]
|
||||||
|
pub audience: Audience,
|
||||||
|
#[serde(rename = "jti")]
|
||||||
|
pub jwt_id: uuid::Uuid,
|
||||||
|
#[serde(rename = "aci")]
|
||||||
|
pub account_id: i32,
|
||||||
|
#[serde(rename = "nbf")]
|
||||||
|
pub not_before: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl actix_jwt_session::Claims for AppClaims {
|
||||||
|
fn jti(&self) -> uuid::Uuid {
|
||||||
|
self.jwt_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subject(&self) -> &str {
|
||||||
|
&self.subject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AppClaims {
|
||||||
|
pub fn account_id(&self) -> i32 {
|
||||||
|
self.account_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AccountModel {
|
||||||
|
id: i32,
|
||||||
|
login: String,
|
||||||
|
pass_hash: String,
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Changelog:
|
||||||
|
|
||||||
|
1.0.0
|
||||||
|
|
||||||
|
* Factory is created using builder pattern
|
||||||
|
* JSON Web Token has automatically created Refresh Token
|
||||||
|
* Higher abstraction layers for Middleware, MiddlewareFactory and SessionStorage
|
||||||
|
* Build-in hashing functions
|
||||||
|
* Build-in TTL structures
|
||||||
|
* Documentation
|
||||||
|
|
||||||
|
1.0.1
|
||||||
|
|
||||||
|
* Returns new pair after refresh lifetime
|
||||||
|
208
crates/actix-jwt-session/src/extractors.rs
Normal file
208
crates/actix-jwt-session/src/extractors.rs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
//! Allow to create own session extractor and extract from cookie or header.
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Trait allowing to extract JWt token from [actix_web::dev::ServiceRequest]
|
||||||
|
///
|
||||||
|
/// Two extractor are implemented by default
|
||||||
|
/// * [HeaderExtractor] which is best for any PWA or micro services requests
|
||||||
|
/// * [CookieExtractor] which is best for simple server with session stored in cookie
|
||||||
|
///
|
||||||
|
/// It's possible to implement GraphQL, JSON payload or query using `req.extract::<JSON<YourStruct>>()` if this is needed.
|
||||||
|
///
|
||||||
|
/// All implementation can use [SessionExtractor::decode] method for decoding raw JWT string into
|
||||||
|
/// Claims and then [SessionExtractor::validate] to validate claims agains session stored in [SessionStorage]
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
pub trait SessionExtractor<ClaimsType: Claims>: Send + Sync + 'static {
|
||||||
|
/// Extract claims from [actix_web::dev::ServiceRequest]
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use actix_web::dev::ServiceRequest;
|
||||||
|
/// use jsonwebtoken::*;
|
||||||
|
/// use actix_jwt_session::*;
|
||||||
|
/// use std::sync::Arc;
|
||||||
|
/// use actix_web::HttpMessage;
|
||||||
|
/// use std::borrow::Cow;
|
||||||
|
///
|
||||||
|
/// # #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
/// # pub struct Claims { id: uuid::Uuid, sub: String }
|
||||||
|
/// # impl actix_jwt_session::Claims for Claims {
|
||||||
|
/// # fn jti(&self) -> uuid::Uuid { self.id }
|
||||||
|
/// # fn subject(&self) -> &str { &self.sub }
|
||||||
|
/// # }
|
||||||
|
///
|
||||||
|
/// #[derive(Debug, Clone, Copy, Default)]
|
||||||
|
/// struct ExampleExtractor;
|
||||||
|
///
|
||||||
|
/// #[async_trait::async_trait(?Send)]
|
||||||
|
/// impl SessionExtractor<Claims> for ExampleExtractor {
|
||||||
|
/// async fn extract_claims(
|
||||||
|
/// &self,
|
||||||
|
/// req: &mut ServiceRequest,
|
||||||
|
/// jwt_encoding_key: Arc<EncodingKey>,
|
||||||
|
/// jwt_decoding_key: Arc<DecodingKey>,
|
||||||
|
/// algorithm: Algorithm,
|
||||||
|
/// storage: SessionStorage,
|
||||||
|
/// ) -> Result<(), Error> {
|
||||||
|
/// if req.peer_addr().unwrap().ip().is_multicast() {
|
||||||
|
/// req.extensions_mut().insert(Authenticated {
|
||||||
|
/// claims: Arc::new(Claims { id: uuid::Uuid::default(), sub: "HUB".into() }),
|
||||||
|
/// jwt_encoding_key,
|
||||||
|
/// algorithm,
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// async fn extract_token_text<'req>(&self, req: &'req mut ServiceRequest) -> Option<Cow<'req, str>> { None }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
async fn extract_claims(
|
||||||
|
&self,
|
||||||
|
req: &mut ServiceRequest,
|
||||||
|
jwt_encoding_key: Arc<EncodingKey>,
|
||||||
|
jwt_decoding_key: Arc<DecodingKey>,
|
||||||
|
algorithm: Algorithm,
|
||||||
|
storage: SessionStorage,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let Some(as_str) = self.extract_token_text(req).await else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let decoded_claims = self.decode(&as_str, jwt_decoding_key, algorithm)?;
|
||||||
|
self.validate(&decoded_claims, storage).await?;
|
||||||
|
req.extensions_mut().insert(Authenticated {
|
||||||
|
claims: Arc::new(decoded_claims),
|
||||||
|
jwt_encoding_key,
|
||||||
|
algorithm,
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode encrypted JWT to structure
|
||||||
|
fn decode(
|
||||||
|
&self,
|
||||||
|
value: &str,
|
||||||
|
jwt_decoding_key: Arc<DecodingKey>,
|
||||||
|
algorithm: Algorithm,
|
||||||
|
) -> Result<ClaimsType, Error> {
|
||||||
|
let mut validation = Validation::new(algorithm);
|
||||||
|
validation.validate_exp = false;
|
||||||
|
validation.validate_nbf = false;
|
||||||
|
validation.leeway = 0;
|
||||||
|
validation.required_spec_claims.clear();
|
||||||
|
|
||||||
|
decode::<ClaimsType>(value, &jwt_decoding_key, &validation)
|
||||||
|
.map_err(|e| {
|
||||||
|
#[cfg(feature = "use-tracing")]
|
||||||
|
tracing::debug!("Failed to decode claims: {e:?}. {e}");
|
||||||
|
Error::CantDecode
|
||||||
|
})
|
||||||
|
.map(|t| t.claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate JWT Claims agains stored in storage tokens.
|
||||||
|
///
|
||||||
|
/// * Token must exists in storage
|
||||||
|
/// * Token must be exactly the same as token from storage
|
||||||
|
async fn validate(&self, claims: &ClaimsType, storage: SessionStorage) -> Result<(), Error> {
|
||||||
|
let stored = storage
|
||||||
|
.clone()
|
||||||
|
.find_jwt::<ClaimsType>(claims.jti())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
#[cfg(feature = "use-tracing")]
|
||||||
|
tracing::debug!(
|
||||||
|
"Failed to load {} from storage: {e:?}",
|
||||||
|
std::any::type_name::<ClaimsType>()
|
||||||
|
);
|
||||||
|
Error::LoadError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if &stored != claims {
|
||||||
|
#[cfg(feature = "use-tracing")]
|
||||||
|
tracing::debug!("{claims:?} != {stored:?}");
|
||||||
|
Err(Error::DontMatch)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lookup for session data as a string in [actix_web::dev::ServiceRequest]
|
||||||
|
///
|
||||||
|
/// If there's no token data in request you should returns `None`. This is not considered as an
|
||||||
|
/// error and until endpoint requires `Authenticated` this will not results in `401`.
|
||||||
|
async fn extract_token_text<'req>(
|
||||||
|
&self,
|
||||||
|
req: &'req mut ServiceRequest,
|
||||||
|
) -> Option<Cow<'req, str>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts JWT token from HTTP Request cookies. This extractor should be used when you can't set
|
||||||
|
/// your own header, for example when user enters http links to browser and you don't have any
|
||||||
|
/// advanced frontend.
|
||||||
|
///
|
||||||
|
/// This exractor is may be used by PWA application or micro services but [HeaderExtractor] is much
|
||||||
|
/// more suitable for this purpose.
|
||||||
|
pub struct CookieExtractor<ClaimsType> {
|
||||||
|
__ty: PhantomData<ClaimsType>,
|
||||||
|
cookie_name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ClaimsType: Claims> CookieExtractor<ClaimsType> {
|
||||||
|
/// Creates new cookie extractor.
|
||||||
|
/// It will extract token data from cookie with given name
|
||||||
|
pub fn new(cookie_name: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
__ty: Default::default(),
|
||||||
|
cookie_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl<ClaimsType: Claims> SessionExtractor<ClaimsType> for CookieExtractor<ClaimsType> {
|
||||||
|
async fn extract_token_text<'req>(
|
||||||
|
&self,
|
||||||
|
req: &'req mut ServiceRequest,
|
||||||
|
) -> Option<Cow<'req, str>> {
|
||||||
|
req.cookie(self.cookie_name)
|
||||||
|
.map(|c| c.value().to_string().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts JWT token from HTTP Request headers
|
||||||
|
///
|
||||||
|
/// This exractor is very useful for all PWA application or for micro services
|
||||||
|
/// because you can set your own headers while making http requests.
|
||||||
|
///
|
||||||
|
/// If you want to have users authorized using simple html anchor (tag A) you should use [CookieExtractor]
|
||||||
|
pub struct HeaderExtractor<ClaimsType> {
|
||||||
|
__ty: PhantomData<ClaimsType>,
|
||||||
|
header_name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ClaimsType: Claims> HeaderExtractor<ClaimsType> {
|
||||||
|
/// Creates new header extractor.
|
||||||
|
/// It will extract token data from header with given name
|
||||||
|
pub fn new(header_name: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
__ty: Default::default(),
|
||||||
|
header_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl<ClaimsType: Claims> SessionExtractor<ClaimsType> for HeaderExtractor<ClaimsType> {
|
||||||
|
async fn extract_token_text<'req>(
|
||||||
|
&self,
|
||||||
|
req: &'req mut ServiceRequest,
|
||||||
|
) -> Option<Cow<'req, str>> {
|
||||||
|
req.headers()
|
||||||
|
.get(self.header_name)
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.map(|h| h.to_owned().into())
|
||||||
|
}
|
||||||
|
}
|
41
crates/actix-jwt-session/src/hashing.rs
Normal file
41
crates/actix-jwt-session/src/hashing.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//! Encrypting and decrypting password
|
||||||
|
//!
|
||||||
|
//! This module is available by default or by enabling `hashing` feature.
|
||||||
|
//! Library docs covers using it in context of `register` and `sign in`.
|
||||||
|
|
||||||
|
use argon2::{
|
||||||
|
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||||
|
Argon2,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Encrypting and decrypting password
|
||||||
|
pub struct Hashing;
|
||||||
|
|
||||||
|
impl Hashing {
|
||||||
|
/// Takes password and returns encrypted hash with random salt
|
||||||
|
pub fn encrypt(password: &str) -> argon2::password_hash::Result<String> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
argon2
|
||||||
|
.hash_password(password.as_bytes(), &salt)
|
||||||
|
.map(|hash| hash.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes password hash and password and validates it.
|
||||||
|
pub fn verify(password_hash: &str, password: &str) -> argon2::password_hash::Result<()> {
|
||||||
|
let parsed_hash = PasswordHash::new(password_hash)?;
|
||||||
|
Argon2::default().verify_password(password.as_bytes(), &parsed_hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_always_random_salt() {
|
||||||
|
let pass = "ahs9dya8tsd7fa8tsa86tT&^R%^DS^%ARS&A";
|
||||||
|
let hash = Hashing::encrypt(pass).unwrap();
|
||||||
|
assert!(Hashing::verify(hash.as_str(), pass).is_ok());
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
295
crates/actix-jwt-session/src/middleware.rs
Normal file
295
crates/actix-jwt-session/src/middleware.rs
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
//! Create session storage and build middleware factory
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
pub use actix_web::cookie::time::{Duration, OffsetDateTime};
|
||||||
|
use actix_web::dev::Transform;
|
||||||
|
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse};
|
||||||
|
use futures_util::future::LocalBoxFuture;
|
||||||
|
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
|
||||||
|
use std::future::{ready, Ready};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Session middleware factory builder
|
||||||
|
///
|
||||||
|
/// It should be constructed with [SessionMiddlewareFactory::build].
|
||||||
|
pub struct SessionMiddlewareBuilder<ClaimsType: Claims> {
|
||||||
|
pub(crate) jwt_encoding_key: Arc<EncodingKey>,
|
||||||
|
pub(crate) jwt_decoding_key: Arc<DecodingKey>,
|
||||||
|
pub(crate) algorithm: Algorithm,
|
||||||
|
pub(crate) storage: Option<SessionStorage>,
|
||||||
|
pub(crate) jwt_extractors: Vec<Box<dyn SessionExtractor<ClaimsType>>>,
|
||||||
|
pub(crate) refresh_extractors: Vec<Box<dyn SessionExtractor<RefreshToken>>>,
|
||||||
|
}
|
||||||
|
impl<ClaimsType: Claims> SessionMiddlewareBuilder<ClaimsType> {
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(crate) fn new(
|
||||||
|
jwt_encoding_key: Arc<EncodingKey>,
|
||||||
|
jwt_decoding_key: Arc<DecodingKey>,
|
||||||
|
algorithm: Algorithm,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
jwt_encoding_key: jwt_encoding_key.clone(),
|
||||||
|
jwt_decoding_key,
|
||||||
|
algorithm,
|
||||||
|
storage: None,
|
||||||
|
jwt_extractors: vec![],
|
||||||
|
refresh_extractors: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set session storage to given instance. Good if for some reason you need to share 1 storage
|
||||||
|
/// with multiple instances of session middleware
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_storage(mut self, storage: SessionStorage) -> Self {
|
||||||
|
self.storage = Some(storage);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add cookie extractor for refresh token.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_refresh_cookie(mut self, name: &'static str) -> Self {
|
||||||
|
self.refresh_extractors
|
||||||
|
.push(Box::new(CookieExtractor::<RefreshToken>::new(name)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add header extractor for refresh token.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_refresh_header(mut self, name: &'static str) -> Self {
|
||||||
|
self.refresh_extractors
|
||||||
|
.push(Box::new(HeaderExtractor::<RefreshToken>::new(name)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add cookie extractor for json web token.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_jwt_cookie(mut self, name: &'static str) -> Self {
|
||||||
|
self.jwt_extractors
|
||||||
|
.push(Box::new(CookieExtractor::<ClaimsType>::new(name)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add header extractor for json web token.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_jwt_header(mut self, name: &'static str) -> Self {
|
||||||
|
self.jwt_extractors
|
||||||
|
.push(Box::new(HeaderExtractor::<ClaimsType>::new(name)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds middleware factory and returns session storage with factory
|
||||||
|
pub fn finish(self) -> (SessionStorage, SessionMiddlewareFactory<ClaimsType>) {
|
||||||
|
let Self {
|
||||||
|
storage,
|
||||||
|
jwt_encoding_key,
|
||||||
|
jwt_decoding_key,
|
||||||
|
algorithm,
|
||||||
|
jwt_extractors,
|
||||||
|
refresh_extractors,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
|
let storage = storage
|
||||||
|
.expect("Session storage must be constracted from pool or set from existing storage");
|
||||||
|
(
|
||||||
|
storage.clone(),
|
||||||
|
SessionMiddlewareFactory {
|
||||||
|
jwt_encoding_key,
|
||||||
|
jwt_decoding_key,
|
||||||
|
algorithm,
|
||||||
|
storage,
|
||||||
|
jwt_extractors: Arc::new(jwt_extractors),
|
||||||
|
refresh_extractors: Arc::new(refresh_extractors),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Factory creates middlware for every single request.
|
||||||
|
///
|
||||||
|
/// All fields here are immutable and have atomic access and only pointer is copied so are very cheap
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use std::sync::Arc;
|
||||||
|
/// use actix_jwt_session::*;
|
||||||
|
///
|
||||||
|
/// # async fn create<AppClaims: actix_jwt_session::Claims>() {
|
||||||
|
/// // create redis connection
|
||||||
|
/// let redis = {
|
||||||
|
/// use redis_async_pool::{RedisConnectionManager, RedisPool};
|
||||||
|
/// RedisPool::new(
|
||||||
|
/// RedisConnectionManager::new(
|
||||||
|
/// redis::Client::open("redis://localhost:6379").expect("Fail to connect to redis"),
|
||||||
|
/// true,
|
||||||
|
/// None,
|
||||||
|
/// ),
|
||||||
|
/// 5,
|
||||||
|
/// )
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // load or create new keys in `./config`
|
||||||
|
/// let keys = JwtSigningKeys::load_or_create();
|
||||||
|
///
|
||||||
|
/// // create new [SessionStorage] and [SessionMiddlewareFactory]
|
||||||
|
/// let (storage, factory) = SessionMiddlewareFactory::<AppClaims>::build(
|
||||||
|
/// Arc::new(keys.encoding_key),
|
||||||
|
/// Arc::new(keys.decoding_key),
|
||||||
|
/// Algorithm::EdDSA
|
||||||
|
/// )
|
||||||
|
/// // pass redis connection
|
||||||
|
/// .with_redis_pool(redis.clone())
|
||||||
|
/// // Check if header "Authorization" exists and contains Bearer with encoded JWT
|
||||||
|
/// .with_jwt_header("Authorization")
|
||||||
|
/// // Check if cookie "jwt" exists and contains encoded JWT
|
||||||
|
/// .with_jwt_cookie("acx-a")
|
||||||
|
/// .with_refresh_header("ACX-Refresh")
|
||||||
|
/// // Check if cookie "jwt" exists and contains encoded JWT
|
||||||
|
/// .with_refresh_cookie("acx-r")
|
||||||
|
/// .finish();
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SessionMiddlewareFactory<ClaimsType: Claims> {
|
||||||
|
pub(crate) jwt_encoding_key: Arc<EncodingKey>,
|
||||||
|
pub(crate) jwt_decoding_key: Arc<DecodingKey>,
|
||||||
|
pub(crate) algorithm: Algorithm,
|
||||||
|
pub(crate) storage: SessionStorage,
|
||||||
|
pub(crate) jwt_extractors: Arc<Vec<Box<dyn SessionExtractor<ClaimsType>>>>,
|
||||||
|
pub(crate) refresh_extractors: Arc<Vec<Box<dyn SessionExtractor<RefreshToken>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ClaimsType: Claims> SessionMiddlewareFactory<ClaimsType> {
|
||||||
|
pub fn build(
|
||||||
|
jwt_encoding_key: Arc<EncodingKey>,
|
||||||
|
jwt_decoding_key: Arc<DecodingKey>,
|
||||||
|
algorithm: Algorithm,
|
||||||
|
) -> SessionMiddlewareBuilder<ClaimsType> {
|
||||||
|
SessionMiddlewareBuilder::new(jwt_encoding_key, jwt_decoding_key, algorithm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B, ClaimsType> Transform<S, ServiceRequest> for SessionMiddlewareFactory<ClaimsType>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
|
||||||
|
ClaimsType: Claims,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Transform = SessionMiddleware<S, ClaimsType>;
|
||||||
|
type InitError = ();
|
||||||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
ready(Ok(SessionMiddleware {
|
||||||
|
service: Rc::new(service),
|
||||||
|
storage: self.storage.clone(),
|
||||||
|
jwt_encoding_key: self.jwt_encoding_key.clone(),
|
||||||
|
jwt_decoding_key: self.jwt_decoding_key.clone(),
|
||||||
|
algorithm: self.algorithm,
|
||||||
|
jwt_extractors: self.jwt_extractors.clone(),
|
||||||
|
refresh_extractors: self.refresh_extractors.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct SessionMiddleware<S, ClaimsType>
|
||||||
|
where
|
||||||
|
ClaimsType: Claims,
|
||||||
|
{
|
||||||
|
pub(crate) service: Rc<S>,
|
||||||
|
pub(crate) jwt_encoding_key: Arc<EncodingKey>,
|
||||||
|
pub(crate) jwt_decoding_key: Arc<DecodingKey>,
|
||||||
|
pub(crate) algorithm: Algorithm,
|
||||||
|
pub(crate) storage: SessionStorage,
|
||||||
|
pub(crate) jwt_extractors: Arc<Vec<Box<dyn SessionExtractor<ClaimsType>>>>,
|
||||||
|
pub(crate) refresh_extractors: Arc<Vec<Box<dyn SessionExtractor<RefreshToken>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, ClaimsType: Claims> SessionMiddleware<S, ClaimsType> {
|
||||||
|
async fn extract_token<C: Claims>(
|
||||||
|
req: &mut ServiceRequest,
|
||||||
|
jwt_encoding_key: Arc<EncodingKey>,
|
||||||
|
jwt_decoding_key: Arc<DecodingKey>,
|
||||||
|
algorithm: Algorithm,
|
||||||
|
storage: SessionStorage,
|
||||||
|
extractors: &[Box<dyn SessionExtractor<C>>],
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut last_error = None;
|
||||||
|
for extractor in extractors.iter() {
|
||||||
|
match extractor
|
||||||
|
.extract_claims(
|
||||||
|
req,
|
||||||
|
jwt_encoding_key.clone(),
|
||||||
|
jwt_decoding_key.clone(),
|
||||||
|
algorithm,
|
||||||
|
storage.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => break,
|
||||||
|
Err(e) => {
|
||||||
|
last_error = Some(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(e) = last_error {
|
||||||
|
return Err(e)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B, ClaimsType> Service<ServiceRequest> for SessionMiddleware<S, ClaimsType>
|
||||||
|
where
|
||||||
|
ClaimsType: Claims,
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||||
|
use futures_lite::FutureExt;
|
||||||
|
|
||||||
|
let svc = self.service.clone();
|
||||||
|
let jwt_decoding_key = self.jwt_decoding_key.clone();
|
||||||
|
let jwt_encoding_key = self.jwt_encoding_key.clone();
|
||||||
|
let algorithm = self.algorithm;
|
||||||
|
let storage = self.storage.clone();
|
||||||
|
let jwt_extractors = self.jwt_extractors.clone();
|
||||||
|
let refresh_extractors = self.refresh_extractors.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
if !jwt_extractors.is_empty() {
|
||||||
|
Self::extract_token(
|
||||||
|
&mut req,
|
||||||
|
jwt_encoding_key.clone(),
|
||||||
|
jwt_decoding_key.clone(),
|
||||||
|
algorithm,
|
||||||
|
storage.clone(),
|
||||||
|
&jwt_extractors,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
if !refresh_extractors.is_empty() {
|
||||||
|
Self::extract_token(
|
||||||
|
&mut req,
|
||||||
|
jwt_encoding_key,
|
||||||
|
jwt_decoding_key,
|
||||||
|
algorithm,
|
||||||
|
storage,
|
||||||
|
&refresh_extractors,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
let res = svc.call(req).await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
.boxed_local()
|
||||||
|
}
|
||||||
|
}
|
@ -7,14 +7,10 @@
|
|||||||
//! [RedisStorage] is constructed by [RedisMiddlewareFactory] from [redis_async_pool::RedisPool] and shared
|
//! [RedisStorage] is constructed by [RedisMiddlewareFactory] from [redis_async_pool::RedisPool] and shared
|
||||||
//! between all [RedisMiddleware] instances.
|
//! between all [RedisMiddleware] instances.
|
||||||
|
|
||||||
use super::*;
|
use crate::*;
|
||||||
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
|
|
||||||
use futures_util::future::LocalBoxFuture;
|
|
||||||
use redis::aio::ConnectionLike;
|
use redis::aio::ConnectionLike;
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
use std::future::{ready, Ready};
|
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Redis implementation for [TokenStorage]
|
/// Redis implementation for [TokenStorage]
|
||||||
@ -40,10 +36,16 @@ where
|
|||||||
{
|
{
|
||||||
async fn get_by_jti(self: Arc<Self>, jti: &[u8]) -> Result<Vec<u8>, Error> {
|
async fn get_by_jti(self: Arc<Self>, jti: &[u8]) -> Result<Vec<u8>, Error> {
|
||||||
let pool = self.pool.clone();
|
let pool = self.pool.clone();
|
||||||
let mut conn = pool.get().await.map_err(|_| Error::RedisConn)?;
|
let mut conn = pool.get().await.map_err(|e| {
|
||||||
conn.get::<_, Vec<u8>>(jti)
|
#[cfg(feature = "use-tracing")]
|
||||||
.await
|
tracing::error!("Unable to obtain redis connection: {e}");
|
||||||
.map_err(|_| Error::NotFound)
|
Error::RedisConn
|
||||||
|
})?;
|
||||||
|
conn.get::<_, Vec<u8>>(jti).await.map_err(|e| {
|
||||||
|
#[cfg(feature = "use-tracing")]
|
||||||
|
tracing::error!("Session record not found in redis: {e}");
|
||||||
|
Error::NotFound
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_by_jti(
|
async fn set_by_jti(
|
||||||
@ -51,149 +53,143 @@ where
|
|||||||
jwt_jti: &[u8],
|
jwt_jti: &[u8],
|
||||||
refresh_jti: &[u8],
|
refresh_jti: &[u8],
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
exp: std::time::Duration,
|
mut exp: Duration,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
bad_ttl!(
|
||||||
|
exp,
|
||||||
|
Duration::seconds(1),
|
||||||
|
"Expiration time is bellow 1s. This is not allowed for redis server."
|
||||||
|
);
|
||||||
let pool = self.pool.clone();
|
let pool = self.pool.clone();
|
||||||
let mut conn = pool.get().await.map_err(|_| Error::RedisConn)?;
|
let mut conn = pool.get().await.map_err(|e| {
|
||||||
|
#[cfg(feature = "use-tracing")]
|
||||||
|
tracing::error!("Unable to obtain redis connection: {e}");
|
||||||
|
Error::RedisConn
|
||||||
|
})?;
|
||||||
let mut pipeline = redis::Pipeline::new();
|
let mut pipeline = redis::Pipeline::new();
|
||||||
pipeline
|
pipeline
|
||||||
.set_ex(jwt_jti, bytes, exp.as_secs() as usize)
|
.set_ex(jwt_jti, bytes, exp.as_seconds_f32() as usize)
|
||||||
.set_ex(refresh_jti, bytes, exp.as_secs() as usize);
|
.set_ex(refresh_jti, bytes, exp.as_seconds_f32() as usize);
|
||||||
conn.req_packed_commands(&pipeline, 0, 2)
|
conn.req_packed_commands(&pipeline, 0, 2)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::WriteFailed)?;
|
.map_err(|e| {
|
||||||
|
#[cfg(feature = "use-tracing")]
|
||||||
|
tracing::error!("Failed to save session in redis: {e}");
|
||||||
|
Error::WriteFailed
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_by_jti(self: Arc<Self>, jti: &[u8]) -> Result<(), Error> {
|
async fn remove_by_jti(self: Arc<Self>, jti: &[u8]) -> Result<(), Error> {
|
||||||
let pool = self.pool.clone();
|
let pool = self.pool.clone();
|
||||||
let mut conn = pool.get().await.map_err(|_| Error::RedisConn)?;
|
let mut conn = pool.get().await.map_err(|e| {
|
||||||
conn.del(jti).await.map_err(|_| Error::NotFound)?;
|
#[cfg(feature = "use-tracing")]
|
||||||
|
tracing::error!("Unable to obtain redis connection: {e}");
|
||||||
|
Error::RedisConn
|
||||||
|
})?;
|
||||||
|
conn.del(jti).await.map_err(|e| {
|
||||||
|
#[cfg(feature = "use-tracing")]
|
||||||
|
tracing::error!("Session record can't be removed from redis: {e}");
|
||||||
|
Error::NotFound
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RedisMiddleware<S, ClaimsType>
|
impl<ClaimsType: Claims> SessionMiddlewareBuilder<ClaimsType> {
|
||||||
where
|
#[must_use]
|
||||||
ClaimsType: Claims,
|
pub fn with_redis_pool(mut self, pool: redis_async_pool::RedisPool) -> Self {
|
||||||
{
|
|
||||||
_claims_type_marker: std::marker::PhantomData<ClaimsType>,
|
|
||||||
service: Rc<S>,
|
|
||||||
jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
algorithm: Algorithm,
|
|
||||||
storage: SessionStorage<ClaimsType>,
|
|
||||||
extractors: Arc<Vec<Box<dyn SessionExtractor<ClaimsType>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, B, ClaimsType> Service<ServiceRequest> for RedisMiddleware<S, ClaimsType>
|
|
||||||
where
|
|
||||||
ClaimsType: Claims,
|
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
|
|
||||||
{
|
|
||||||
type Response = ServiceResponse<B>;
|
|
||||||
type Error = actix_web::Error;
|
|
||||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
|
||||||
|
|
||||||
forward_ready!(service);
|
|
||||||
|
|
||||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
|
||||||
use futures_lite::FutureExt;
|
|
||||||
|
|
||||||
let svc = self.service.clone();
|
|
||||||
let jwt_decoding_key = self.jwt_decoding_key.clone();
|
|
||||||
let jwt_encoding_key = self.jwt_encoding_key.clone();
|
|
||||||
let algorithm = self.algorithm;
|
|
||||||
let storage = self.storage.clone();
|
|
||||||
let extractors = self.extractors.clone();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let mut last_error = None;
|
|
||||||
for extractor in extractors.iter() {
|
|
||||||
match extractor
|
|
||||||
.extract_jwt(
|
|
||||||
&req,
|
|
||||||
jwt_encoding_key.clone(),
|
|
||||||
jwt_decoding_key.clone(),
|
|
||||||
algorithm,
|
|
||||||
storage.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => break,
|
|
||||||
Err(e) => {
|
|
||||||
last_error = Some(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if let Some(e) = last_error {
|
|
||||||
return Err(e)?;
|
|
||||||
}
|
|
||||||
let res = svc.call(req).await?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
.boxed_local()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RedisMiddlewareFactory<ClaimsType: Claims> {
|
|
||||||
jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
algorithm: Algorithm,
|
|
||||||
storage: SessionStorage<ClaimsType>,
|
|
||||||
extractors: Arc<Vec<Box<dyn SessionExtractor<ClaimsType>>>>,
|
|
||||||
_claims_type_marker: PhantomData<ClaimsType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<ClaimsType: Claims> RedisMiddlewareFactory<ClaimsType> {
|
|
||||||
pub fn new(
|
|
||||||
jwt_encoding_key: Arc<EncodingKey>,
|
|
||||||
jwt_decoding_key: Arc<DecodingKey>,
|
|
||||||
algorithm: Algorithm,
|
|
||||||
pool: redis_async_pool::RedisPool,
|
|
||||||
extractors: Vec<Box<dyn SessionExtractor<ClaimsType>>>,
|
|
||||||
) -> (SessionStorage<ClaimsType>, Self) {
|
|
||||||
let storage = Arc::new(RedisStorage::<ClaimsType>::new(pool));
|
let storage = Arc::new(RedisStorage::<ClaimsType>::new(pool));
|
||||||
let storage = SessionStorage::new(storage, jwt_encoding_key.clone(), algorithm);
|
let storage = SessionStorage::new(storage, self.jwt_encoding_key.clone(), self.algorithm);
|
||||||
(
|
self.storage = Some(storage);
|
||||||
storage.clone(),
|
self
|
||||||
Self {
|
}
|
||||||
jwt_encoding_key: jwt_encoding_key.clone(),
|
}
|
||||||
jwt_decoding_key,
|
|
||||||
algorithm,
|
#[cfg(test)]
|
||||||
storage,
|
mod tests {
|
||||||
extractors: Arc::new(extractors),
|
use actix_web::cookie::time::*;
|
||||||
_claims_type_marker: Default::default(),
|
|
||||||
},
|
use super::*;
|
||||||
|
use std::ops::Add;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct Claims {
|
||||||
|
#[serde(rename = "exp")]
|
||||||
|
pub expires_at: usize,
|
||||||
|
#[serde(rename = "iat")]
|
||||||
|
pub issues_at: usize,
|
||||||
|
/// Account login
|
||||||
|
#[serde(rename = "sub")]
|
||||||
|
pub subject: String,
|
||||||
|
#[serde(rename = "aud")]
|
||||||
|
pub audience: String,
|
||||||
|
#[serde(rename = "jti")]
|
||||||
|
pub jwt_id: uuid::Uuid,
|
||||||
|
#[serde(rename = "aci")]
|
||||||
|
pub account_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::Claims for Claims {
|
||||||
|
fn jti(&self) -> uuid::Uuid {
|
||||||
|
self.jwt_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subject(&self) -> &str {
|
||||||
|
&self.subject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_storage() -> (SessionStorage, SessionMiddlewareFactory<Claims>) {
|
||||||
|
let redis = {
|
||||||
|
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
||||||
|
RedisPool::new(
|
||||||
|
RedisConnectionManager::new(
|
||||||
|
redis::Client::open("redis://localhost:6379")
|
||||||
|
.expect("Fail to connect to redis"),
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
5,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let jwt_signing_keys = JwtSigningKeys::generate(false).unwrap();
|
||||||
|
SessionMiddlewareFactory::<Claims>::build(
|
||||||
|
Arc::new(jwt_signing_keys.encoding_key),
|
||||||
|
Arc::new(jwt_signing_keys.decoding_key),
|
||||||
|
Algorithm::EdDSA,
|
||||||
)
|
)
|
||||||
|
.with_redis_pool(redis)
|
||||||
|
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
||||||
|
.with_refresh_header(REFRESH_HEADER_NAME)
|
||||||
|
.with_jwt_cookie(JWT_COOKIE_NAME)
|
||||||
|
.with_jwt_header(JWT_HEADER_NAME)
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn storage(&self) -> SessionStorage<ClaimsType> {
|
#[tokio::test]
|
||||||
self.storage.clone()
|
async fn check_encode() {
|
||||||
}
|
let (store, _) = create_storage().await;
|
||||||
}
|
let jwt_exp = JwtTtl(Duration::days(31));
|
||||||
|
let refresh_exp = RefreshTtl(Duration::days(31));
|
||||||
impl<S, B, ClaimsType> Transform<S, ServiceRequest> for RedisMiddlewareFactory<ClaimsType>
|
|
||||||
where
|
let original = Claims {
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
|
subject: "me".into(),
|
||||||
ClaimsType: Claims,
|
expires_at: OffsetDateTime::now_utc()
|
||||||
{
|
.add(Duration::days(31))
|
||||||
type Response = ServiceResponse<B>;
|
.unix_timestamp() as usize,
|
||||||
type Error = actix_web::Error;
|
issues_at: OffsetDateTime::now_utc().unix_timestamp() as usize,
|
||||||
type Transform = RedisMiddleware<S, ClaimsType>;
|
audience: "web".into(),
|
||||||
type InitError = ();
|
jwt_id: Uuid::new_v4(),
|
||||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
account_id: 24234,
|
||||||
|
};
|
||||||
fn new_transform(&self, service: S) -> Self::Future {
|
|
||||||
ready(Ok(RedisMiddleware {
|
store
|
||||||
service: Rc::new(service),
|
.store(original.clone(), jwt_exp, refresh_exp)
|
||||||
storage: self.storage.clone(),
|
.await
|
||||||
jwt_encoding_key: self.jwt_encoding_key.clone(),
|
.unwrap();
|
||||||
jwt_decoding_key: self.jwt_decoding_key.clone(),
|
let loaded = store.find_jwt(original.jwt_id).await.unwrap();
|
||||||
algorithm: self.algorithm,
|
assert_eq!(original, loaded);
|
||||||
extractors: self.extractors.clone(),
|
|
||||||
_claims_type_marker: PhantomData,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use actix_jwt_session::{
|
use actix_jwt_session::*;
|
||||||
Authenticated, HeaderExtractor, RedisMiddlewareFactory, SessionStorage, JWT_HEADER_NAME,
|
use actix_web::dev::ServiceResponse;
|
||||||
};
|
use actix_web::http::{Method, StatusCode};
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::web::{Data, Json};
|
use actix_web::web::{Data, Json};
|
||||||
use actix_web::HttpResponse;
|
use actix_web::HttpResponse;
|
||||||
use actix_web::{get, post};
|
use actix_web::{get, post};
|
||||||
use actix_web::{http::header::ContentType, test, App};
|
use actix_web::{http::header::ContentType, test, App};
|
||||||
use jsonwebtoken::*;
|
|
||||||
use ring::rand::SystemRandom;
|
|
||||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -30,7 +26,7 @@ impl actix_jwt_session::Claims for Claims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn not_authenticated() {
|
async fn full_flow() {
|
||||||
let redis = {
|
let redis = {
|
||||||
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
use redis_async_pool::{RedisConnectionManager, RedisPool};
|
||||||
RedisPool::new(
|
RedisPool::new(
|
||||||
@ -43,26 +39,36 @@ async fn not_authenticated() {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let keys = JwtSigningKeys::generate().unwrap();
|
let keys = JwtSigningKeys::generate(false).unwrap();
|
||||||
let factory = RedisMiddlewareFactory::<Claims>::new(
|
let (storage, factory) = SessionMiddlewareFactory::<Claims>::build(
|
||||||
Arc::new(keys.encoding_key),
|
Arc::new(keys.encoding_key),
|
||||||
Arc::new(keys.decoding_key),
|
Arc::new(keys.decoding_key),
|
||||||
Algorithm::EdDSA,
|
Algorithm::EdDSA,
|
||||||
redis.clone(),
|
)
|
||||||
vec![Box::new(HeaderExtractor::new(JWT_HEADER_NAME))],
|
.with_redis_pool(redis.clone())
|
||||||
);
|
.with_jwt_header(JWT_HEADER_NAME)
|
||||||
|
.with_refresh_header(REFRESH_HEADER_NAME)
|
||||||
|
.with_jwt_cookie(JWT_COOKIE_NAME)
|
||||||
|
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
||||||
|
.finish();
|
||||||
|
|
||||||
let app = App::new()
|
let app = App::new()
|
||||||
.app_data(Data::new(factory.storage()))
|
.app_data(Data::new(storage.clone()))
|
||||||
.wrap(factory.clone())
|
.wrap(factory.clone())
|
||||||
.app_data(Data::new(redis.clone()))
|
.app_data(Data::new(redis.clone()))
|
||||||
|
.app_data(Data::new(JwtTtl(Duration::seconds(1))))
|
||||||
|
.app_data(Data::new(RefreshTtl(Duration::seconds(30))))
|
||||||
.service(sign_in)
|
.service(sign_in)
|
||||||
.service(sign_out)
|
.service(sign_out)
|
||||||
.service(session)
|
.service(session)
|
||||||
|
.service(refresh_session)
|
||||||
.service(root);
|
.service(root);
|
||||||
|
|
||||||
let app = actix_web::test::init_service(app).await;
|
let app = actix_web::test::init_service(app).await;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Assert authorization is ignored when token is not needed
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
let res = test::call_service(
|
let res = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
test::TestRequest::default()
|
test::TestRequest::default()
|
||||||
@ -72,24 +78,25 @@ async fn not_authenticated() {
|
|||||||
.await;
|
.await;
|
||||||
assert!(res.status().is_success());
|
assert!(res.status().is_success());
|
||||||
|
|
||||||
let res = test::call_service(
|
// -----------------------------------------------------------------
|
||||||
&app,
|
// Assert signed out when active session
|
||||||
test::TestRequest::default()
|
// -----------------------------------------------------------------
|
||||||
.uri("/s")
|
let res = test::call_service(&app, session_request("", "").to_request()).await;
|
||||||
.insert_header(ContentType::plaintext())
|
|
||||||
.to_request(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
|
||||||
let origina_claims = Claims {
|
let origina_claims = Claims {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
subject: "foo".to_string(),
|
subject: "foo".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Create session
|
||||||
|
// ----------------------------------------------
|
||||||
|
println!("-> Creating session");
|
||||||
let res = test::call_service(
|
let res = test::call_service(
|
||||||
&app,
|
&app,
|
||||||
test::TestRequest::default()
|
test::TestRequest::default()
|
||||||
.uri("/in")
|
.uri("/session/sign-in")
|
||||||
.method(actix_web::http::Method::POST)
|
.method(actix_web::http::Method::POST)
|
||||||
.insert_header(ContentType::json())
|
.insert_header(ContentType::json())
|
||||||
.set_json(&origina_claims)
|
.set_json(&origina_claims)
|
||||||
@ -97,52 +104,157 @@ async fn not_authenticated() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(res.status(), StatusCode::OK);
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
println!(" <- OK");
|
||||||
|
|
||||||
|
let auth_bearer = res
|
||||||
|
.headers()
|
||||||
|
.get(JWT_HEADER_NAME)
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
|
let refresh_bearer = res
|
||||||
|
.headers()
|
||||||
|
.get(REFRESH_HEADER_NAME)
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Assert signed in
|
||||||
|
// ----------------------------------------------
|
||||||
|
println!("-> Assert signed in");
|
||||||
|
let res = test::call_service(
|
||||||
|
&app,
|
||||||
|
session_request(&auth_bearer, &refresh_bearer).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
println!(" <- OK");
|
||||||
|
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Access Token TTL expires
|
||||||
|
// ----------------------------------------------
|
||||||
|
println!("-> Access Token TTL expires");
|
||||||
|
let res = test::try_call_service(
|
||||||
|
&app,
|
||||||
|
session_request(&auth_bearer, &refresh_bearer).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
expect_invalid_session(res);
|
||||||
|
println!(" <- OK");
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Refresh token
|
||||||
|
// ----------------------------------------------
|
||||||
|
println!("-> Refresh token");
|
||||||
|
let res = test::call_service(
|
||||||
|
&app,
|
||||||
|
test::TestRequest::default()
|
||||||
|
.uri("/session/refresh")
|
||||||
|
.method(Method::GET)
|
||||||
|
.insert_header((REFRESH_HEADER_NAME, refresh_bearer))
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
println!(" <- OK");
|
||||||
|
|
||||||
|
// ----------------------------------------------
|
||||||
|
// Logout
|
||||||
|
// ----------------------------------------------
|
||||||
|
println!("-> Logout");
|
||||||
|
let res = test::call_service(
|
||||||
|
&app,
|
||||||
|
test::TestRequest::default()
|
||||||
|
.uri("/session/sign-out")
|
||||||
|
.method(Method::POST)
|
||||||
|
.insert_header((JWT_HEADER_NAME, auth_bearer))
|
||||||
|
.insert_header((REFRESH_HEADER_NAME, refresh_bearer))
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
println!(" <- OK");
|
||||||
|
|
||||||
|
// --------------------------------------------------------------
|
||||||
|
// Assert signed out - session destroyed
|
||||||
|
// --------------------------------------------------------------
|
||||||
|
println!("-> Assert signed out - session destroyed");
|
||||||
|
let res = test::try_call_service(
|
||||||
|
&app,
|
||||||
|
session_request(&auth_bearer, &refresh_bearer).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
expect_invalid_session(res);
|
||||||
|
println!(" <- OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/in")]
|
#[post("/session/sign-in")]
|
||||||
async fn sign_in(
|
async fn sign_in(
|
||||||
store: Data<SessionStorage<Claims>>,
|
store: Data<SessionStorage>,
|
||||||
claims: Json<Claims>,
|
claims: Json<Claims>,
|
||||||
|
jwt_ttl: Data<JwtTtl>,
|
||||||
|
refresh_ttl: Data<RefreshTtl>,
|
||||||
) -> Result<HttpResponse, actix_web::Error> {
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
let claims = claims.into_inner();
|
let claims = claims.into_inner();
|
||||||
let store = store.into_inner();
|
let store = store.into_inner();
|
||||||
store
|
let pair = store
|
||||||
.clone()
|
.clone()
|
||||||
.set_by_jti(claims, std::time::Duration::from_secs(300))
|
.store(claims, *jwt_ttl.into_inner(), *refresh_ttl.into_inner())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::Ok()
|
||||||
|
.append_header((JWT_HEADER_NAME, pair.jwt.encode().unwrap()))
|
||||||
|
.append_header((REFRESH_HEADER_NAME, pair.refresh.encode().unwrap()))
|
||||||
|
.finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/out")]
|
#[post("/session/sign-out")]
|
||||||
async fn sign_out(_store: Data<SessionStorage<Claims>>) -> HttpResponse {
|
async fn sign_out(store: Data<SessionStorage>, auth: Authenticated<Claims>) -> HttpResponse {
|
||||||
HttpResponse::Ok().body("")
|
let store = store.into_inner();
|
||||||
|
store.erase::<Claims>(auth.id).await.unwrap();
|
||||||
|
HttpResponse::Ok().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/s")]
|
#[get("/session/info")]
|
||||||
async fn session(auth: Authenticated<Claims>) -> HttpResponse {
|
async fn session(auth: Authenticated<Claims>) -> HttpResponse {
|
||||||
HttpResponse::Ok().json(&*auth)
|
HttpResponse::Ok().json(&*auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/session/refresh")]
|
||||||
|
async fn refresh_session(
|
||||||
|
auth: Authenticated<RefreshToken>,
|
||||||
|
storage: Data<SessionStorage>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let storage = storage.into_inner();
|
||||||
|
storage.refresh::<Claims>(auth.refresh_jti).await.unwrap();
|
||||||
|
HttpResponse::Ok().json(&*auth)
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn root() -> HttpResponse {
|
async fn root() -> HttpResponse {
|
||||||
HttpResponse::Ok().body("")
|
HttpResponse::Ok().finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct JwtSigningKeys {
|
fn session_request(auth_bearer: &str, refresh_bearer: &str) -> actix_web::test::TestRequest {
|
||||||
encoding_key: EncodingKey,
|
let req = test::TestRequest::default()
|
||||||
decoding_key: DecodingKey,
|
.uri("/session/info")
|
||||||
}
|
.method(Method::GET);
|
||||||
|
if !auth_bearer.is_empty() {
|
||||||
impl JwtSigningKeys {
|
req.insert_header((JWT_HEADER_NAME, auth_bearer))
|
||||||
fn generate() -> Result<Self, Box<dyn std::error::Error>> {
|
.insert_header((REFRESH_HEADER_NAME, refresh_bearer))
|
||||||
let doc = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?;
|
} else {
|
||||||
let keypair = Ed25519KeyPair::from_pkcs8(doc.as_ref())?;
|
req
|
||||||
let encoding_key = EncodingKey::from_ed_der(doc.as_ref());
|
|
||||||
let decoding_key = DecodingKey::from_ed_der(keypair.public_key().as_ref());
|
|
||||||
Ok(JwtSigningKeys {
|
|
||||||
encoding_key,
|
|
||||||
decoding_key,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn expect_invalid_session(res: Result<ServiceResponse, actix_web::Error>) {
|
||||||
|
let err = res
|
||||||
|
.expect_err("Must be unauthorized")
|
||||||
|
.as_error::<actix_jwt_session::Error>()
|
||||||
|
.expect("Must be authorization error")
|
||||||
|
.clone();
|
||||||
|
assert_eq!(err, actix_jwt_session::Error::LoadError);
|
||||||
|
}
|
||||||
|
@ -54,6 +54,7 @@ struct AllPartialParkingSpace {
|
|||||||
parking_space_rents: Vec<parking_space_rents::Model>,
|
parking_space_rents: Vec<parking_space_rents::Model>,
|
||||||
parking_space_by_id: HashMap<i32, parking_spaces::Model>,
|
parking_space_by_id: HashMap<i32, parking_spaces::Model>,
|
||||||
account_by_id: HashMap<i32, accounts::Model>,
|
account_by_id: HashMap<i32, accounts::Model>,
|
||||||
|
#[allow(dead_code)]
|
||||||
locations: Vec<parking_space_locations::Model>,
|
locations: Vec<parking_space_locations::Model>,
|
||||||
session: Option<SessionOpts>,
|
session: Option<SessionOpts>,
|
||||||
}
|
}
|
||||||
@ -225,7 +226,7 @@ async fn create(
|
|||||||
|
|
||||||
let model = ActiveModel {
|
let model = ActiveModel {
|
||||||
id: NotSet,
|
id: NotSet,
|
||||||
location_id: Set(Some(location_id.clone() as i32)),
|
location_id: Set(Some(location_id as i32)),
|
||||||
spot: Set(spot.map(|n| n as i32)),
|
spot: Set(spot.map(|n| n as i32)),
|
||||||
account_id: Set(session.account_id()),
|
account_id: Set(session.account_id()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
use argon2::{
|
|
||||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
|
||||||
Argon2,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn encrypt(password: &str) -> argon2::password_hash::Result<String> {
|
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
|
||||||
let argon2 = Argon2::default();
|
|
||||||
argon2
|
|
||||||
.hash_password(password.as_bytes(), &salt)
|
|
||||||
.map(|hash| hash.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(password_hash: &str, password: &str) -> argon2::password_hash::Result<()> {
|
|
||||||
let parsed_hash = PasswordHash::new(&password_hash)?;
|
|
||||||
Argon2::default().verify_password(password.as_bytes(), &parsed_hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn check_always_random_salt() {
|
|
||||||
let pass = "ahs9dya8tsd7fa8tsa86tT&^R%^DS^%ARS&A";
|
|
||||||
let hash = encrypt(pass).unwrap();
|
|
||||||
assert!(verify(hash.as_str(), pass).is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +1,21 @@
|
|||||||
use std::ops::Add;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use actix_jwt_session::{
|
pub use actix_jwt_session::*;
|
||||||
CookieExtractor, HeaderExtractor, JwtSigningKeys, SessionStorage, JWT_HEADER_NAME,
|
use actix_web::cookie::Cookie;
|
||||||
};
|
|
||||||
pub use actix_jwt_session::{Error, RedisMiddlewareFactory};
|
|
||||||
use actix_web::web::{Data, Form, ServiceConfig};
|
use actix_web::web::{Data, Form, ServiceConfig};
|
||||||
use actix_web::{get, post, HttpRequest, HttpResponse};
|
use actix_web::{get, post, HttpRequest, HttpResponse};
|
||||||
use askama_actix::Template;
|
use askama_actix::Template;
|
||||||
use autometrics::autometrics;
|
use autometrics::autometrics;
|
||||||
use garde::Validate;
|
use garde::Validate;
|
||||||
use jsonwebtoken::*;
|
|
||||||
use oswilno_view::{Blank, Errors, Lang, Layout, Main, MainOpts, TranslationStorage};
|
use oswilno_view::{Blank, Errors, Lang, Layout, Main, MainOpts, TranslationStorage};
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
mod hashing;
|
|
||||||
|
|
||||||
pub use oswilno_view::filters;
|
pub use oswilno_view::filters;
|
||||||
|
|
||||||
pub type Authenticated = actix_jwt_session::Authenticated<Claims>;
|
pub type Authenticated = actix_jwt_session::Authenticated<Claims>;
|
||||||
pub type MaybeAuthenticated = actix_jwt_session::MaybeAuthenticated<Claims>;
|
pub type MaybeAuthenticated = actix_jwt_session::MaybeAuthenticated<Claims>;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct JWTTtl(std::time::Duration);
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct RefreshTtl(std::time::Duration);
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Audience {
|
pub enum Audience {
|
||||||
@ -39,7 +26,7 @@ pub enum Audience {
|
|||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
#[serde(rename = "exp")]
|
#[serde(rename = "exp")]
|
||||||
pub expires_at: usize,
|
pub expiration_time: u64,
|
||||||
#[serde(rename = "iat")]
|
#[serde(rename = "iat")]
|
||||||
pub issues_at: usize,
|
pub issues_at: usize,
|
||||||
/// Account login
|
/// Account login
|
||||||
@ -51,6 +38,8 @@ pub struct Claims {
|
|||||||
pub jwt_id: uuid::Uuid,
|
pub jwt_id: uuid::Uuid,
|
||||||
#[serde(rename = "aci")]
|
#[serde(rename = "aci")]
|
||||||
pub account_id: i32,
|
pub account_id: i32,
|
||||||
|
#[serde(rename = "nbf")]
|
||||||
|
pub not_before: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl actix_jwt_session::Claims for Claims {
|
impl actix_jwt_session::Claims for Claims {
|
||||||
@ -80,10 +69,10 @@ pub struct LoginResponse {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SessionConfigurator {
|
pub struct SessionConfigurator {
|
||||||
jwt_ttl: Data<JWTTtl>,
|
jwt_ttl: Data<JwtTtl>,
|
||||||
refresh_ttl: Data<RefreshTtl>,
|
refresh_ttl: Data<RefreshTtl>,
|
||||||
factory: RedisMiddlewareFactory<Claims>,
|
factory: SessionMiddlewareFactory<Claims>,
|
||||||
session_storage: SessionStorage<Claims>,
|
session_storage: SessionStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionConfigurator {
|
impl SessionConfigurator {
|
||||||
@ -97,10 +86,11 @@ impl SessionConfigurator {
|
|||||||
.service(logout)
|
.service(logout)
|
||||||
.service(session_info)
|
.service(session_info)
|
||||||
.service(register)
|
.service(register)
|
||||||
.service(register_view);
|
.service(register_view)
|
||||||
|
.service(refresh_token);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn factory(&self) -> RedisMiddlewareFactory<Claims> {
|
pub fn factory(&self) -> SessionMiddlewareFactory<Claims> {
|
||||||
self.factory.clone()
|
self.factory.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,22 +117,23 @@ impl SessionConfigurator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(redis: redis_async_pool::RedisPool) -> Self {
|
pub fn new(redis: redis_async_pool::RedisPool) -> Self {
|
||||||
let jwt_ttl = JWTTtl(std::time::Duration::from_secs(31 * 60 * 60));
|
let jwt_ttl = JwtTtl(Duration::days(31));
|
||||||
let refresh_ttl = RefreshTtl(std::time::Duration::from_secs(6 * 31 * 60 * 60));
|
let refresh_ttl = RefreshTtl(Duration::days(6 * 31));
|
||||||
|
|
||||||
std::fs::create_dir_all("./config").ok();
|
std::fs::create_dir_all("./config").ok();
|
||||||
|
|
||||||
let jwt_signing_keys = JwtSigningKeys::load_or_create();
|
let jwt_signing_keys = JwtSigningKeys::load_or_create();
|
||||||
let (session_storage, auth_middleware_factory) = RedisMiddlewareFactory::<Claims>::new(
|
let (session_storage, auth_middleware_factory) = SessionMiddlewareFactory::<Claims>::build(
|
||||||
Arc::new(jwt_signing_keys.encoding_key),
|
Arc::new(jwt_signing_keys.encoding_key),
|
||||||
Arc::new(jwt_signing_keys.decoding_key),
|
Arc::new(jwt_signing_keys.decoding_key),
|
||||||
Algorithm::EdDSA,
|
Algorithm::EdDSA,
|
||||||
redis,
|
)
|
||||||
vec![
|
.with_redis_pool(redis)
|
||||||
Box::new(CookieExtractor::<Claims>::new(JWT_HEADER_NAME)),
|
.with_refresh_cookie(REFRESH_COOKIE_NAME)
|
||||||
Box::new(HeaderExtractor::<Claims>::new(JWT_HEADER_NAME)),
|
.with_refresh_header(REFRESH_HEADER_NAME)
|
||||||
],
|
.with_jwt_cookie(JWT_COOKIE_NAME)
|
||||||
);
|
.with_jwt_header(JWT_HEADER_NAME)
|
||||||
|
.finish();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
jwt_ttl: Data::new(jwt_ttl),
|
jwt_ttl: Data::new(jwt_ttl),
|
||||||
@ -212,28 +203,29 @@ async fn login_view(req: HttpRequest, t: Data<TranslationStorage>) -> HttpRespon
|
|||||||
#[autometrics]
|
#[autometrics]
|
||||||
#[post("/login")]
|
#[post("/login")]
|
||||||
async fn login(
|
async fn login(
|
||||||
jwt_ttl: Data<JWTTtl>,
|
jwt_ttl: Data<JwtTtl>,
|
||||||
refresh_ttl: Data<RefreshTtl>,
|
refresh_ttl: Data<RefreshTtl>,
|
||||||
db: Data<DatabaseConnection>,
|
db: Data<DatabaseConnection>,
|
||||||
redis: Data<SessionStorage<Claims>>,
|
redis: Data<SessionStorage>,
|
||||||
payload: Form<SignInPayload>,
|
payload: Form<SignInPayload>,
|
||||||
t: Data<oswilno_view::TranslationStorage>,
|
t: Data<oswilno_view::TranslationStorage>,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let t = t.into_inner();
|
let t = t.into_inner();
|
||||||
let mut errors = Errors::default();
|
let mut errors = Errors::default();
|
||||||
|
let form = payload.into_inner();
|
||||||
match login_inner(
|
match login_inner(
|
||||||
jwt_ttl.into_inner(),
|
jwt_ttl.into_inner(),
|
||||||
refresh_ttl.into_inner(),
|
refresh_ttl.into_inner(),
|
||||||
payload.into_inner(),
|
&form,
|
||||||
db.into_inner(),
|
db.into_inner(),
|
||||||
redis.into_inner(),
|
redis.into_inner(),
|
||||||
&mut errors,
|
&mut errors,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(res) => Ok(res),
|
Some(res) => Ok(res),
|
||||||
Err(form) => Ok(HttpResponse::Ok().body(
|
None => Ok(HttpResponse::Ok().body(
|
||||||
(SignInPartialTemplate {
|
(SignInPartialTemplate {
|
||||||
form,
|
form,
|
||||||
lang,
|
lang,
|
||||||
@ -247,19 +239,94 @@ async fn login(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn login_inner(
|
async fn login_inner(
|
||||||
jwt_ttl: Arc<JWTTtl>,
|
jwt_ttl: Arc<JwtTtl>,
|
||||||
refresh_ttl: Arc<RefreshTtl>,
|
refresh_ttl: Arc<RefreshTtl>,
|
||||||
payload: SignInPayload,
|
payload: &SignInPayload,
|
||||||
db: Arc<DatabaseConnection>,
|
db: Arc<DatabaseConnection>,
|
||||||
redis: Arc<SessionStorage<Claims>>,
|
redis: Arc<SessionStorage>,
|
||||||
errors: &mut Errors,
|
errors: &mut Errors,
|
||||||
) -> Result<HttpResponse, SignInPayload> {
|
) -> Option<HttpResponse> {
|
||||||
let iat = OffsetDateTime::now_utc().unix_timestamp() as usize;
|
let iat = OffsetDateTime::now_utc().unix_timestamp() as usize;
|
||||||
let expires_at = OffsetDateTime::now_utc().add(jwt_ttl.0);
|
|
||||||
let exp = expires_at.unix_timestamp() as usize;
|
|
||||||
|
|
||||||
|
let account = find_account(payload, errors, db.clone()).await?;
|
||||||
|
|
||||||
|
if let Err(e) = Hashing::verify(account.pass_hash.as_str(), payload.password.as_str()) {
|
||||||
|
tracing::warn!("Hashing verification failed: {e}");
|
||||||
|
errors.push_global("Bad credentials");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let jwt_claims = Claims {
|
||||||
|
issues_at: iat,
|
||||||
|
subject: account.login.clone(),
|
||||||
|
expiration_time: jwt_ttl.0.as_seconds_f64() as u64,
|
||||||
|
audience: Audience::Web,
|
||||||
|
jwt_id: uuid::Uuid::new_v4(),
|
||||||
|
account_id: account.id,
|
||||||
|
not_before: 0,
|
||||||
|
};
|
||||||
|
let pair = redis
|
||||||
|
.store(jwt_claims.clone(), *jwt_ttl, *refresh_ttl)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::warn!("Failed to set sign-in claims in redis: {e}");
|
||||||
|
errors.push_global("Failed to sign in. Please try later");
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
let encrypted_jwt = pair
|
||||||
|
.jwt
|
||||||
|
.encode()
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::warn!("Failed to encode claims: {e}");
|
||||||
|
errors.push_global("Failed to sign in. Please try later");
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
let encrypted_refresh = pair
|
||||||
|
.refresh
|
||||||
|
.encode()
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::warn!("Failed to encode claims: {e}");
|
||||||
|
errors.push_global("Failed to sign in. Please try later");
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
Some(
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.append_header((
|
||||||
|
actix_jwt_session::JWT_HEADER_NAME,
|
||||||
|
format!("Bearer {encrypted_jwt}").as_str(),
|
||||||
|
))
|
||||||
|
.append_header((
|
||||||
|
actix_jwt_session::REFRESH_HEADER_NAME,
|
||||||
|
format!("Bearer {}", encrypted_refresh).as_str(),
|
||||||
|
))
|
||||||
|
.append_header((
|
||||||
|
"ACX-JWT-TTL",
|
||||||
|
(pair.refresh.issues_at + pair.refresh.refresh_ttl.0).to_string(),
|
||||||
|
))
|
||||||
|
.append_header(("HX-Redirect", "/"))
|
||||||
|
.append_header(("HX-Retarget", "main"))
|
||||||
|
.cookie(
|
||||||
|
Cookie::build(actix_jwt_session::JWT_COOKIE_NAME, &encrypted_jwt)
|
||||||
|
.http_only(true)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.cookie(
|
||||||
|
Cookie::build(actix_jwt_session::REFRESH_COOKIE_NAME, &encrypted_refresh)
|
||||||
|
.http_only(true)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_account(
|
||||||
|
payload: &SignInPayload,
|
||||||
|
errors: &mut Errors,
|
||||||
|
db: Arc<DatabaseConnection>,
|
||||||
|
) -> Option<oswilno_contract::accounts::Model> {
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
let account = match oswilno_contract::accounts::Entity::find()
|
match oswilno_contract::accounts::Entity::find()
|
||||||
.filter(oswilno_contract::accounts::Column::Login.eq(payload.login.as_str()))
|
.filter(oswilno_contract::accounts::Column::Login.eq(payload.login.as_str()))
|
||||||
.filter(oswilno_contract::accounts::Column::Banned.eq(false))
|
.filter(oswilno_contract::accounts::Column::Banned.eq(false))
|
||||||
// TODO: Add email confirmation
|
// TODO: Add email confirmation
|
||||||
@ -267,78 +334,62 @@ async fn login_inner(
|
|||||||
.one(&*db)
|
.one(&*db)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(a)) => a,
|
Ok(Some(a)) => Some(a),
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
errors.push_global("Bad credentials");
|
errors.push_global("Bad credentials");
|
||||||
return Err(payload);
|
None
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Failed to find account: {e}");
|
tracing::warn!("Failed to find account: {e}");
|
||||||
errors.push_global("Bad credentials");
|
errors.push_global("Bad credentials");
|
||||||
return Err(payload);
|
None
|
||||||
}
|
}
|
||||||
};
|
|
||||||
if let Err(e) = hashing::verify(account.pass_hash.as_str(), payload.password.as_str()) {
|
|
||||||
tracing::warn!("Hashing verification failed: {e}");
|
|
||||||
errors.push_global("Bad credentials");
|
|
||||||
return Err(payload);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let jwt_claims = Claims {
|
#[autometrics]
|
||||||
issues_at: iat,
|
#[post("/refresh")]
|
||||||
subject: account.login.clone(),
|
async fn refresh_token(
|
||||||
expires_at: exp,
|
refresh_token: actix_jwt_session::Authenticated<RefreshToken>,
|
||||||
audience: Audience::Web,
|
storage: Data<SessionStorage>,
|
||||||
jwt_id: uuid::Uuid::new_v4(),
|
) -> HttpResponse {
|
||||||
account_id: account.id,
|
let s = storage.into_inner();
|
||||||
};
|
let pair = match s.refresh::<Claims>(refresh_token.access_jti()).await {
|
||||||
let jwt_token = match redis
|
|
||||||
.store(jwt_claims.clone(), jwt_ttl.0, refresh_ttl.0)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("Failed to set sign-in claims in redis: {e}");
|
tracing::warn!("Failed to refresh token: {e}");
|
||||||
errors.push_global("Failed to sign in. Please try later");
|
return HttpResponse::BadRequest().finish();
|
||||||
return Err(payload);
|
|
||||||
}
|
|
||||||
Ok(jwt_token) => jwt_token,
|
|
||||||
};
|
|
||||||
let bearer_token = match jwt_token.jwt.encode() {
|
|
||||||
Ok(token) => token,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("Failed to encode claims: {e}");
|
|
||||||
errors.push_global("Failed to sign in. Please try later");
|
|
||||||
return Err(payload);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let refresh_token = match jwt_token.refresh.encode() {
|
|
||||||
Ok(token) => token,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("Failed to encode claims: {e}");
|
|
||||||
errors.push_global("Failed to sign in. Please try later");
|
|
||||||
return Err(payload);
|
|
||||||
}
|
}
|
||||||
|
Ok(pair) => pair,
|
||||||
};
|
};
|
||||||
|
|
||||||
let jwt_cookie =
|
let encrypted_jwt = match pair.jwt.encode() {
|
||||||
actix_web::cookie::Cookie::build(actix_jwt_session::JWT_COOKIE_NAME, &bearer_token)
|
Ok(text) => text,
|
||||||
.http_only(true)
|
Err(e) => {
|
||||||
.finish();
|
tracing::warn!("Failed to encode claims: {e}");
|
||||||
let refresh_cookie =
|
return HttpResponse::InternalServerError().finish();
|
||||||
actix_web::cookie::Cookie::build(actix_jwt_session::REFRESH_COOKIE_NAME, &refresh_token)
|
}
|
||||||
.http_only(true)
|
};
|
||||||
.finish();
|
let encrypted_refresh = match pair.refresh.encode() {
|
||||||
Ok(HttpResponse::SeeOther()
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to encode claims: {e}");
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
}
|
||||||
|
Ok(text) => text,
|
||||||
|
};
|
||||||
|
HttpResponse::Ok()
|
||||||
.append_header((
|
.append_header((
|
||||||
actix_jwt_session::JWT_HEADER_NAME,
|
actix_jwt_session::JWT_HEADER_NAME,
|
||||||
format!("Bearer {bearer_token}").as_str(),
|
format!("Bearer {encrypted_jwt}").as_str(),
|
||||||
))
|
))
|
||||||
.append_header(("Location", "/"))
|
.append_header((
|
||||||
.append_header(("HX-Redirect", "/"))
|
actix_jwt_session::REFRESH_HEADER_NAME,
|
||||||
.append_header(("HX-Retarget", "main"))
|
format!("Bearer {}", encrypted_refresh).as_str(),
|
||||||
.cookie(jwt_cookie)
|
))
|
||||||
.cookie(refresh_cookie)
|
.append_header((
|
||||||
.body(""))
|
"ACX-JWT-TTL",
|
||||||
|
(pair.refresh.issues_at + pair.refresh.refresh_ttl.0).to_string(),
|
||||||
|
))
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[autometrics]
|
#[autometrics]
|
||||||
@ -351,13 +402,26 @@ async fn session_info(authenticated: Authenticated) -> Result<HttpResponse, Erro
|
|||||||
#[get("/logout")]
|
#[get("/logout")]
|
||||||
async fn logout(
|
async fn logout(
|
||||||
authenticated: Authenticated,
|
authenticated: Authenticated,
|
||||||
redis: Data<SessionStorage<Claims>>,
|
redis: Data<SessionStorage>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let jwt_id = authenticated.jwt_id;
|
let jwt_id = authenticated.jwt_id;
|
||||||
if let Err(_e) = redis.erase(jwt_id).await {};
|
if let Err(_e) = redis.erase::<Claims>(jwt_id).await {};
|
||||||
Ok(HttpResponse::SeeOther()
|
Ok(HttpResponse::Ok()
|
||||||
.append_header(("Location", "/"))
|
.append_header((JWT_HEADER_NAME, ""))
|
||||||
.body(""))
|
.append_header((REFRESH_HEADER_NAME, ""))
|
||||||
|
.cookie(
|
||||||
|
actix_web::cookie::Cookie::build(JWT_COOKIE_NAME, "")
|
||||||
|
.expires(OffsetDateTime::now_utc())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.cookie(
|
||||||
|
actix_web::cookie::Cookie::build(REFRESH_COOKIE_NAME, "")
|
||||||
|
.expires(OffsetDateTime::now_utc())
|
||||||
|
.finish(),
|
||||||
|
)
|
||||||
|
.append_header(("HX-Redirect", "/"))
|
||||||
|
.append_header(("HX-Retarget", "main"))
|
||||||
|
.finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, garde::Validate)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq, garde::Validate)]
|
||||||
@ -500,7 +564,7 @@ fn is_strong_password(value: &str, _context: &RegisterContext) -> garde::Result
|
|||||||
spec = spec || !c.is_alphanumeric();
|
spec = spec || !c.is_alphanumeric();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(garde::Error::new(WEAK_PASS));
|
Err(garde::Error::new(WEAK_PASS))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn register_internal(
|
async fn register_internal(
|
||||||
@ -525,7 +589,9 @@ async fn register_internal(
|
|||||||
p.clone()
|
p.clone()
|
||||||
})?;
|
})?;
|
||||||
let (login_taken, email_taken) = if let Some(query_result) = query_result {
|
let (login_taken, email_taken) = if let Some(query_result) = query_result {
|
||||||
let Ok((login_taken, email_taken)): Result<(bool,bool), _> = query_result.try_get_many("", &["login_taken".into(), "email_taken".into()]) else {
|
let Ok((login_taken, email_taken)): Result<(bool, bool), _> =
|
||||||
|
query_result.try_get_many("", &["login_taken".into(), "email_taken".into()])
|
||||||
|
else {
|
||||||
tracing::warn!("Failed to fetch fields from query result while checking if account info exists in db");
|
tracing::warn!("Failed to fetch fields from query result while checking if account info exists in db");
|
||||||
errors.push_global("Something went wrong");
|
errors.push_global("Something went wrong");
|
||||||
return Err(p);
|
return Err(p);
|
||||||
@ -545,7 +611,7 @@ async fn register_internal(
|
|||||||
}
|
}
|
||||||
tracing::warn!("{errors:#?}");
|
tracing::warn!("{errors:#?}");
|
||||||
|
|
||||||
let pass = match hashing::encrypt(p.password.as_str()) {
|
let pass = match Hashing::encrypt(p.password.as_str()) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("{e}");
|
tracing::warn!("{e}");
|
||||||
|
@ -214,7 +214,7 @@ pub enum Translation {
|
|||||||
impl std::fmt::Display for Translation {
|
impl std::fmt::Display for Translation {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Simple(s) => f.write_str(&s),
|
Self::Simple(s) => f.write_str(s),
|
||||||
Self::Format(inner) => inner.fmt(f),
|
Self::Format(inner) => inner.fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,9 @@ import './elements/oswilno-price.js';
|
|||||||
import './elements/oswilno-error.js';
|
import './elements/oswilno-error.js';
|
||||||
import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");
|
import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");
|
||||||
|
|
||||||
|
const READ_AUTH_HEADER = 'Authorization';
|
||||||
const AUTH_HEADER = 'ACX-Authorization';
|
const AUTH_HEADER = 'ACX-Authorization';
|
||||||
const REFRESH_HEADER = '';
|
const REFRESH_HEADER = 'ACX-Refresh';
|
||||||
const body = document.body;
|
const body = document.body;
|
||||||
body.addEventListener('htmx:beforeOnLoad', function (evt) {
|
body.addEventListener('htmx:beforeOnLoad', function (evt) {
|
||||||
const detail = evt.detail;
|
const detail = evt.detail;
|
||||||
@ -12,8 +13,9 @@ body.addEventListener('htmx:beforeOnLoad', function (evt) {
|
|||||||
const successful = detail.successful;
|
const successful = detail.successful;
|
||||||
|
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
const bearer = xhr.getResponseHeader(AUTH_HEADER);
|
const bearer = xhr.getResponseHeader(READ_AUTH_HEADER);
|
||||||
if (bearer) {
|
if (bearer) {
|
||||||
|
console.log(xhr);
|
||||||
localStorage.setItem('jwt', bearer.replace(/^Bearer /i, ''));
|
localStorage.setItem('jwt', bearer.replace(/^Bearer /i, ''));
|
||||||
}
|
}
|
||||||
const refresh = xhr.getResponseHeader(REFRESH_HEADER);
|
const refresh = xhr.getResponseHeader(REFRESH_HEADER);
|
||||||
@ -31,7 +33,8 @@ body.addEventListener('htmx:beforeOnLoad', function (evt) {
|
|||||||
body.addEventListener('htmx:configRequest', function (evt) {
|
body.addEventListener('htmx:configRequest', function (evt) {
|
||||||
if (localStorage.getItem('jwt')) {
|
if (localStorage.getItem('jwt')) {
|
||||||
evt.detail.headers[AUTH_HEADER] = 'Bearer ' + (localStorage.getItem('jwt') || '');
|
evt.detail.headers[AUTH_HEADER] = 'Bearer ' + (localStorage.getItem('jwt') || '');
|
||||||
evt.detail.headers[REFRESH_HEADER] = 'Bearer ' + (localStorage.getItem('refresh') || '');
|
evt.detail.headers[READ_AUTH_HEADER] = 'Bearer ' + (localStorage.getItem('jwt') || '');
|
||||||
|
evt.detail.headers[REFRESH_HEADER] = (localStorage.getItem('refresh') || '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user