GitHub Actions commited on
Commit
e8f3ea9
ยท
1 Parent(s): 5eb8d48

Auto-deploy from GitHub Actions - 2025-12-12 04:33:23

Browse files
app/__init__.py CHANGED
@@ -221,13 +221,18 @@ def migrate_database(app: Flask) -> None:
221
  with app.app_context():
222
  inspector = inspect(db.engine)
223
 
224
- # 1. user ํ…Œ์ด๋ธ” - nickname ์ปฌ๋Ÿผ
225
  if inspector.has_table('user'):
226
  columns = [c['name'] for c in inspector.get_columns('user')]
227
- if 'nickname' not in columns:
228
- logger.info("user ํ…Œ์ด๋ธ”์— nickname ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
229
- with db.engine.begin() as conn:
230
  conn.execute(text("ALTER TABLE \"user\" ADD COLUMN nickname VARCHAR(80)"))
 
 
 
 
 
231
 
232
  # 2. uploaded_file ํ…Œ์ด๋ธ” - uploaded_by, parent_file_id ์ปฌ๋Ÿผ
233
  if inspector.has_table('uploaded_file'):
 
221
  with app.app_context():
222
  inspector = inspect(db.engine)
223
 
224
+ # 1. user ํ…Œ์ด๋ธ” - nickname, must_change_password ์ปฌ๋Ÿผ
225
  if inspector.has_table('user'):
226
  columns = [c['name'] for c in inspector.get_columns('user')]
227
+ with db.engine.begin() as conn:
228
+ if 'nickname' not in columns:
229
+ logger.info("user ํ…Œ์ด๋ธ”์— nickname ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
230
  conn.execute(text("ALTER TABLE \"user\" ADD COLUMN nickname VARCHAR(80)"))
231
+
232
+ if 'must_change_password' not in columns:
233
+ logger.info("user ํ…Œ์ด๋ธ”์— must_change_password ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ์ค‘...")
234
+ # Boolean ํƒ€์ž…์€ DB์— ๋”ฐ๋ผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ฃผ์˜ (PostgreSQL/SQLite ๋ชจ๋‘ BOOLEAN ์ง€์›)
235
+ conn.execute(text("ALTER TABLE \"user\" ADD COLUMN must_change_password BOOLEAN DEFAULT FALSE NOT NULL"))
236
 
237
  # 2. uploaded_file ํ…Œ์ด๋ธ” - uploaded_by, parent_file_id ์ปฌ๋Ÿผ
238
  if inspector.has_table('uploaded_file'):
app/database.py CHANGED
@@ -13,6 +13,7 @@ class User(UserMixin, db.Model):
13
  password_hash = db.Column(db.String(255), nullable=False)
14
  is_admin = db.Column(db.Boolean, default=False, nullable=False)
15
  is_active = db.Column(db.Boolean, default=True, nullable=False)
 
16
  created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
17
  last_login = db.Column(db.DateTime, nullable=True)
18
 
@@ -29,6 +30,7 @@ class User(UserMixin, db.Model):
29
  'nickname': self.nickname,
30
  'is_admin': self.is_admin,
31
  'is_active': self.is_active,
 
32
  'created_at': self.created_at.isoformat() if self.created_at else None,
33
  'last_login': self.last_login.isoformat() if self.last_login else None
34
  }
 
13
  password_hash = db.Column(db.String(255), nullable=False)
14
  is_admin = db.Column(db.Boolean, default=False, nullable=False)
15
  is_active = db.Column(db.Boolean, default=True, nullable=False)
16
+ must_change_password = db.Column(db.Boolean, default=False, nullable=False) # ๋‹ค์Œ ๋กœ๊ทธ์ธ ์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ๊ฐ•์ œ ์—ฌ๋ถ€
17
  created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
18
  last_login = db.Column(db.DateTime, nullable=True)
19
 
 
30
  'nickname': self.nickname,
31
  'is_admin': self.is_admin,
32
  'is_active': self.is_active,
33
+ 'must_change_password': self.must_change_password,
34
  'created_at': self.created_at.isoformat() if self.created_at else None,
35
  'last_login': self.last_login.isoformat() if self.last_login else None
36
  }
