1// auth.js â ë¡ê·¸ì¸ + ìí 2// ë°ëª¨ëª¨ë: ìí ì íì¼ë¡ ë¡ê·¸ì¸ ì뮬 (ë§ì¤í°/ê´ë¦¬ì/ê³ ê° íë©´ ì°¨ì´ë¥¼ ê·¸ëë¡ ì²´í) 3// ë¼ì´ë¸ëª¨ë: Supabase ì´ë©ì¼+ë¹ë°ë²í¸ â accounts íìì ìí ì¡°í. ì´ì´ì êµ¬ê¸ OTP 2ë¨ê³ ì¸ì¦ â ë§ì¤í° íì·그 ì¸ ê¶ì (mfa.js, 2026-09-19). 4import { IS_DEMO, SUPABASE, APP_NAME } from './config.js'; 5import { esc } from './util.js'; 6import { setAuthToken } from './data.js'; 7import { checkBeforeLogin, recordFail, recordSuccess, humanWait } from './loginguard.js'; 8import { mfaGate } from './mfa.js'; // 2ë¨ê³ ì¸ì¦: ë§ì¤í° íì · ê·¸ ì¸ ê¶ì (2026-09-19) 9import { SCOPE } from './scope.js'; // ì ì© ì½ìì´ë©´ ì ëª©ë§ ë°ê¾¼ë¤ (2026-09-17) 10 11export const ROLES = { 12 master: { label: 'ë§ì¤í°(ëí)', desc: 'ì ì²´ ì´ë(ê°ëª )·ê³ì /ë¨ê°/í기 ê´ë¦¬' }, 13 manager: { label: 'ê´ë¦¬ì', desc: 'ê°ëª DB·ì ì ê²½ë¡Â·ì ì , ì기 ì ì²´ë§' }, 14 client: { label: 'ê³ ê°(ê´ê³ 주)', desc: 'ì기 ì ì²´ DB(ì본)·íê´ë¦¬Â·DB ë¶ë°°' }, 15 client_member: { label: 'ê³ ê° íì', desc: 'ë°°ì ë°ì DBë§ (ì본)' }, 16}; 17 18export function currentUser() { 19 try { return JSON.parse(sessionStorage.getItem('dbs_user')) || null; } catch { return null; } 20} 21export function logout() { sessionStorage.removeItem('dbs_user'); location.reload(); } 22 23// ë¹ë°ë²í¸ ì¤ì ë§í¬(recovery)ë¡ ì§ì í ê²½ì°: ì ë¹ë°ë²í¸ ë±ë¡ íë©´ (2026-08-27) 24// ê´ë¦¬ìê° ë°ê¸í ë§í¬ í´ë¦ â #access_token...type=recovery ë¡ ëì°© â ì¬ê¸°ì ì ë¹ë² ì ì¥ 25function handleRecovery(el) { 26 const h = new URLSearchParams(location.hash.slice(1)); 27 if (h.get('type') !== 'recovery' || !h.get('access_token')) return false; 28 const token = h.get('access_token'); 29 el.innerHTML = ` 30 <div class="login-card"> 31 <h2>${esc(SCOPE ? SCOPE.appName : APP_NAME)}</h2> 32 <p class="muted">ì ë¹ë°ë²í¸ë¥¼ ì¤ì íì¸ì.</p> 33 <input id="rc-pw" type="password" placeholder="ì ë¹ë°ë²í¸ (8ì ì´ì)" style="width:100%;margin-bottom:8px"> 34 <input id="rc-pw2" type="password" placeholder="ë¹ë°ë²í¸ íì¸" style="width:100%;margin-bottom:12px"> 35 <button id="rc-save" class="primary" style="width:100%">ë¹ë°ë²í¸ ì¤ì </button> 36 <p id="rc-err" style="color:#d62b1f;font-size:13px;margin-top:8px"></p> 37 </div>`; 38 el.querySelector('#rc-save').addEventListener('click', async () => { 39 const pw = el.querySelector('#rc-pw').value, pw2 = el.querySelector('#rc-pw2').value; 40 const err = el.querySelector('#rc-err'); 41 if (pw.length < 8) { err.textContent = '8ì ì´ìì¼ë¡ ì í´ì£¼ì¸ì.'; return; } 42 if (pw !== pw2) { err.textContent = 'ë ì ë ¥ì´ ìë¡ ë¤ë¦ ëë¤.'; return; } 43 try { 44 const res = await fetch(SUPABASE.url + '/auth/v1/user', { 45 method: 'PUT', 46 headers: { apikey: SUPABASE.anonKey, Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, 47 body: JSON.stringify({ password: pw }), 48 }); 49 if (!res.ok) throw new Error((await res.json()).msg || 'ì¤ì ì¤í¨'); 50 history.replaceState(null, '', location.pathname); 51 alert('ë¹ë°ë²í¸ê° ì¤ì ëììµëë¤. ì ë¹ë°ë²í¸ë¡ ë¡ê·¸ì¸íì¸ì.'); 52 location.reload(); 53 } catch (e) { err.textContent = String(e.message || e); } 54 }); 55 return true; 56} 57 58// ë¡ê·¸ì¸ íë©´ì ê·¸ë¦¬ê³ , ì±ê³µ ì onLogin(user) í¸ì¶ 59export function renderLogin(el, onLogin) { 60 if (!IS_DEMO && handleRecovery(el)) return; 61 if (IS_DEMO) { 62 el.innerHTML = ` 63 <div class="login-card"> 64 <h2>${esc(SCOPE ? SCOPE.appName : APP_NAME)}</h2> 65 <p class="muted">ë°ëª¨ëª¨ë â ìí ì ê³¨ë¼ íë©´ ì°¨ì´ë¥¼ íì¸íì¸ì.<br>(config.jsì Supabase í¤ë¥¼ ë£ì¼ë©´ ì¤ê³ì ë¡ê·¸ì¸ì¼ë¡ ë°ëëë¤)</p> 66 <div class="role-btns"> 67 <button data-role="master" data-acc="acc-boss">ð ë§ì¤í°(ëí)</button> 68 <button data-role="manager" data-co="co-1" data-acc="acc-mgr">ð§âð¼ ê´ë¦¬ì â íê²°ë²ë¬´ ë´ë¹</button> 69 <button data-role="client" data-co="co-1" data-acc="acc-client">ð¤ ê³ ê°(ê´ê³ 주 íì¥) â íê²°ë²ë¬´</button> 70 <button data-role="client_member" data-co="co-1" data-acc="acc-m1">ð¤ ê³ ê° íì â ìë´ìA (ë°°ì ë¶ë§)</button> 71 </div> 72 <p class="muted" style="font-size:11px">ë§ì¤í°Â·ê´ë¦¬ìë ì´ë¦Â·ì°ë½ì²ê° <b>ê°ëª </b>ì¼ë¡, ê³ ê°ì ì기 ì ì²´ <b>ì본</b>ì¼ë¡ ë³´ì ëë¤ (ì구ì¬íì 기ì¤).</p> 73 </div>`; 74 el.querySelectorAll('button[data-role]').forEach(b => b.addEventListener('click', () => { 75 const user = { role: b.dataset.role, company_id: b.dataset.co || null, account_id: b.dataset.acc || null, email: b.dataset.role + '@demo', name: b.textContent.trim() }; 76 sessionStorage.setItem('dbs_user', JSON.stringify(user)); 77 onLogin(user); 78 })); 79 return; 80 } 81 // ââ ë¼ì´ë¸ëª¨ë ë¡ê·¸ì¸ ââ 82 el.innerHTML = ` 83 <div class="login-card"> 84 <h2>${esc(SCOPE ? SCOPE.appName : APP_NAME)}</h2> 85 <input id="li-email" type="email" placeholder="ì´ë©ì¼" autocomplete="username"> 86 <input id="li-pw" type="password" placeholder="ë¹ë°ë²í¸" autocomplete="current-password"> 87 <button id="li-btn" class="primary">ë¡ê·¸ì¸</button> 88 <div id="li-err" class="err"></div> 89 </div>`; 90 // ìí°ë¡ ì ì¶ 91 el.querySelectorAll('#li-email, #li-pw').forEach(i => i.addEventListener('keydown', e => { 92 if (e.key === 'Enter') el.querySelector('#li-btn').click(); 93 })); 94 el.querySelector('#li-btn').addEventListener('click', async () => { 95 const err = el.querySelector('#li-err'); 96 const btn = el.querySelector('#li-btn'); 97 err.textContent = ''; 98 const email = el.querySelector('#li-email').value.trim(); 99 try { 100 // ì ê¸ íì¸ (2026-09-16) â 5íâ 10ë¶ / 10íâ 1ìê° / 15íâ 10ìê° 101 if (email) { 102 const g = await checkBeforeLogin(email); 103 if (g.locked) { 104 err.textContent = `ë¡ê·¸ì¸ì´ ì ê²¼ìµëë¤. ${humanWait(g.wait_sec)} í ë¤ì ìëíì¸ì. (ì¤í¨ ${g.fails}í)`; 105 return; 106 } 107 } 108 btn.disabled = true; 109 const res = await fetch(SUPABASE.url + '/auth/v1/token?grant_type=password', { 110 method: 'POST', 111 headers: { apikey: SUPABASE.anonKey, 'Content-Type': 'application/json' }, 112 body: JSON.stringify({ email, password: el.querySelector('#li-pw').value }), 113 }); 114 const out = await res.json(); 115 if (!res.ok) {
116 // ë¹ë°ë²í¸ ì¤ë¥ â ì¤í¨ 1í 기ë¡íê³ , ë¤ì ë¨ê³ ì ê¸ì´ë©´ ê·¸ ì¬ì¤ì ìë ¤ì¤ë¤ 117 const st = await recordFail(email); 118 const base = out.error_description || out.msg || 'ë¡ê·¸ì¸ ì¤í¨'; 119 throw new Error(st.locked ? `${base} â ì¤í¨ ${st.fails}íë¡ ${humanWait(st.wait_sec)} ëì ì ê²¼ìµëë¤.` 120 : `${base} (ì¤í¨ ${st.fails}í)`); 121 } 122 setAuthToken(out.access_token); 123 await recordSuccess(email, out.access_token); // ì±ê³µ ì ì¹´ì´í° 리ì 124 const rows = await fetch(SUPABASE.url + '/rest/v1/accounts?select=*&auth_id=eq.' + out.user.id, { 125 headers: { apikey: SUPABASE.anonKey, Authorization: 'Bearer ' + out.access_token }, 126 }).then(r => r.json()); 127 const acc = rows && rows[0]; 128 if (!acc || !acc.active) throw new Error('ë±ë¡ëì§ ììê±°ë ë¹íì±íë ê³ì ì ëë¤.'); 129 // ë³´ì: ìí íì´í¸ë¦¬ì¤í¸ â 미ì ìí ì ìµìê¶í(client_member) 130 const role = ['master', 'manager', 'client', 'client_member'].includes(acc.role) ? acc.role : 'client_member'; 131 // 2ë¨ê³ ì¸ì¦ (2026-09-19) â ë§ì¤í°ë íì, ê·¸ ì¸ë ê¶ì (í루 ëì ë³´ì§ ì기 ê°ë¥). ë±ë¡í ì¬ëì 6ì리를 íµê³¼í´ì¼ ë¤ì´ê°ë¤. 132 // íµê³¼íë©´ "2ë¨ê³ íµê³¼" ëì¥ì´ ì°í ì í í°ì ë°ëë¤. ê¶ì 를 ë기면 s2 = null â ì§ê¸ í í° ê·¸ëë¡ ì ì¥. 133 const s2 = await mfaGate(el, out, email, role); 134 if (s2) { 135 out.access_token = s2.access_token; 136 out.refresh_token = s2.refresh_token || out.refresh_token; 137 out.expires_in = s2.expires_in || out.expires_in; 138 setAuthToken(out.access_token); 139 } 140 const user = { 141 role, company_id: acc.company_id, account_id: acc.id, email: acc.email, name: acc.name, 142 // ë¤ì¤ ë´ë¹(2026-08-26): company_ids ë°°ì´ ì°ì , ìì¼ë©´ ë¨ì¼ company_id í´ë°± 143 company_ids: (acc.company_ids && acc.company_ids.length ? acc.company_ids : (acc.company_id ? [acc.company_id] : [])), 144 token: out.access_token, 145 refresh_token: out.refresh_token, // JWT ìë ê°±ì ì© 146 expires_at: Date.now() + (out.expires_in || 3600) * 1000, // ë§ë£ ìê° 147 }; 148 sessionStorage.setItem('dbs_user', JSON.stringify(user)); 149 onLogin(user); 150 } catch (e) { 151 // 2ë¨ê³ íë©´ì¼ë¡ ëì´ê° ë¤ ë ì¤ë¥ë©´ ë¡ê·¸ì¸ ì¹¸ì´ ì´ë¯¸ ì¬ë¼ì¡ë¤ â ë¡ê·¸ì¸ íë©´ì ë¤ì ê·¸ë¦¬ê³ ìë¦°ë¤ 152 if (!err.isConnected) { renderLogin(el, onLogin); const e2 = el.querySelector('#li-err'); if (e2) e2.textContent = String(e.message || e); } 153 else err.textContent = String(e.message || e); 154 } finally { 155 const b = el.querySelector('#li-btn'); if (b) b.disabled = false; 156 } 157 }); 158}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.