6690 lines
155 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Build Power. Not Rent It. Own your digital infrastructure.">
<meta name="author" content="Bunker Operations">
<link rel="canonical" href="https://bnkserve.org/v2/database/models/auth/">
<link rel="prev" href="../">
<link rel="next" href="../settings/">
<link rel="icon" href="../../../../assets/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.1">
<title>Auth Models - Changemaker Lite</title>
<link rel="stylesheet" href="../../../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
<link rel="stylesheet" href="../../../../stylesheets/extra.css">
<link rel="stylesheet" href="../../../../stylesheets/home.css">
<link rel="stylesheet" href="../../../../assets/css/video-player.css">
<script>__md_scope=new URL("../../../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
<meta property="og:type" content="website" />
<meta property="og:title" content="Auth Models - Changemaker Lite" />
<meta property="og:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="og:image" content="https://bnkserve.org/assets/images/social/v2/database/models/auth.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://bnkserve.org/v2/database/models/auth/" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="Auth Models - Changemaker Lite" />
<meta property="twitter:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="twitter:image" content="https://bnkserve.org/assets/images/social/v2/database/models/auth.png" />
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#auth-users-models" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../../assets/logo.png" alt="logo">
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Changemaker Lite
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Auth Models
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_0">
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_1" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31"/></svg>
</label>
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_0" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z"/></svg>
</label>
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
<div class="md-header__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
</nav>
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../../../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../../../" class="md-tabs__link">
V2 Documentation
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../phil/" class="md-tabs__link">
Philosophy
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../v1/" class="md-tabs__link">
V1 Documentation (Legacy)
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../blog/" class="md-tabs__link">
Blog
</a>
</li>
</ul>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../../../.." title="Changemaker Lite" class="md-nav__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../../assets/logo.png" alt="logo">
</a>
Changemaker Lite
</label>
<div class="md-nav__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../../../" class="md-nav__link ">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
<label class="md-nav__link " for="__nav_2" id="__nav_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2">
<span class="md-nav__icon md-icon"></span>
V2 Documentation
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_2" >
<div class="md-nav__link md-nav__container">
<a href="../../../getting-started/" class="md-nav__link ">
<span class="md-ellipsis">
Getting Started
</span>
</a>
<label class="md-nav__link " for="__nav_2_2" id="__nav_2_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_2_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_2">
<span class="md-nav__icon md-icon"></span>
Getting Started
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../getting-started/quick-start/" class="md-nav__link">
<span class="md-ellipsis">
Quick Start
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_3" >
<div class="md-nav__link md-nav__container">
<a href="../../../architecture/" class="md-nav__link ">
<span class="md-ellipsis">
Architecture
</span>
</a>
<label class="md-nav__link " for="__nav_2_3" id="__nav_2_3_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_3_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_3">
<span class="md-nav__icon md-icon"></span>
Architecture
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../architecture/dual-api/" class="md-nav__link">
<span class="md-ellipsis">
Dual API System
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../architecture/authentication/" class="md-nav__link">
<span class="md-ellipsis">
Authentication & Security
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_4" >
<div class="md-nav__link md-nav__container">
<a href="../../../backend/" class="md-nav__link ">
<span class="md-ellipsis">
Backend
</span>
</a>
<label class="md-nav__link " for="__nav_2_4" id="__nav_2_4_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_4_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_4">
<span class="md-nav__icon md-icon"></span>
Backend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/modules/" class="md-nav__link">
<span class="md-ellipsis">
Modules
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/services/" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/middleware/" class="md-nav__link">
<span class="md-ellipsis">
Middleware
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/utilities/" class="md-nav__link">
<span class="md-ellipsis">
Utilities
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_5" >
<div class="md-nav__link md-nav__container">
<a href="../../../frontend/" class="md-nav__link ">
<span class="md-ellipsis">
Frontend
</span>
</a>
<label class="md-nav__link " for="__nav_2_5" id="__nav_2_5_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_5">
<span class="md-nav__icon md-icon"></span>
Frontend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/components/" class="md-nav__link">
<span class="md-ellipsis">
Components
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/layouts/" class="md-nav__link">
<span class="md-ellipsis">
Layouts
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/pages/" class="md-nav__link">
<span class="md-ellipsis">
Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_6" checked>
<div class="md-nav__link md-nav__container">
<a href="../../" class="md-nav__link ">
<span class="md-ellipsis">
Database
</span>
</a>
<label class="md-nav__link " for="__nav_2_6" id="__nav_2_6_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_6_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_6">
<span class="md-nav__icon md-icon"></span>
Database
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../schema/" class="md-nav__link">
<span class="md-ellipsis">
Schema Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../seeding/" class="md-nav__link">
<span class="md-ellipsis">
Seeding
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../indexes/" class="md-nav__link">
<span class="md-ellipsis">
Indexes
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_6_6" checked>
<div class="md-nav__link md-nav__container">
<a href="../" class="md-nav__link ">
<span class="md-ellipsis">
Models
</span>
</a>
<label class="md-nav__link " for="__nav_2_6_6" id="__nav_2_6_6_label" tabindex="0">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="3" aria-labelledby="__nav_2_6_6_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_6_6">
<span class="md-nav__icon md-icon"></span>
Models
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Auth Models
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Auth Models
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#overview" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#models-summary" class="md-nav__link">
<span class="md-ellipsis">
Models Summary
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#user-model" class="md-nav__link">
<span class="md-ellipsis">
User Model
</span>
</a>
<nav class="md-nav" aria-label="User Model">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#purpose" class="md-nav__link">
<span class="md-ellipsis">
Purpose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#fields" class="md-nav__link">
<span class="md-ellipsis">
Fields
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#enums" class="md-nav__link">
<span class="md-ellipsis">
Enums
</span>
</a>
<nav class="md-nav" aria-label="Enums">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#userrole" class="md-nav__link">
<span class="md-ellipsis">
UserRole
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#userstatus" class="md-nav__link">
<span class="md-ellipsis">
UserStatus
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#usercreatedvia" class="md-nav__link">
<span class="md-ellipsis">
UserCreatedVia
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#relations-33-total" class="md-nav__link">
<span class="md-ellipsis">
Relations (33 total)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#indexes" class="md-nav__link">
<span class="md-ellipsis">
Indexes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#constraints" class="md-nav__link">
<span class="md-ellipsis">
Constraints
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#refreshtoken-model" class="md-nav__link">
<span class="md-ellipsis">
RefreshToken Model
</span>
</a>
<nav class="md-nav" aria-label="RefreshToken Model">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#purpose_1" class="md-nav__link">
<span class="md-ellipsis">
Purpose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#fields_1" class="md-nav__link">
<span class="md-ellipsis">
Fields
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#relations" class="md-nav__link">
<span class="md-ellipsis">
Relations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#indexes_1" class="md-nav__link">
<span class="md-ellipsis">
Indexes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#constraints_1" class="md-nav__link">
<span class="md-ellipsis">
Constraints
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#relationships-diagram" class="md-nav__link">
<span class="md-ellipsis">
Relationships Diagram
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#common-queries" class="md-nav__link">
<span class="md-ellipsis">
Common Queries
</span>
</a>
<nav class="md-nav" aria-label="Common Queries">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#create-user-admin" class="md-nav__link">
<span class="md-ellipsis">
Create User (Admin)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#create-temp-user-public-shift-signup" class="md-nav__link">
<span class="md-ellipsis">
Create Temp User (Public Shift Signup)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#find-user-with-relations" class="md-nav__link">
<span class="md-ellipsis">
Find User with Relations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#update-last-login" class="md-nav__link">
<span class="md-ellipsis">
Update Last Login
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#store-refresh-token" class="md-nav__link">
<span class="md-ellipsis">
Store Refresh Token
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#refresh-token-rotation-atomic" class="md-nav__link">
<span class="md-ellipsis">
Refresh Token Rotation (Atomic)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#expire-temp-users-cron" class="md-nav__link">
<span class="md-ellipsis">
Expire Temp Users (Cron)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#clean-expired-refresh-tokens-cron" class="md-nav__link">
<span class="md-ellipsis">
Clean Expired Refresh Tokens (Cron)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#data-flow" class="md-nav__link">
<span class="md-ellipsis">
Data Flow
</span>
</a>
<nav class="md-nav" aria-label="Data Flow">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#user-registration-flow" class="md-nav__link">
<span class="md-ellipsis">
User Registration Flow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#login-flow" class="md-nav__link">
<span class="md-ellipsis">
Login Flow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#token-refresh-flow" class="md-nav__link">
<span class="md-ellipsis">
Token Refresh Flow
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#performance-notes" class="md-nav__link">
<span class="md-ellipsis">
Performance Notes
</span>
</a>
<nav class="md-nav" aria-label="Performance Notes">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#index-usage" class="md-nav__link">
<span class="md-ellipsis">
Index Usage
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#query-optimization" class="md-nav__link">
<span class="md-ellipsis">
Query Optimization
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#n1-prevention" class="md-nav__link">
<span class="md-ellipsis">
N+1 Prevention
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#security-considerations" class="md-nav__link">
<span class="md-ellipsis">
Security Considerations
</span>
</a>
<nav class="md-nav" aria-label="Security Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#password-policy" class="md-nav__link">
<span class="md-ellipsis">
Password Policy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#user-enumeration-prevention" class="md-nav__link">
<span class="md-ellipsis">
User Enumeration Prevention
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#refresh-token-security" class="md-nav__link">
<span class="md-ellipsis">
Refresh Token Security
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#role-based-access-control" class="md-nav__link">
<span class="md-ellipsis">
Role-Based Access Control
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#temp-user-restrictions" class="md-nav__link">
<span class="md-ellipsis">
TEMP User Restrictions
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<nav class="md-nav" aria-label="Troubleshooting">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#email-already-exists-on-registration" class="md-nav__link">
<span class="md-ellipsis">
"Email already exists" on registration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#invalid-refresh-token-on-refresh" class="md-nav__link">
<span class="md-ellipsis">
"Invalid refresh token" on refresh
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#password-does-not-meet-policy-on-update" class="md-nav__link">
<span class="md-ellipsis">
"Password does not meet policy" on update
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#temp-user-cannot-access-route" class="md-nav__link">
<span class="md-ellipsis">
TEMP user cannot access route
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#circular-dependency-auth-store-api-client" class="md-nav__link">
<span class="md-ellipsis">
Circular dependency: auth store ↔ api client
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#related-documentation" class="md-nav__link">
<span class="md-ellipsis">
Related Documentation
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../settings/" class="md-nav__link">
<span class="md-ellipsis">
Settings Models
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../influence/" class="md-nav__link">
<span class="md-ellipsis">
Influence Models
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../map/" class="md-nav__link">
<span class="md-ellipsis">
Map Models
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../canvass/" class="md-nav__link">
<span class="md-ellipsis">
Canvass Models
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../pages/" class="md-nav__link">
<span class="md-ellipsis">
Pages Models
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../email-templates/" class="md-nav__link">
<span class="md-ellipsis">
Email Templates Models
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../media/" class="md-nav__link">
<span class="md-ellipsis">
Media Models
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_7" >
<div class="md-nav__link md-nav__container">
<a href="../../../features/" class="md-nav__link ">
<span class="md-ellipsis">
Features
</span>
</a>
<label class="md-nav__link " for="__nav_2_7" id="__nav_2_7_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_7_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_7">
<span class="md-nav__icon md-icon"></span>
Features
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/influence/" class="md-nav__link">
<span class="md-ellipsis">
Influence
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/map/" class="md-nav__link">
<span class="md-ellipsis">
Map
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/landing-pages/" class="md-nav__link">
<span class="md-ellipsis">
Landing Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/email-templates/" class="md-nav__link">
<span class="md-ellipsis">
Email Templates
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/media/" class="md-nav__link">
<span class="md-ellipsis">
Media
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/newsletter/" class="md-nav__link">
<span class="md-ellipsis">
Newsletter
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/observability/" class="md-nav__link">
<span class="md-ellipsis">
Observability
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/tunnel/" class="md-nav__link">
<span class="md-ellipsis">
Tunnel
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_8" >
<div class="md-nav__link md-nav__container">
<a href="../../../deployment/" class="md-nav__link ">
<span class="md-ellipsis">
Deployment
</span>
</a>
<label class="md-nav__link " for="__nav_2_8" id="__nav_2_8_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_8_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_8">
<span class="md-nav__icon md-icon"></span>
Deployment
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../deployment/docker-compose/" class="md-nav__link">
<span class="md-ellipsis">
Docker Compose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/environment-variables/" class="md-nav__link">
<span class="md-ellipsis">
Environment Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/nginx/" class="md-nav__link">
<span class="md-ellipsis">
Nginx Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/ssl-tls/" class="md-nav__link">
<span class="md-ellipsis">
SSL/TLS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/tunneling/" class="md-nav__link">
<span class="md-ellipsis">
Tunneling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/monitoring-stack/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Stack
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/healthchecks/" class="md-nav__link">
<span class="md-ellipsis">
Health Checks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/scaling/" class="md-nav__link">
<span class="md-ellipsis">
Scaling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/backup-restore/" class="md-nav__link">
<span class="md-ellipsis">
Backup & Restore
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_9" >
<div class="md-nav__link md-nav__container">
<a href="../../../development/" class="md-nav__link ">
<span class="md-ellipsis">
Development
</span>
</a>
<label class="md-nav__link " for="__nav_2_9" id="__nav_2_9_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_9_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_9">
<span class="md-nav__icon md-icon"></span>
Development
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../development/local-setup/" class="md-nav__link">
<span class="md-ellipsis">
Local Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/docker-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Docker Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/git-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Git Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/npm-commands/" class="md-nav__link">
<span class="md-ellipsis">
NPM Commands
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/typescript/" class="md-nav__link">
<span class="md-ellipsis">
TypeScript
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/debugging/" class="md-nav__link">
<span class="md-ellipsis">
Debugging
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/code-style/" class="md-nav__link">
<span class="md-ellipsis">
Code Style
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_10" >
<div class="md-nav__link md-nav__container">
<a href="../../../api-reference/" class="md-nav__link ">
<span class="md-ellipsis">
API Reference
</span>
</a>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_10_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_10">
<span class="md-nav__icon md-icon"></span>
API Reference
</label>
<ul class="md-nav__list" data-md-scrollfix>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_11" >
<div class="md-nav__link md-nav__container">
<a href="../../../user-guides/" class="md-nav__link ">
<span class="md-ellipsis">
User Guides
</span>
</a>
<label class="md-nav__link " for="__nav_2_11" id="__nav_2_11_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_11_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_11">
<span class="md-nav__icon md-icon"></span>
User Guides
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../user-guides/admin-guide/" class="md-nav__link">
<span class="md-ellipsis">
Admin Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/campaign-manager-guide/" class="md-nav__link">
<span class="md-ellipsis">
Campaign Manager Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/map-organizer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Map Organizer Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/content-editor-guide/" class="md-nav__link">
<span class="md-ellipsis">
Content Editor Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/volunteer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Guide
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_12" >
<div class="md-nav__link md-nav__container">
<a href="../../../troubleshooting/" class="md-nav__link ">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<label class="md-nav__link " for="__nav_2_12" id="__nav_2_12_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_12_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_12">
<span class="md-nav__icon md-icon"></span>
Troubleshooting
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../troubleshooting/faq/" class="md-nav__link">
<span class="md-ellipsis">
FAQ
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/common-errors/" class="md-nav__link">
<span class="md-ellipsis">
Common Errors
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/auth-issues/" class="md-nav__link">
<span class="md-ellipsis">
Auth Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/database-issues/" class="md-nav__link">
<span class="md-ellipsis">
Database Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/docker-issues/" class="md-nav__link">
<span class="md-ellipsis">
Docker Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/email-issues/" class="md-nav__link">
<span class="md-ellipsis">
Email Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/geocoding-issues/" class="md-nav__link">
<span class="md-ellipsis">
Geocoding Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/monitoring-issues/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/performance-optimization/" class="md-nav__link">
<span class="md-ellipsis">
Performance Optimization
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_13" >
<div class="md-nav__link md-nav__container">
<a href="../../../migration/" class="md-nav__link ">
<span class="md-ellipsis">
Migration
</span>
</a>
<label class="md-nav__link " for="__nav_2_13" id="__nav_2_13_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_13_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_13">
<span class="md-nav__icon md-icon"></span>
Migration
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../migration/feature-parity/" class="md-nav__link">
<span class="md-ellipsis">
Feature Parity
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/breaking-changes/" class="md-nav__link">
<span class="md-ellipsis">
Breaking Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/api-changes/" class="md-nav__link">
<span class="md-ellipsis">
API Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/data-migration/" class="md-nav__link">
<span class="md-ellipsis">
Data Migration
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_14" >
<div class="md-nav__link md-nav__container">
<a href="../../../contributing/" class="md-nav__link ">
<span class="md-ellipsis">
Contributing
</span>
</a>
<label class="md-nav__link " for="__nav_2_14" id="__nav_2_14_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_14_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_14">
<span class="md-nav__icon md-icon"></span>
Contributing
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../contributing/development-setup/" class="md-nav__link">
<span class="md-ellipsis">
Development Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/code-of-conduct/" class="md-nav__link">
<span class="md-ellipsis">
Code of Conduct
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/pull-requests/" class="md-nav__link">
<span class="md-ellipsis">
Pull Requests
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/roadmap/" class="md-nav__link">
<span class="md-ellipsis">
Roadmap
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../phil/" class="md-nav__link">
<span class="md-ellipsis">
Philosophy
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../v1/" class="md-nav__link">
<span class="md-ellipsis">
V1 Documentation (Legacy)
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../blog/" class="md-nav__link">
<span class="md-ellipsis">
Blog
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#overview" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#models-summary" class="md-nav__link">
<span class="md-ellipsis">
Models Summary
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#user-model" class="md-nav__link">
<span class="md-ellipsis">
User Model
</span>
</a>
<nav class="md-nav" aria-label="User Model">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#purpose" class="md-nav__link">
<span class="md-ellipsis">
Purpose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#fields" class="md-nav__link">
<span class="md-ellipsis">
Fields
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#enums" class="md-nav__link">
<span class="md-ellipsis">
Enums
</span>
</a>
<nav class="md-nav" aria-label="Enums">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#userrole" class="md-nav__link">
<span class="md-ellipsis">
UserRole
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#userstatus" class="md-nav__link">
<span class="md-ellipsis">
UserStatus
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#usercreatedvia" class="md-nav__link">
<span class="md-ellipsis">
UserCreatedVia
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#relations-33-total" class="md-nav__link">
<span class="md-ellipsis">
Relations (33 total)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#indexes" class="md-nav__link">
<span class="md-ellipsis">
Indexes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#constraints" class="md-nav__link">
<span class="md-ellipsis">
Constraints
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#refreshtoken-model" class="md-nav__link">
<span class="md-ellipsis">
RefreshToken Model
</span>
</a>
<nav class="md-nav" aria-label="RefreshToken Model">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#purpose_1" class="md-nav__link">
<span class="md-ellipsis">
Purpose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#fields_1" class="md-nav__link">
<span class="md-ellipsis">
Fields
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#relations" class="md-nav__link">
<span class="md-ellipsis">
Relations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#indexes_1" class="md-nav__link">
<span class="md-ellipsis">
Indexes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#constraints_1" class="md-nav__link">
<span class="md-ellipsis">
Constraints
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#relationships-diagram" class="md-nav__link">
<span class="md-ellipsis">
Relationships Diagram
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#common-queries" class="md-nav__link">
<span class="md-ellipsis">
Common Queries
</span>
</a>
<nav class="md-nav" aria-label="Common Queries">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#create-user-admin" class="md-nav__link">
<span class="md-ellipsis">
Create User (Admin)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#create-temp-user-public-shift-signup" class="md-nav__link">
<span class="md-ellipsis">
Create Temp User (Public Shift Signup)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#find-user-with-relations" class="md-nav__link">
<span class="md-ellipsis">
Find User with Relations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#update-last-login" class="md-nav__link">
<span class="md-ellipsis">
Update Last Login
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#store-refresh-token" class="md-nav__link">
<span class="md-ellipsis">
Store Refresh Token
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#refresh-token-rotation-atomic" class="md-nav__link">
<span class="md-ellipsis">
Refresh Token Rotation (Atomic)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#expire-temp-users-cron" class="md-nav__link">
<span class="md-ellipsis">
Expire Temp Users (Cron)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#clean-expired-refresh-tokens-cron" class="md-nav__link">
<span class="md-ellipsis">
Clean Expired Refresh Tokens (Cron)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#data-flow" class="md-nav__link">
<span class="md-ellipsis">
Data Flow
</span>
</a>
<nav class="md-nav" aria-label="Data Flow">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#user-registration-flow" class="md-nav__link">
<span class="md-ellipsis">
User Registration Flow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#login-flow" class="md-nav__link">
<span class="md-ellipsis">
Login Flow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#token-refresh-flow" class="md-nav__link">
<span class="md-ellipsis">
Token Refresh Flow
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#performance-notes" class="md-nav__link">
<span class="md-ellipsis">
Performance Notes
</span>
</a>
<nav class="md-nav" aria-label="Performance Notes">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#index-usage" class="md-nav__link">
<span class="md-ellipsis">
Index Usage
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#query-optimization" class="md-nav__link">
<span class="md-ellipsis">
Query Optimization
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#n1-prevention" class="md-nav__link">
<span class="md-ellipsis">
N+1 Prevention
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#security-considerations" class="md-nav__link">
<span class="md-ellipsis">
Security Considerations
</span>
</a>
<nav class="md-nav" aria-label="Security Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#password-policy" class="md-nav__link">
<span class="md-ellipsis">
Password Policy
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#user-enumeration-prevention" class="md-nav__link">
<span class="md-ellipsis">
User Enumeration Prevention
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#refresh-token-security" class="md-nav__link">
<span class="md-ellipsis">
Refresh Token Security
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#role-based-access-control" class="md-nav__link">
<span class="md-ellipsis">
Role-Based Access Control
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#temp-user-restrictions" class="md-nav__link">
<span class="md-ellipsis">
TEMP User Restrictions
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<nav class="md-nav" aria-label="Troubleshooting">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#email-already-exists-on-registration" class="md-nav__link">
<span class="md-ellipsis">
"Email already exists" on registration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#invalid-refresh-token-on-refresh" class="md-nav__link">
<span class="md-ellipsis">
"Invalid refresh token" on refresh
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#password-does-not-meet-policy-on-update" class="md-nav__link">
<span class="md-ellipsis">
"Password does not meet policy" on update
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#temp-user-cannot-access-route" class="md-nav__link">
<span class="md-ellipsis">
TEMP user cannot access route
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#circular-dependency-auth-store-api-client" class="md-nav__link">
<span class="md-ellipsis">
Circular dependency: auth store ↔ api client
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#related-documentation" class="md-nav__link">
<span class="md-ellipsis">
Related Documentation
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<nav class="md-path" aria-label="Navigation" >
<ol class="md-path__list">
<li class="md-path__item">
<a href="../../../.." class="md-path__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../../" class="md-path__link">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../" class="md-path__link">
<span class="md-ellipsis">
Database
</span>
</a>
</li>
<li class="md-path__item">
<a href="../" class="md-path__link">
<span class="md-ellipsis">
Models
</span>
</a>
</li>
</ol>
</nav>
<article class="md-content__inner md-typeset">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/database/models/auth.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/database/models/auth.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
<h1 id="auth-users-models">Auth &amp; Users Models<a class="headerlink" href="#auth-users-models" title="Permanent link">&para;</a></h1>
<h2 id="overview">Overview<a class="headerlink" href="#overview" title="Permanent link">&para;</a></h2>
<p>The Auth &amp; Users module provides JWT-based authentication with role-based access control (RBAC), temporary user support for public shift signups, and refresh token rotation for enhanced security.</p>
<p><strong>Models:</strong>
- <strong>User</strong> — User accounts with roles and permissions
- <strong>RefreshToken</strong> — JWT refresh token storage with expiration</p>
<p><strong>Key Features:</strong>
- bcrypt password hashing (12+ character policy enforced at schema level)
- JWT access tokens (15min) + refresh tokens (7 days)
- Refresh token rotation with atomic transactions
- Role hierarchy: SUPER_ADMIN &gt; INFLUENCE_ADMIN &gt; MAP_ADMIN &gt; USER &gt; TEMP
- Temporary user support with auto-expiration
- Email verification workflow
- User enumeration prevention (401 not 404)</p>
<hr />
<h2 id="models-summary">Models Summary<a class="headerlink" href="#models-summary" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Model</th>
<th>Table</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>User</td>
<td><code>users</code></td>
<td>User accounts with RBAC, permissions, temp user support</td>
</tr>
<tr>
<td>RefreshToken</td>
<td><code>refresh_tokens</code></td>
<td>JWT refresh tokens with expiration tracking</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="user-model">User Model<a class="headerlink" href="#user-model" title="Permanent link">&para;</a></h2>
<h3 id="purpose">Purpose<a class="headerlink" href="#purpose" title="Permanent link">&para;</a></h3>
<p>The User model represents all system users, from super admins to temporary volunteers created via public shift signup. It supports role-based access control, granular permissions, temporary user expiration, and comprehensive audit tracking via 33 relation fields.</p>
<h3 id="fields">Fields<a class="headerlink" href="#fields" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Identity</strong></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>id</td>
<td>String</td>
<td></td>
<td><code>cuid()</code></td>
<td>Primary key</td>
</tr>
<tr>
<td>email</td>
<td>String</td>
<td></td>
<td></td>
<td>Unique email address (lowercase)</td>
</tr>
<tr>
<td>password</td>
<td>String</td>
<td></td>
<td></td>
<td>bcrypt hashed (12+ chars, 1 uppercase, 1 lowercase, 1 digit)</td>
</tr>
<tr>
<td>name</td>
<td>String</td>
<td></td>
<td><code>null</code></td>
<td>User display name</td>
</tr>
<tr>
<td>phone</td>
<td>String</td>
<td></td>
<td><code>null</code></td>
<td>Phone number</td>
</tr>
<tr>
<td><strong>Authorization</strong></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>role</td>
<td>UserRole</td>
<td></td>
<td><code>USER</code></td>
<td>User role (see enum below)</td>
</tr>
<tr>
<td>status</td>
<td>UserStatus</td>
<td></td>
<td><code>ACTIVE</code></td>
<td>Account status (see enum below)</td>
</tr>
<tr>
<td>permissions</td>
<td>Json</td>
<td></td>
<td><code>null</code></td>
<td>Granular per-app permissions object</td>
</tr>
<tr>
<td><strong>User Lifecycle</strong></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>createdVia</td>
<td>UserCreatedVia</td>
<td></td>
<td><code>STANDARD</code></td>
<td>Creation source (see enum below)</td>
</tr>
<tr>
<td>expiresAt</td>
<td>DateTime</td>
<td></td>
<td><code>null</code></td>
<td>Expiration date for TEMP users</td>
</tr>
<tr>
<td>expireDays</td>
<td>Int</td>
<td></td>
<td><code>null</code></td>
<td>Days until expiration (for TEMP users)</td>
</tr>
<tr>
<td>lastLoginAt</td>
<td>DateTime</td>
<td></td>
<td><code>null</code></td>
<td>Last login timestamp</td>
</tr>
<tr>
<td>emailVerified</td>
<td>Boolean</td>
<td></td>
<td><code>false</code></td>
<td>Email verification status</td>
</tr>
<tr>
<td><strong>Audit</strong></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>createdAt</td>
<td>DateTime</td>
<td></td>
<td><code>now()</code></td>
<td>Creation timestamp</td>
</tr>
<tr>
<td>updatedAt</td>
<td>DateTime</td>
<td></td>
<td>Auto</td>
<td>Last update timestamp</td>
</tr>
</tbody>
</table>
<h3 id="enums">Enums<a class="headerlink" href="#enums" title="Permanent link">&para;</a></h3>
<h4 id="userrole">UserRole<a class="headerlink" href="#userrole" title="Permanent link">&para;</a></h4>
<p>Role hierarchy (descending):</p>
<div class="language-text highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>enum UserRole {
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a> SUPER_ADMIN // Full system access, can manage all users
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a> INFLUENCE_ADMIN // Manage influence module (campaigns, responses)
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a> MAP_ADMIN // Manage map module (locations, shifts, cuts)
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a> USER // Standard volunteer with assigned permissions
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a> TEMP // Temporary user (auto-expires, restricted access)
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a>}
</span></code></pre></div>
<p><strong>Role Capabilities:</strong>
- <strong>SUPER_ADMIN:</strong> Full access to all modules, user management, settings
- <strong>INFLUENCE_ADMIN:</strong> Campaign CRUD, response moderation, email queue admin
- <strong>MAP_ADMIN:</strong> Location CRUD, shift management, cut creation, canvass oversight
- <strong>USER:</strong> Public campaign actions, shift signup, canvass sessions (via assigned cut)
- <strong>TEMP:</strong> Canvass sessions only (via shift signup), auto-expires after X days</p>
<h4 id="userstatus">UserStatus<a class="headerlink" href="#userstatus" title="Permanent link">&para;</a></h4>
<div class="language-text highlight"><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a>enum UserStatus {
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a> ACTIVE // Normal active user
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a> INACTIVE // Manually deactivated (login blocked)
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4" href="#__codelineno-1-4"></a> SUSPENDED // Temporarily suspended (login blocked)
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5" href="#__codelineno-1-5"></a> EXPIRED // Auto-expired temp user (login blocked)
</span><span id="__span-1-6"><a id="__codelineno-1-6" name="__codelineno-1-6" href="#__codelineno-1-6"></a>}
</span></code></pre></div>
<h4 id="usercreatedvia">UserCreatedVia<a class="headerlink" href="#usercreatedvia" title="Permanent link">&para;</a></h4>
<div class="language-text highlight"><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a>enum UserCreatedVia {
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a> ADMIN // Created by admin in user management
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3" href="#__codelineno-2-3"></a> PUBLIC_SHIFT_SIGNUP // Auto-created via public shift signup
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4" href="#__codelineno-2-4"></a> STANDARD // Self-registered (if enabled)
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5" href="#__codelineno-2-5"></a>}
</span></code></pre></div>
<h3 id="relations-33-total">Relations (33 total)<a class="headerlink" href="#relations-33-total" title="Permanent link">&para;</a></h3>
<p><strong>Authentication:</strong>
- <code>refreshTokens</code> → RefreshToken[] (onDelete: Cascade)</p>
<p><strong>Influence Module (6):</strong>
- <code>campaignsCreated</code> → Campaign[] (creator, onDelete: SetNull)
- <code>campaignEmails</code> → CampaignEmail[] (sender, onDelete: SetNull)
- <code>responses</code> → RepresentativeResponse[] (submitter, onDelete: SetNull)
- <code>responseUpvotes</code> → ResponseUpvote[] (onDelete: SetNull)</p>
<p><strong>Map Module (8):</strong>
- <code>locationsCreated</code> → Location[] (creator, onDelete: SetNull)
- <code>locationsUpdated</code> → Location[] (updater, onDelete: SetNull)
- <code>addressesCreated</code> → Address[] (creator, onDelete: SetNull)
- <code>addressesUpdated</code> → Address[] (updater, onDelete: SetNull)
- <code>locationEdits</code> → LocationHistory[] (editor, onDelete: SetNull)
- <code>cutsCreated</code> → Cut[] (creator, onDelete: SetNull)
- <code>shiftSignups</code> → ShiftSignup[] (onDelete: SetNull)</p>
<p><strong>Canvassing Module (4):</strong>
- <code>canvassVisits</code> → CanvassVisit[] (visitor, onDelete: Cascade)
- <code>canvassSessions</code> → CanvassSession[] (onDelete: Cascade)
- <code>trackingSessions</code> → TrackingSession[] (onDelete: Cascade)</p>
<p><strong>Email Templates Module (4):</strong>
- <code>templatesCreated</code> → EmailTemplate[] (creator)
- <code>templatesUpdated</code> → EmailTemplate[] (updater)
- <code>templateVersionsCreated</code> → EmailTemplateVersion[]
- <code>templateTestsSent</code> → EmailTemplateTestLog[]</p>
<h3 id="indexes">Indexes<a class="headerlink" href="#indexes" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>Unique:</strong> <code>email</code> (case-insensitive via Prisma transform)</li>
</ul>
<h3 id="constraints">Constraints<a class="headerlink" href="#constraints" title="Permanent link">&para;</a></h3>
<ul>
<li>Email must be unique across all users</li>
<li>Password must meet policy: 12+ chars, 1 uppercase, 1 lowercase, 1 digit (enforced by Zod schema)</li>
<li>TEMP users must have <code>expiresAt</code> set</li>
<li>EXPIRED status auto-applied when <code>expiresAt</code> &lt; now()</li>
</ul>
<hr />
<h2 id="refreshtoken-model">RefreshToken Model<a class="headerlink" href="#refreshtoken-model" title="Permanent link">&para;</a></h2>
<h3 id="purpose_1">Purpose<a class="headerlink" href="#purpose_1" title="Permanent link">&para;</a></h3>
<p>The RefreshToken model stores JWT refresh tokens for token rotation. When a user logs in, both an access token (15min expiry, stored client-side) and a refresh token (7 day expiry, stored in DB) are issued. When the access token expires, the client uses the refresh token to obtain a new access token. For security, refresh tokens are rotated on each refresh (old token deleted, new token issued) using atomic Prisma transactions.</p>
<h3 id="fields_1">Fields<a class="headerlink" href="#fields_1" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>String</td>
<td></td>
<td><code>cuid()</code></td>
<td>Primary key</td>
</tr>
<tr>
<td>token</td>
<td>String</td>
<td></td>
<td></td>
<td>JWT refresh token string (unique, 512 chars)</td>
</tr>
<tr>
<td>userId</td>
<td>String</td>
<td></td>
<td></td>
<td>Foreign key to User</td>
</tr>
<tr>
<td>expiresAt</td>
<td>DateTime</td>
<td></td>
<td></td>
<td>Token expiration timestamp (7 days from issue)</td>
</tr>
<tr>
<td>createdAt</td>
<td>DateTime</td>
<td></td>
<td><code>now()</code></td>
<td>Token creation timestamp</td>
</tr>
</tbody>
</table>
<h3 id="relations">Relations<a class="headerlink" href="#relations" title="Permanent link">&para;</a></h3>
<ul>
<li><code>user</code> → User (onDelete: Cascade) — deleting user deletes all refresh tokens</li>
</ul>
<h3 id="indexes_1">Indexes<a class="headerlink" href="#indexes_1" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>Unique:</strong> <code>token</code> (fast lookup for refresh endpoint)</li>
<li><strong>Foreign Key:</strong> <code>userId</code> (join to User)</li>
</ul>
<h3 id="constraints_1">Constraints<a class="headerlink" href="#constraints_1" title="Permanent link">&para;</a></h3>
<ul>
<li>Token must be unique (prevents replay attacks)</li>
<li>ExpiresAt must be &gt; now() for valid tokens</li>
<li>Expired tokens cleaned up via cron job (daily)</li>
</ul>
<hr />
<h2 id="relationships-diagram">Relationships Diagram<a class="headerlink" href="#relationships-diagram" title="Permanent link">&para;</a></h2>
<pre class="mermaid"><code>erDiagram
User ||--o{ RefreshToken : has
User ||--o{ Campaign : creates
User ||--o{ CampaignEmail : sends
User ||--o{ RepresentativeResponse : submits
User ||--o{ ResponseUpvote : upvotes
User ||--o{ ShiftSignup : "signs up for"
User ||--o{ Location : creates
User ||--o{ Location : updates
User ||--o{ Address : "creates (addresses)"
User ||--o{ Address : "updates (addresses)"
User ||--o{ LocationHistory : edits
User ||--o{ Cut : "creates (cuts)"
User ||--o{ CanvassVisit : visits
User ||--o{ CanvassSession : "has (sessions)"
User ||--o{ TrackingSession : "tracks (gps)"
User ||--o{ EmailTemplate : "creates (templates)"
User ||--o{ EmailTemplate : "updates (templates)"
User ||--o{ EmailTemplateVersion : "versions (templates)"
User ||--o{ EmailTemplateTestLog : "tests (templates)"
User {
String id PK
String email UK "unique, lowercase"
String password "bcrypt hashed"
String name
String phone
UserRole role "SUPER_ADMIN | INFLUENCE_ADMIN | MAP_ADMIN | USER | TEMP"
UserStatus status "ACTIVE | INACTIVE | SUSPENDED | EXPIRED"
Json permissions "granular per-app"
UserCreatedVia createdVia
DateTime expiresAt "TEMP user expiration"
Int expireDays
DateTime lastLoginAt
Boolean emailVerified
DateTime createdAt
DateTime updatedAt
}
RefreshToken {
String id PK
String token UK
String userId FK
DateTime expiresAt
DateTime createdAt
}</code></pre>
<hr />
<h2 id="common-queries">Common Queries<a class="headerlink" href="#common-queries" title="Permanent link">&para;</a></h2>
<h3 id="create-user-admin">Create User (Admin)<a class="headerlink" href="#create-user-admin" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">user</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a><span class="w"> </span><span class="nx">email</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;volunteer@example.com&#39;</span><span class="p">,</span>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a><span class="w"> </span><span class="nx">password</span><span class="o">:</span><span class="w"> </span><span class="kt">await</span><span class="w"> </span><span class="nx">bcrypt</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="s1">&#39;SecurePass123!&#39;</span><span class="p">,</span><span class="w"> </span><span class="mf">10</span><span class="p">),</span>
</span><span id="__span-3-5"><a id="__codelineno-3-5" name="__codelineno-3-5" href="#__codelineno-3-5"></a><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Jane Volunteer&#39;</span><span class="p">,</span>
</span><span id="__span-3-6"><a id="__codelineno-3-6" name="__codelineno-3-6" href="#__codelineno-3-6"></a><span class="w"> </span><span class="nx">phone</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;555-0100&#39;</span><span class="p">,</span>
</span><span id="__span-3-7"><a id="__codelineno-3-7" name="__codelineno-3-7" href="#__codelineno-3-7"></a><span class="w"> </span><span class="nx">role</span><span class="o">:</span><span class="w"> </span><span class="kt">UserRole.USER</span><span class="p">,</span>
</span><span id="__span-3-8"><a id="__codelineno-3-8" name="__codelineno-3-8" href="#__codelineno-3-8"></a><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">UserStatus.ACTIVE</span><span class="p">,</span>
</span><span id="__span-3-9"><a id="__codelineno-3-9" name="__codelineno-3-9" href="#__codelineno-3-9"></a><span class="w"> </span><span class="nx">createdVia</span><span class="o">:</span><span class="w"> </span><span class="kt">UserCreatedVia.ADMIN</span><span class="p">,</span>
</span><span id="__span-3-10"><a id="__codelineno-3-10" name="__codelineno-3-10" href="#__codelineno-3-10"></a><span class="w"> </span><span class="nx">emailVerified</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-3-11"><a id="__codelineno-3-11" name="__codelineno-3-11" href="#__codelineno-3-11"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-3-12"><a id="__codelineno-3-12" name="__codelineno-3-12" href="#__codelineno-3-12"></a><span class="p">});</span>
</span></code></pre></div>
<h3 id="create-temp-user-public-shift-signup">Create Temp User (Public Shift Signup)<a class="headerlink" href="#create-temp-user-public-shift-signup" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">tempUser</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a><span class="w"> </span><span class="nx">email</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;temp@example.com&#39;</span><span class="p">,</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="w"> </span><span class="nx">password</span><span class="o">:</span><span class="w"> </span><span class="kt">await</span><span class="w"> </span><span class="nx">bcrypt</span><span class="p">.</span><span class="nx">hash</span><span class="p">(</span><span class="nx">randomPassword</span><span class="p">,</span><span class="w"> </span><span class="mf">10</span><span class="p">),</span><span class="w"> </span><span class="c1">// Generated password</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Temp Volunteer&#39;</span><span class="p">,</span>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a><span class="w"> </span><span class="nx">role</span><span class="o">:</span><span class="w"> </span><span class="kt">UserRole.TEMP</span><span class="p">,</span>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7" href="#__codelineno-4-7"></a><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">UserStatus.ACTIVE</span><span class="p">,</span>
</span><span id="__span-4-8"><a id="__codelineno-4-8" name="__codelineno-4-8" href="#__codelineno-4-8"></a><span class="w"> </span><span class="nx">createdVia</span><span class="o">:</span><span class="w"> </span><span class="kt">UserCreatedVia.PUBLIC_SHIFT_SIGNUP</span><span class="p">,</span>
</span><span id="__span-4-9"><a id="__codelineno-4-9" name="__codelineno-4-9" href="#__codelineno-4-9"></a><span class="w"> </span><span class="nx">expiresAt</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nb">Date</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">30</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">24</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">1000</span><span class="p">),</span><span class="w"> </span><span class="c1">// 30 days</span>
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10" href="#__codelineno-4-10"></a><span class="w"> </span><span class="nx">expireDays</span><span class="o">:</span><span class="w"> </span><span class="kt">30</span><span class="p">,</span>
</span><span id="__span-4-11"><a id="__codelineno-4-11" name="__codelineno-4-11" href="#__codelineno-4-11"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-4-12"><a id="__codelineno-4-12" name="__codelineno-4-12" href="#__codelineno-4-12"></a><span class="p">});</span>
</span></code></pre></div>
<h3 id="find-user-with-relations">Find User with Relations<a class="headerlink" href="#find-user-with-relations" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">user</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">findUnique</span><span class="p">({</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">email</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;admin@example.com&#39;</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a><span class="w"> </span><span class="nx">include</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4" href="#__codelineno-5-4"></a><span class="w"> </span><span class="nx">campaignsCreated</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">take</span><span class="o">:</span><span class="w"> </span><span class="kt">5</span><span class="p">,</span><span class="w"> </span><span class="nx">orderBy</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">createdAt</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;desc&#39;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5" href="#__codelineno-5-5"></a><span class="w"> </span><span class="nx">canvassSessions</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">take</span><span class="o">:</span><span class="w"> </span><span class="kt">10</span><span class="p">,</span><span class="w"> </span><span class="nx">orderBy</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">startedAt</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;desc&#39;</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6" href="#__codelineno-5-6"></a><span class="w"> </span><span class="nx">shiftSignups</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">include</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">shift</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-7"><a id="__codelineno-5-7" name="__codelineno-5-7" href="#__codelineno-5-7"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-5-8"><a id="__codelineno-5-8" name="__codelineno-5-8" href="#__codelineno-5-8"></a><span class="p">});</span>
</span></code></pre></div>
<h3 id="update-last-login">Update Last Login<a class="headerlink" href="#update-last-login" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-6-1"><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">update</span><span class="p">({</span>
</span><span id="__span-6-2"><a id="__codelineno-6-2" name="__codelineno-6-2" href="#__codelineno-6-2"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">userId</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-6-3"><a id="__codelineno-6-3" name="__codelineno-6-3" href="#__codelineno-6-3"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">lastLoginAt</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nb">Date</span><span class="p">()</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-6-4"><a id="__codelineno-6-4" name="__codelineno-6-4" href="#__codelineno-6-4"></a><span class="p">});</span>
</span></code></pre></div>
<h3 id="store-refresh-token">Store Refresh Token<a class="headerlink" href="#store-refresh-token" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-7-1"><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">refreshToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">refreshToken</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
</span><span id="__span-7-2"><a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-7-3"><a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a><span class="w"> </span><span class="nx">token</span><span class="o">:</span><span class="w"> </span><span class="kt">jwtRefreshToken</span><span class="p">,</span>
</span><span id="__span-7-4"><a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a><span class="w"> </span><span class="nx">userId</span><span class="o">:</span><span class="w"> </span><span class="kt">user.id</span><span class="p">,</span>
</span><span id="__span-7-5"><a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a><span class="w"> </span><span class="nx">expiresAt</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nb">Date</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">7</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">24</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">1000</span><span class="p">),</span><span class="w"> </span><span class="c1">// 7 days</span>
</span><span id="__span-7-6"><a id="__codelineno-7-6" name="__codelineno-7-6" href="#__codelineno-7-6"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-7-7"><a id="__codelineno-7-7" name="__codelineno-7-7" href="#__codelineno-7-7"></a><span class="p">});</span>
</span></code></pre></div>
<h3 id="refresh-token-rotation-atomic">Refresh Token Rotation (Atomic)<a class="headerlink" href="#refresh-token-rotation-atomic" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-8-1"><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">newTokens</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">$transaction</span><span class="p">(</span><span class="k">async</span><span class="w"> </span><span class="p">(</span><span class="nx">tx</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-8-2"><a id="__codelineno-8-2" name="__codelineno-8-2" href="#__codelineno-8-2"></a><span class="w"> </span><span class="c1">// 1. Verify old token exists and is valid</span>
</span><span id="__span-8-3"><a id="__codelineno-8-3" name="__codelineno-8-3" href="#__codelineno-8-3"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">oldToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">refreshToken</span><span class="p">.</span><span class="nx">findUnique</span><span class="p">({</span>
</span><span id="__span-8-4"><a id="__codelineno-8-4" name="__codelineno-8-4" href="#__codelineno-8-4"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">token</span><span class="o">:</span><span class="w"> </span><span class="kt">oldRefreshToken</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-8-5"><a id="__codelineno-8-5" name="__codelineno-8-5" href="#__codelineno-8-5"></a><span class="w"> </span><span class="nx">include</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">user</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-8-6"><a id="__codelineno-8-6" name="__codelineno-8-6" href="#__codelineno-8-6"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-8-7"><a id="__codelineno-8-7" name="__codelineno-8-7" href="#__codelineno-8-7"></a>
</span><span id="__span-8-8"><a id="__codelineno-8-8" name="__codelineno-8-8" href="#__codelineno-8-8"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">oldToken</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">oldToken</span><span class="p">.</span><span class="nx">expiresAt</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">Date</span><span class="p">())</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-8-9"><a id="__codelineno-8-9" name="__codelineno-8-9" href="#__codelineno-8-9"></a><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="ne">Error</span><span class="p">(</span><span class="s1">&#39;Invalid or expired refresh token&#39;</span><span class="p">);</span>
</span><span id="__span-8-10"><a id="__codelineno-8-10" name="__codelineno-8-10" href="#__codelineno-8-10"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-8-11"><a id="__codelineno-8-11" name="__codelineno-8-11" href="#__codelineno-8-11"></a>
</span><span id="__span-8-12"><a id="__codelineno-8-12" name="__codelineno-8-12" href="#__codelineno-8-12"></a><span class="w"> </span><span class="c1">// 2. Delete old token</span>
</span><span id="__span-8-13"><a id="__codelineno-8-13" name="__codelineno-8-13" href="#__codelineno-8-13"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">refreshToken</span><span class="p">.</span><span class="ow">delete</span><span class="p">({</span>
</span><span id="__span-8-14"><a id="__codelineno-8-14" name="__codelineno-8-14" href="#__codelineno-8-14"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">oldToken.id</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-8-15"><a id="__codelineno-8-15" name="__codelineno-8-15" href="#__codelineno-8-15"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-8-16"><a id="__codelineno-8-16" name="__codelineno-8-16" href="#__codelineno-8-16"></a>
</span><span id="__span-8-17"><a id="__codelineno-8-17" name="__codelineno-8-17" href="#__codelineno-8-17"></a><span class="w"> </span><span class="c1">// 3. Generate new access + refresh tokens</span>
</span><span id="__span-8-18"><a id="__codelineno-8-18" name="__codelineno-8-18" href="#__codelineno-8-18"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">newAccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">jwt</span><span class="p">.</span><span class="nx">sign</span><span class="p">({</span><span class="w"> </span><span class="nx">userId</span><span class="o">:</span><span class="w"> </span><span class="kt">oldToken.userId</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nx">ACCESS_SECRET</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">expiresIn</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;15m&#39;</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-8-19"><a id="__codelineno-8-19" name="__codelineno-8-19" href="#__codelineno-8-19"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">newRefreshToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">jwt</span><span class="p">.</span><span class="nx">sign</span><span class="p">({</span><span class="w"> </span><span class="nx">userId</span><span class="o">:</span><span class="w"> </span><span class="kt">oldToken.userId</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nx">REFRESH_SECRET</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">expiresIn</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;7d&#39;</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-8-20"><a id="__codelineno-8-20" name="__codelineno-8-20" href="#__codelineno-8-20"></a>
</span><span id="__span-8-21"><a id="__codelineno-8-21" name="__codelineno-8-21" href="#__codelineno-8-21"></a><span class="w"> </span><span class="c1">// 4. Store new refresh token</span>
</span><span id="__span-8-22"><a id="__codelineno-8-22" name="__codelineno-8-22" href="#__codelineno-8-22"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">tx</span><span class="p">.</span><span class="nx">refreshToken</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
</span><span id="__span-8-23"><a id="__codelineno-8-23" name="__codelineno-8-23" href="#__codelineno-8-23"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-8-24"><a id="__codelineno-8-24" name="__codelineno-8-24" href="#__codelineno-8-24"></a><span class="w"> </span><span class="nx">token</span><span class="o">:</span><span class="w"> </span><span class="kt">newRefreshToken</span><span class="p">,</span>
</span><span id="__span-8-25"><a id="__codelineno-8-25" name="__codelineno-8-25" href="#__codelineno-8-25"></a><span class="w"> </span><span class="nx">userId</span><span class="o">:</span><span class="w"> </span><span class="kt">oldToken.userId</span><span class="p">,</span>
</span><span id="__span-8-26"><a id="__codelineno-8-26" name="__codelineno-8-26" href="#__codelineno-8-26"></a><span class="w"> </span><span class="nx">expiresAt</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nb">Date</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mf">7</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">24</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">1000</span><span class="p">),</span>
</span><span id="__span-8-27"><a id="__codelineno-8-27" name="__codelineno-8-27" href="#__codelineno-8-27"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-8-28"><a id="__codelineno-8-28" name="__codelineno-8-28" href="#__codelineno-8-28"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-8-29"><a id="__codelineno-8-29" name="__codelineno-8-29" href="#__codelineno-8-29"></a>
</span><span id="__span-8-30"><a id="__codelineno-8-30" name="__codelineno-8-30" href="#__codelineno-8-30"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">accessToken</span><span class="o">:</span><span class="w"> </span><span class="kt">newAccessToken</span><span class="p">,</span><span class="w"> </span><span class="nx">refreshToken</span><span class="o">:</span><span class="w"> </span><span class="kt">newRefreshToken</span><span class="w"> </span><span class="p">};</span>
</span><span id="__span-8-31"><a id="__codelineno-8-31" name="__codelineno-8-31" href="#__codelineno-8-31"></a><span class="p">});</span>
</span></code></pre></div>
<h3 id="expire-temp-users-cron">Expire Temp Users (Cron)<a class="headerlink" href="#expire-temp-users-cron" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-9-1"><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">updateMany</span><span class="p">({</span>
</span><span id="__span-9-2"><a id="__codelineno-9-2" name="__codelineno-9-2" href="#__codelineno-9-2"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-9-3"><a id="__codelineno-9-3" name="__codelineno-9-3" href="#__codelineno-9-3"></a><span class="w"> </span><span class="nx">role</span><span class="o">:</span><span class="w"> </span><span class="kt">UserRole.TEMP</span><span class="p">,</span>
</span><span id="__span-9-4"><a id="__codelineno-9-4" name="__codelineno-9-4" href="#__codelineno-9-4"></a><span class="w"> </span><span class="nx">expiresAt</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">lt</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nb">Date</span><span class="p">()</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-9-5"><a id="__codelineno-9-5" name="__codelineno-9-5" href="#__codelineno-9-5"></a><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">not</span><span class="o">:</span><span class="w"> </span><span class="kt">UserStatus.EXPIRED</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-9-6"><a id="__codelineno-9-6" name="__codelineno-9-6" href="#__codelineno-9-6"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-9-7"><a id="__codelineno-9-7" name="__codelineno-9-7" href="#__codelineno-9-7"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-9-8"><a id="__codelineno-9-8" name="__codelineno-9-8" href="#__codelineno-9-8"></a><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">UserStatus.EXPIRED</span><span class="p">,</span>
</span><span id="__span-9-9"><a id="__codelineno-9-9" name="__codelineno-9-9" href="#__codelineno-9-9"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-9-10"><a id="__codelineno-9-10" name="__codelineno-9-10" href="#__codelineno-9-10"></a><span class="p">});</span>
</span></code></pre></div>
<h3 id="clean-expired-refresh-tokens-cron">Clean Expired Refresh Tokens (Cron)<a class="headerlink" href="#clean-expired-refresh-tokens-cron" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-10-1"><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">refreshToken</span><span class="p">.</span><span class="nx">deleteMany</span><span class="p">({</span>
</span><span id="__span-10-2"><a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-10-3"><a id="__codelineno-10-3" name="__codelineno-10-3" href="#__codelineno-10-3"></a><span class="w"> </span><span class="nx">expiresAt</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">lt</span><span class="o">:</span><span class="w"> </span><span class="kt">new</span><span class="w"> </span><span class="nb">Date</span><span class="p">()</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-10-4"><a id="__codelineno-10-4" name="__codelineno-10-4" href="#__codelineno-10-4"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-10-5"><a id="__codelineno-10-5" name="__codelineno-10-5" href="#__codelineno-10-5"></a><span class="p">});</span>
</span></code></pre></div>
<hr />
<h2 id="data-flow">Data Flow<a class="headerlink" href="#data-flow" title="Permanent link">&para;</a></h2>
<h3 id="user-registration-flow">User Registration Flow<a class="headerlink" href="#user-registration-flow" title="Permanent link">&para;</a></h3>
<pre class="mermaid"><code>sequenceDiagram
participant Client
participant API
participant Prisma
participant bcrypt
participant JWT
Client-&gt;&gt;API: POST /api/auth/register (email, password, name)
API-&gt;&gt;bcrypt: hash(password)
bcrypt--&gt;&gt;API: hashedPassword
API-&gt;&gt;Prisma: user.create({ email, password: hashed, role: USER })
Prisma--&gt;&gt;API: user
API-&gt;&gt;JWT: sign(accessToken, { userId, email, role })
API-&gt;&gt;JWT: sign(refreshToken, { userId })
JWT--&gt;&gt;API: tokens
API-&gt;&gt;Prisma: refreshToken.create({ token, userId, expiresAt })
Prisma--&gt;&gt;API: refreshToken
API--&gt;&gt;Client: { user, accessToken, refreshToken }</code></pre>
<h3 id="login-flow">Login Flow<a class="headerlink" href="#login-flow" title="Permanent link">&para;</a></h3>
<pre class="mermaid"><code>sequenceDiagram
participant Client
participant API
participant Prisma
participant bcrypt
participant JWT
Client-&gt;&gt;API: POST /api/auth/login (email, password)
API-&gt;&gt;Prisma: user.findUnique({ where: { email } })
Prisma--&gt;&gt;API: user | null
API-&gt;&gt;bcrypt: compare(password, user.password)
bcrypt--&gt;&gt;API: isValid
alt Invalid credentials
API--&gt;&gt;Client: 401 Unauthorized
else Valid credentials
API-&gt;&gt;Prisma: user.update({ lastLoginAt: now() })
API-&gt;&gt;JWT: sign(accessToken, { userId, email, role })
API-&gt;&gt;JWT: sign(refreshToken, { userId })
JWT--&gt;&gt;API: tokens
API-&gt;&gt;Prisma: refreshToken.create({ token, userId, expiresAt })
Prisma--&gt;&gt;API: refreshToken
API--&gt;&gt;Client: { user, accessToken, refreshToken }
end</code></pre>
<h3 id="token-refresh-flow">Token Refresh Flow<a class="headerlink" href="#token-refresh-flow" title="Permanent link">&para;</a></h3>
<pre class="mermaid"><code>sequenceDiagram
participant Client
participant API
participant Prisma
participant JWT
Client-&gt;&gt;API: POST /api/auth/refresh (refreshToken)
API-&gt;&gt;JWT: verify(refreshToken)
JWT--&gt;&gt;API: payload | error
alt Invalid token
API--&gt;&gt;Client: 401 Unauthorized
else Valid token
API-&gt;&gt;Prisma: $transaction start
API-&gt;&gt;Prisma: refreshToken.findUnique({ where: { token } })
Prisma--&gt;&gt;API: oldToken | null
alt Token not found or expired
API-&gt;&gt;Prisma: $transaction rollback
API--&gt;&gt;Client: 401 Unauthorized
else Token valid
API-&gt;&gt;Prisma: refreshToken.delete({ where: { id: oldToken.id } })
API-&gt;&gt;JWT: sign(newAccessToken, { userId, email, role })
API-&gt;&gt;JWT: sign(newRefreshToken, { userId })
JWT--&gt;&gt;API: newTokens
API-&gt;&gt;Prisma: refreshToken.create({ token: newRefreshToken, userId, expiresAt })
API-&gt;&gt;Prisma: $transaction commit
Prisma--&gt;&gt;API: success
API--&gt;&gt;Client: { accessToken, refreshToken }
end
end</code></pre>
<hr />
<h2 id="performance-notes">Performance Notes<a class="headerlink" href="#performance-notes" title="Permanent link">&para;</a></h2>
<h3 id="index-usage">Index Usage<a class="headerlink" href="#index-usage" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>email unique index:</strong> Used for login lookups (<code>WHERE email = ?</code>)</li>
<li><strong>refreshToken.token unique index:</strong> Used for refresh endpoint (<code>WHERE token = ?</code>)</li>
<li><strong>refreshToken.userId index:</strong> Used for user deletion cascades</li>
</ul>
<h3 id="query-optimization">Query Optimization<a class="headerlink" href="#query-optimization" title="Permanent link">&para;</a></h3>
<ul>
<li>Avoid loading all 33 user relations by default — use selective <code>include</code> or <code>select</code></li>
<li>Use <code>findFirst</code> instead of <code>findMany().take(1)</code> for single record queries</li>
<li>Paginate user lists with <code>skip</code> + <code>take</code> + cursor-based pagination for large datasets</li>
</ul>
<h3 id="n1-prevention">N+1 Prevention<a class="headerlink" href="#n1-prevention" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-11-1"><a id="__codelineno-11-1" name="__codelineno-11-1" href="#__codelineno-11-1"></a><span class="c1">// ❌ N+1 query (loads campaigns one-by-one)</span>
</span><span id="__span-11-2"><a id="__codelineno-11-2" name="__codelineno-11-2" href="#__codelineno-11-2"></a><span class="kd">const</span><span class="w"> </span><span class="nx">users</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">findMany</span><span class="p">();</span>
</span><span id="__span-11-3"><a id="__codelineno-11-3" name="__codelineno-11-3" href="#__codelineno-11-3"></a><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">user</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">users</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-11-4"><a id="__codelineno-11-4" name="__codelineno-11-4" href="#__codelineno-11-4"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">campaigns</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">campaign</span><span class="p">.</span><span class="nx">findMany</span><span class="p">({</span><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">createdByUserId</span><span class="o">:</span><span class="w"> </span><span class="kt">user.id</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-11-5"><a id="__codelineno-11-5" name="__codelineno-11-5" href="#__codelineno-11-5"></a><span class="p">}</span>
</span><span id="__span-11-6"><a id="__codelineno-11-6" name="__codelineno-11-6" href="#__codelineno-11-6"></a>
</span><span id="__span-11-7"><a id="__codelineno-11-7" name="__codelineno-11-7" href="#__codelineno-11-7"></a><span class="c1">// ✅ Single query with include</span>
</span><span id="__span-11-8"><a id="__codelineno-11-8" name="__codelineno-11-8" href="#__codelineno-11-8"></a><span class="kd">const</span><span class="w"> </span><span class="nx">users</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">findMany</span><span class="p">({</span>
</span><span id="__span-11-9"><a id="__codelineno-11-9" name="__codelineno-11-9" href="#__codelineno-11-9"></a><span class="w"> </span><span class="nx">include</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-11-10"><a id="__codelineno-11-10" name="__codelineno-11-10" href="#__codelineno-11-10"></a><span class="w"> </span><span class="nx">campaignsCreated</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-11-11"><a id="__codelineno-11-11" name="__codelineno-11-11" href="#__codelineno-11-11"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-11-12"><a id="__codelineno-11-12" name="__codelineno-11-12" href="#__codelineno-11-12"></a><span class="p">});</span>
</span></code></pre></div>
<hr />
<h2 id="security-considerations">Security Considerations<a class="headerlink" href="#security-considerations" title="Permanent link">&para;</a></h2>
<h3 id="password-policy">Password Policy<a class="headerlink" href="#password-policy" title="Permanent link">&para;</a></h3>
<p>Enforced at API schema level (<code>auth.schemas.ts</code>):
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-12-1"><a id="__codelineno-12-1" name="__codelineno-12-1" href="#__codelineno-12-1"></a><span class="nx">password</span><span class="o">:</span><span class="w"> </span><span class="kt">z.string</span><span class="p">()</span>
</span><span id="__span-12-2"><a id="__codelineno-12-2" name="__codelineno-12-2" href="#__codelineno-12-2"></a><span class="w"> </span><span class="p">.</span><span class="nx">min</span><span class="p">(</span><span class="mf">12</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Password must be at least 12 characters&#39;</span><span class="p">)</span>
</span><span id="__span-12-3"><a id="__codelineno-12-3" name="__codelineno-12-3" href="#__codelineno-12-3"></a><span class="w"> </span><span class="p">.</span><span class="nx">regex</span><span class="p">(</span><span class="sr">/[A-Z]/</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Password must contain at least one uppercase letter&#39;</span><span class="p">)</span>
</span><span id="__span-12-4"><a id="__codelineno-12-4" name="__codelineno-12-4" href="#__codelineno-12-4"></a><span class="w"> </span><span class="p">.</span><span class="nx">regex</span><span class="p">(</span><span class="sr">/[a-z]/</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Password must contain at least one lowercase letter&#39;</span><span class="p">)</span>
</span><span id="__span-12-5"><a id="__codelineno-12-5" name="__codelineno-12-5" href="#__codelineno-12-5"></a><span class="w"> </span><span class="p">.</span><span class="nx">regex</span><span class="p">(</span><span class="sr">/[0-9]/</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;Password must contain at least one digit&#39;</span><span class="p">)</span>
</span></code></pre></div></p>
<h3 id="user-enumeration-prevention">User Enumeration Prevention<a class="headerlink" href="#user-enumeration-prevention" title="Permanent link">&para;</a></h3>
<ul>
<li><code>/api/auth/me</code> returns 401 (not 404) for missing users</li>
<li>Login endpoint returns generic "Invalid credentials" (not "Email not found")</li>
<li>Registration endpoint returns generic "Email already exists" (no user details)</li>
</ul>
<h3 id="refresh-token-security">Refresh Token Security<a class="headerlink" href="#refresh-token-security" title="Permanent link">&para;</a></h3>
<ul>
<li>Tokens stored in database (not just signed JWTs)</li>
<li>Rotation on every refresh (old token deleted)</li>
<li>Atomic transaction prevents race conditions</li>
<li>7-day expiration with daily cleanup cron</li>
</ul>
<h3 id="role-based-access-control">Role-Based Access Control<a class="headerlink" href="#role-based-access-control" title="Permanent link">&para;</a></h3>
<p>Middleware enforces role requirements:
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-13-1"><a id="__codelineno-13-1" name="__codelineno-13-1" href="#__codelineno-13-1"></a><span class="c1">// Requires SUPER_ADMIN or MAP_ADMIN</span>
</span><span id="__span-13-2"><a id="__codelineno-13-2" name="__codelineno-13-2" href="#__codelineno-13-2"></a><span class="nx">router</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/api/locations&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">requireRole</span><span class="p">(</span><span class="nx">UserRole</span><span class="p">.</span><span class="nx">SUPER_ADMIN</span><span class="p">,</span><span class="w"> </span><span class="nx">UserRole</span><span class="p">.</span><span class="nx">MAP_ADMIN</span><span class="p">),</span><span class="w"> </span><span class="p">...)</span>
</span><span id="__span-13-3"><a id="__codelineno-13-3" name="__codelineno-13-3" href="#__codelineno-13-3"></a>
</span><span id="__span-13-4"><a id="__codelineno-13-4" name="__codelineno-13-4" href="#__codelineno-13-4"></a><span class="c1">// Requires any non-TEMP user</span>
</span><span id="__span-13-5"><a id="__codelineno-13-5" name="__codelineno-13-5" href="#__codelineno-13-5"></a><span class="nx">router</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/api/campaigns&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">requireNonTemp</span><span class="p">,</span><span class="w"> </span><span class="p">...)</span>
</span><span id="__span-13-6"><a id="__codelineno-13-6" name="__codelineno-13-6" href="#__codelineno-13-6"></a>
</span><span id="__span-13-7"><a id="__codelineno-13-7" name="__codelineno-13-7" href="#__codelineno-13-7"></a><span class="c1">// Requires any authenticated user</span>
</span><span id="__span-13-8"><a id="__codelineno-13-8" name="__codelineno-13-8" href="#__codelineno-13-8"></a><span class="nx">router</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/api/profile&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">authenticate</span><span class="p">,</span><span class="w"> </span><span class="p">...)</span>
</span></code></pre></div></p>
<h3 id="temp-user-restrictions">TEMP User Restrictions<a class="headerlink" href="#temp-user-restrictions" title="Permanent link">&para;</a></h3>
<ul>
<li>Cannot access admin routes (blocked by <code>requireNonTemp</code> middleware)</li>
<li>Cannot create campaigns, locations, or templates</li>
<li>Can only canvass within assigned cut (verified by canvass service)</li>
<li>Auto-expire after <code>expireDays</code> (default 30)</li>
</ul>
<hr />
<h2 id="troubleshooting">Troubleshooting<a class="headerlink" href="#troubleshooting" title="Permanent link">&para;</a></h2>
<h3 id="email-already-exists-on-registration">"Email already exists" on registration<a class="headerlink" href="#email-already-exists-on-registration" title="Permanent link">&para;</a></h3>
<p><strong>Cause:</strong> Email uniqueness constraint violated
<strong>Solution:</strong> Check for existing user: <code>prisma.user.findUnique({ where: { email } })</code></p>
<h3 id="invalid-refresh-token-on-refresh">"Invalid refresh token" on refresh<a class="headerlink" href="#invalid-refresh-token-on-refresh" title="Permanent link">&para;</a></h3>
<p><strong>Cause:</strong> Token already used (rotation), expired, or manually deleted
<strong>Solution:</strong> User must re-login to obtain new token pair</p>
<h3 id="password-does-not-meet-policy-on-update">"Password does not meet policy" on update<a class="headerlink" href="#password-does-not-meet-policy-on-update" title="Permanent link">&para;</a></h3>
<p><strong>Cause:</strong> Password validation regex mismatch
<strong>Solution:</strong> Ensure new password has 12+ chars, 1 uppercase, 1 lowercase, 1 digit</p>
<h3 id="temp-user-cannot-access-route">TEMP user cannot access route<a class="headerlink" href="#temp-user-cannot-access-route" title="Permanent link">&para;</a></h3>
<p><strong>Cause:</strong> Route uses <code>requireNonTemp</code> middleware
<strong>Solution:</strong> Upgrade user to <code>USER</code> role via admin panel</p>
<h3 id="circular-dependency-auth-store-api-client">Circular dependency: auth store ↔ api client<a class="headerlink" href="#circular-dependency-auth-store-api-client" title="Permanent link">&para;</a></h3>
<p><strong>Cause:</strong> Both modules import each other
<strong>Solution:</strong> Use callback registration pattern (see <code>admin/src/lib/api.ts</code> + <code>admin/src/stores/auth.store.ts</code>)</p>
<hr />
<h2 id="related-documentation">Related Documentation<a class="headerlink" href="#related-documentation" title="Permanent link">&para;</a></h2>
<ul>
<li><a href="../../">Database Overview</a> — Complete ER diagram</li>
<li><a href="../../schema/">Schema Reference</a> — All model fields</li>
<li><a href="../influence/">Influence Models</a> — Campaign relations</li>
<li><a href="../map/">Map Models</a> — Location relations</li>
<li><a href="../canvass/">Canvassing Models</a> — Session relations</li>
<li><a href="../email-templates/">Email Template Models</a> — Template relations</li>
<li><a href="../../api/auth.md">API Auth Routes</a> — Authentication endpoints</li>
<li><a href="../../deployment/security.md">Security Audit</a> — Security findings and fixes</li>
</ul>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../" class="md-footer__link md-footer__link--prev" aria-label="Previous: Database Models">
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</div>
<div class="md-footer__title">
<span class="md-footer__direction">
Previous
</span>
<div class="md-ellipsis">
Database Models
</div>
</div>
</a>
<a href="../settings/" class="md-footer__link md-footer__link--next" aria-label="Next: Settings Models">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
Settings Models
</div>
</div>
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg>
</div>
</a>
</nav>
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
<div class="md-copyright__highlight">
Copyright &copy; 2024 The Bunker Operations <a href="#__consent">Change cookie settings</a>
</div>
</div>
<div class="md-social">
<a href="https://gitea.bnkops.com/admin" target="_blank" rel="noopener" title="Gitea Repository" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M173.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9M252.8 8C114.1 8 8 113.3 8 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C436.2 457.8 504 362.9 504 252 504 113.3 391.5 8 252.8 8M105.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2"/></svg>
</a>
<a href="https://listmonk.bnkops.com/subscription/form" target="_blank" rel="noopener" title="Newsletter" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M536.4-26.3c9.8-3.5 20.6-1 28 6.3s9.8 18.2 6.3 28l-178 496.9c-5 13.9-18.1 23.1-32.8 23.1-14.2 0-27-8.6-32.3-21.7l-64.2-158c-4.5-11-2.5-23.6 5.2-32.6l94.5-112.4c5.1-6.1 4.7-15-.9-20.6s-14.6-6-20.6-.9l-112.4 94.3c-9.1 7.6-21.6 9.6-32.6 5.2L38.1 216.8c-13.1-5.3-21.7-18.1-21.7-32.3 0-14.7 9.2-27.8 23.1-32.8z"/></svg>
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "../../../..", "features": ["announce.dismiss", "content.action.edit", "content.action.view", "content.code.annotate", "content.code.copy", "content.tooltips", "navigation.expand", "navigation.footer", "navigation.indexes", "navigation.path", "navigation.prune", "navigation.sections", "navigation.tabs", "navigation.tabs.sticky", "navigation.top", "navigation.tracking", "search.highlight", "search.share", "search.suggest", "toc.follow"], "search": "../../../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../../../assets/javascripts/bundle.79ae519e.min.js"></script>
<script src="../../../../javascripts/home.js"></script>
<script src="../../../../javascripts/github-widget.js"></script>
<script src="../../../../javascripts/gitea-widget.js"></script>
<script src="../../../../assets/js/env-config.js"></script>
<script src="../../../../assets/js/video-player.js"></script>
</body>
</html>