app/routes.py CHANGED
@@ -1807,6 +1807,50 @@ def search_relevant_chunks_fallback(query, file_ids=None, model_name=None, top_k
1807
  traceback.print_exc()
1808
  return []
1809
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1810
  @main_bp.route('/login', methods=['GET', 'POST'])
1811
  def login():
1812
  """๋กœ๊ทธ์ธ ํŽ˜์ด์ง€"""
@@ -1830,6 +1874,11 @@ def login():
1830
  login_user(user)
1831
  user.last_login = datetime.utcnow()
1832
  db.session.commit()
 
 
 
 
 
1833
  next_page = request.args.get('next')
1834
  # ๊ด€๋ฆฌ์ž์ธ ๊ฒฝ์šฐ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
1835
  if user.is_admin:
 
1807
  traceback.print_exc()
1808
  return []
1809
 
1810
+ @main_bp.before_request
1811
+ def check_password_change_requirement():
1812
+ """๋‹ค์Œ ๋กœ๊ทธ์ธ ์‹œ ํŒจ์Šค์›Œ๋“œ ๋ณ€๊ฒฝ ๊ฐ•์ œ ํ™•์ธ ๋ฏธ๋“ค์›จ์–ด"""
1813
+ if current_user.is_authenticated and current_user.must_change_password:
1814
+ # ํ—ˆ์šฉ๋œ ์—”๋“œํฌ์ธํŠธ ๋ชฉ๋ก (๋กœ๊ทธ์•„์›ƒ, ํŒจ์Šค์›Œ๋“œ ๋ณ€๊ฒฝ, ์ •์  ํŒŒ์ผ)
1815
+ allowed_endpoints = [
1816
+ 'main.logout',
1817
+ 'main.change_password',
1818
+ 'main.change_password_post',
1819
+ 'static'
1820
+ ]
1821
+
1822
+ # ํ˜„์žฌ ์š”์ฒญ์ด ํ—ˆ์šฉ๋œ ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์•„๋‹ˆ๊ณ  ์ •์  ํŒŒ์ผ๋„ ์•„๋‹ˆ๋ฉด ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
1823
+ if request.endpoint not in allowed_endpoints and not request.path.startswith('/static/'):
1824
+ return redirect(url_for('main.change_password'))
1825
+
1826
+ @main_bp.route('/change-password', methods=['GET'])
1827
+ @login_required
1828
+ def change_password():
1829
+ """ํŒจ์Šค์›Œ๋“œ ๋ณ€๊ฒฝ ํŽ˜์ด์ง€"""
1830
+ return render_template('change_password.html')
1831
+
1832
+ @main_bp.route('/change-password', methods=['POST'])
1833
+ @login_required
1834
+ def change_password_post():
1835
+ """ํŒจ์Šค์›Œ๋“œ ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ"""
1836
+ password = request.form.get('password')
1837
+ confirm_password = request.form.get('confirm_password')
1838
+
1839
+ if not password or not confirm_password:
1840
+ return render_template('change_password.html', error="๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
1841
+
1842
+ if password != confirm_password:
1843
+ return render_template('change_password.html', error="๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
1844
+
1845
+ try:
1846
+ current_user.set_password(password)
1847
+ current_user.must_change_password = False
1848
+ db.session.commit()
1849
+ return redirect(url_for('main.index'))
1850
+ except Exception as e:
1851
+ db.session.rollback()
1852
+ return render_template('change_password.html', error=f"์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}")
1853
+
1854
  @main_bp.route('/login', methods=['GET', 'POST'])
1855
  def login():
1856
  """๋กœ๊ทธ์ธ ํŽ˜์ด์ง€"""
 
1874
  login_user(user)
1875
  user.last_login = datetime.utcnow()
1876
  db.session.commit()
1877
+
1878
+ # ํŒจ์Šค์›Œ๋“œ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
1879
+ if user.must_change_password:
1880
+ return redirect(url_for('main.change_password'))
1881
+
1882
  next_page = request.args.get('next')
1883
  # ๊ด€๋ฆฌ์ž์ธ ๊ฒฝ์šฐ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
1884
  if user.is_admin:
templates/admin.html CHANGED
@@ -657,7 +657,7 @@
657
  <td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else '-' }}</td>
658
  <td>{{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else '-' }}</td>
659
  <td>
660
- <button class="btn btn-secondary" onclick="openEditModal({{ user.id }}, '{{ user.username }}', '{{ user.nickname or '' }}', {{ user.is_admin|lower }}, {{ user.is_active|lower }})" style="padding: 4px 8px; font-size: 12px;">์ˆ˜์ •</button>
661
  {% if user.id != current_user.id %}
662
  <button class="btn btn-danger" onclick="deleteUser({{ user.id }})" style="padding: 4px 8px; font-size: 12px;">์‚ญ์ œ</button>
663
  {% endif %}
@@ -699,6 +699,10 @@
699
  <input type="checkbox" id="isActive" name="is_active" checked>
700
  <label for="isActive">ํ™œ์„ฑ ์ƒํƒœ</label>
701
  </div>
 
 
 
 
702
  <div class="modal-actions">
703
  <button type="button" class="btn btn-secondary" onclick="closeModal()">์ทจ์†Œ</button>
704
  <button type="submit" class="btn btn-primary">์ €์žฅ</button>
@@ -743,10 +747,11 @@
743
  document.getElementById('userId').value = '';
744
  document.getElementById('password').required = true;
745
  document.getElementById('isActive').checked = true;
 
746
  document.getElementById('userModal').classList.add('active');
747
  }
748
 
749
- function openEditModal(userId, username, nickname, isAdmin, isActive) {
750
  currentEditUserId = userId;
751
  document.getElementById('modalTitle').textContent = '์‚ฌ์šฉ์ž ์ˆ˜์ •';
752
  document.getElementById('userId').value = userId;
@@ -756,6 +761,7 @@
756
  document.getElementById('password').required = false;
757
  document.getElementById('isAdmin').checked = isAdmin;
758
  document.getElementById('isActive').checked = isActive;
 
759
  document.getElementById('userModal').classList.add('active');
760
  }
761
 
@@ -772,7 +778,8 @@
772
  nickname: document.getElementById('nickname').value.trim(),
773
  password: document.getElementById('password').value,
774
  is_admin: document.getElementById('isAdmin').checked,
775
- is_active: document.getElementById('isActive').checked
 
776
  };
777
 
778
  const userId = document.getElementById('userId').value;
 
657
  <td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else '-' }}</td>
658
  <td>{{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else '-' }}</td>
659
  <td>
660
+ <button class="btn btn-secondary" onclick="openEditModal({{ user.id }}, '{{ user.username }}', '{{ user.nickname or '' }}', {{ user.is_admin|lower }}, {{ user.is_active|lower }}, {{ user.must_change_password|lower }})" style="padding: 4px 8px; font-size: 12px;">์ˆ˜์ •</button>
661
  {% if user.id != current_user.id %}
662
  <button class="btn btn-danger" onclick="deleteUser({{ user.id }})" style="padding: 4px 8px; font-size: 12px;">์‚ญ์ œ</button>
663
  {% endif %}
 
699
  <input type="checkbox" id="isActive" name="is_active" checked>
700
  <label for="isActive">ํ™œ์„ฑ ์ƒํƒœ</label>
701
  </div>
702
+ <div class="form-group-checkbox">
703
+ <input type="checkbox" id="mustChangePassword" name="must_change_password">
704
+ <label for="mustChangePassword">๋‹ค์Œ ๋กœ๊ทธ์ธ ์‹œ ํŒจ์Šค์›Œ๋“œ ๋ณ€๊ฒฝ</label>
705
+ </div>
706
  <div class="modal-actions">
707
  <button type="button" class="btn btn-secondary" onclick="closeModal()">์ทจ์†Œ</button>
708
  <button type="submit" class="btn btn-primary">์ €์žฅ</button>
 
747
  document.getElementById('userId').value = '';
748
  document.getElementById('password').required = true;
749
  document.getElementById('isActive').checked = true;
750
+ document.getElementById('mustChangePassword').checked = false;
751
  document.getElementById('userModal').classList.add('active');
752
  }
753
 
754
+ function openEditModal(userId, username, nickname, isAdmin, isActive, mustChangePassword) {
755
  currentEditUserId = userId;
756
  document.getElementById('modalTitle').textContent = '์‚ฌ์šฉ์ž ์ˆ˜์ •';
757
  document.getElementById('userId').value = userId;
 
761
  document.getElementById('password').required = false;
762
  document.getElementById('isAdmin').checked = isAdmin;
763
  document.getElementById('isActive').checked = isActive;
764
+ document.getElementById('mustChangePassword').checked = mustChangePassword;
765
  document.getElementById('userModal').classList.add('active');
766
  }
767
 
 
778
  nickname: document.getElementById('nickname').value.trim(),
779
  password: document.getElementById('password').value,
780
  is_admin: document.getElementById('isAdmin').checked,
781
+ is_active: document.getElementById('isActive').checked,
782
+ must_change_password: document.getElementById('mustChangePassword').checked
783
  };
784
 
785
  const userId = document.getElementById('userId').value;
templates/change_password.html ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <script type="text/javascript">
5
+ (function(c,l,a,r,i,t,y){
6
+ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
7
+ t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
8
+ y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
9
+ })(window, document, "clarity", "script", "ujskfvh0bu");
10
+ </script>
11
+ <meta charset="UTF-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
+ <title>๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ - SOY NV AI</title>
14
+ <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
15
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
16
+ <style>
17
+ * {
18
+ margin: 0;
19
+ padding: 0;
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ body {
24
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
25
+ background: #f0f2f5;
26
+ color: #202124;
27
+ display: flex;
28
+ justify-content: center;
29
+ align-items: center;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ .container {
34
+ width: 100%;
35
+ max-width: 400px;
36
+ padding: 20px;
37
+ }
38
+
39
+ .card {
40
+ background: white;
41
+ padding: 40px;
42
+ border-radius: 8px;
43
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.1);
44
+ }
45
+
46
+ .logo {
47
+ text-align: center;
48
+ margin-bottom: 24px;
49
+ font-size: 24px;
50
+ font-weight: 600;
51
+ color: #1a73e8;
52
+ }
53
+
54
+ .title {
55
+ text-align: center;
56
+ margin-bottom: 8px;
57
+ font-size: 20px;
58
+ font-weight: 500;
59
+ }
60
+
61
+ .subtitle {
62
+ text-align: center;
63
+ margin-bottom: 24px;
64
+ font-size: 14px;
65
+ color: #5f6368;
66
+ }
67
+
68
+ .form-group {
69
+ margin-bottom: 16px;
70
+ }
71
+
72
+ .form-group label {
73
+ display: block;
74
+ margin-bottom: 8px;
75
+ font-size: 14px;
76
+ font-weight: 500;
77
+ color: #202124;
78
+ }
79
+
80
+ .form-group input {
81
+ width: 100%;
82
+ padding: 10px 12px;
83
+ border: 1px solid #dadce0;
84
+ border-radius: 4px;
85
+ font-size: 16px;
86
+ transition: border-color 0.2s;
87
+ }
88
+
89
+ .form-group input:focus {
90
+ outline: none;
91
+ border-color: #1a73e8;
92
+ box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
93
+ }
94
+
95
+ .btn {
96
+ width: 100%;
97
+ padding: 10px 12px;
98
+ background: #1a73e8;
99
+ color: white;
100
+ border: none;
101
+ border-radius: 4px;
102
+ font-size: 16px;
103
+ font-weight: 500;
104
+ cursor: pointer;
105
+ transition: background 0.2s;
106
+ }
107
+
108
+ .btn:hover {
109
+ background: #1557b0;
110
+ }
111
+
112
+ .alert {
113
+ padding: 12px;
114
+ border-radius: 4px;
115
+ margin-bottom: 20px;
116
+ font-size: 14px;
117
+ background: #fce8e6;
118
+ color: #c5221f;
119
+ text-align: center;
120
+ }
121
+ </style>
122
+ </head>
123
+ <body>
124
+ <div class="container">
125
+ <div class="card">
126
+ <div class="logo">SOY NV AI</div>
127
+ <div class="title">๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ</div>
128
+ <div class="subtitle">๋ณด์•ˆ์„ ์œ„ํ•ด ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.</div>
129
+
130
+ {% if error %}
131
+ <div class="alert">
132
+ {{ error }}
133
+ </div>
134
+ {% endif %}
135
+
136
+ <form method="POST" action="{{ url_for('main.change_password_post') }}">
137
+ <div class="form-group">
138
+ <label for="password">์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ</label>
139
+ <input type="password" id="password" name="password" required autofocus>
140
+ </div>
141
+
142
+ <div class="form-group">
143
+ <label for="confirm_password">์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ</label>
144
+ <input type="password" id="confirm_password" name="confirm_password" required>
145
+ </div>
146
+
147
+ <button type="submit" class="btn">๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ</button>
148
+ </form>
149
+ </div>
150
+ </div>
151
+ </body>
152
+ </html>