로그인 후 타임아웃시간을 지정해서 사용자가 일정시간동안 액션이 없다면 로그아웃시키는 기능을 추가합니다
별도의 세션을 체크하는 방법을 찾지 못해서 State에 login check var를 추가하고 로그인할때, 매뉴클릭시 각 화면의 권한 체크하는 pc.cond 로 login check var를 호출하도록 구현하도록 하겠습니다.
demo_state.py
날짜 변환 로컬 함수를 먼저 정의합니다.
from datetime import date, datetime, timezone, timedelta
# logincheck에서 사용할 함수 현재 시간을 리턴
def get_ymd_hms():
# 클라우드 서버가 싱가폴 데이터 센터에 있기때문에 UTC=8로 한국시간과 맞춘다.
servertime = timezone(timedelta(hours=8))
now = datetime.now(servertime)
now_ymd = now.strftime('%Y%m%d')
now_ymd_hms = now.strftime('%Y-%m-%d %H:%M:%S')
#print("now_ymd_hms : {}".format(now_ymd_hms))
return now_ymd_hms
# 문자열의 시간을 시간타입으로 변환
def get_strptime(ymd_hms):
datetime_format = '%Y-%m-%d %H:%M:%S'
datetime_ymd_hms = datetime.strptime(ymd_hms, datetime_format)
return datetime_ymd_hms
logincheck var 추가
class State(pc.State):
"""The base state for the app."""
username: str
logged_in: bool = False # 로그인 상태 True, False
authrole: str
logged_ymd_hms:str #로그인 및 마지막 액션 시간을 저장
@pc.var
def logincheck(self) -> bool:
"""Logged in check a user."""
# 현재 시간을 '%Y-%m-%d %H:%M:%S' 포켓으로 가져오기
now_ymd_hms = get_ymd_hms()
# 현재 로그인 상태이면
if self.logged_in:
# 로그인 상태이고, 로그인 시간이 기록되어 있으면
if len(self.logged_ymd_hms) > 0:
# 이전 로그인 또는 액션의 시간을 조회
before_time = get_strptime(self.logged_ymd_hms)
now_time = get_strptime(now_ymd_hms)
# 현재 시간과 이전 시간의 차이를 계산
cul_time = now_time - before_time
# 차이가 지정된 시간보다 크다면 로그인 상태 False로
if cul_time.seconds > 3600: #600 : 10 Minutes
self.logged_in = False
self.logged_ymd_hms = ""
return self.logged_in
# 지정된 시간안에 있으면, 현재 시간을 logged_ymd_hms에 저장
self.logged_ymd_hms = now_ymd_hms
return self.logged_in
demo_servers.py
demo_servers.py 의 servers 함수에서 pc.cond에서 로그인 상태 체크를 logged_in 에서 logincheck var 로 변경한다.
import pynecone as pc
from .demo_state import State # Substate of Base State
from .demo_helpers import navbar
class ServerState(State):
"""The Substate of base state for the server page."""
print("──────────────────── [ ServerState(State) ] ────────────────────")
app_name: str = "Servers" #1 Navigation Bar에 표시될 Page 명
def servers():
return pc.box(
pc.vstack(
navbar(State, ServerState.app_name), #2 맨위 상단에 navbar를 추가
pc.cond(
State.logged_in,
pc.box(
pc.vstack(
pc.heading("서버 모니터링 페이지", font_size="2em"),
),
width="100%",
border_width="0px",
),
pc.link(
pc.button("먼저 로그인 하세요"),
href="/login",
color="rgb(107,99,246)",
button=True,
)
),
padding_top="5em",
width="100%",
),
)
demo_server.py 화면을 구성하기위한 데이터는 따로 python 서비스 를 구현해서 데이터를 List[Dict] Type으로 리턴 받는다는 가정하에 샘플데이터를 만들어서 구현하겠습니다.
추가되는 함수 설명
defget_data() # Table, DataTable을 위한 Sample List[Dict] 데이터 리턴
logout 이 호출되면 State에 저장된값을 모두 reset 시키고 "/login" 페이지로 redirect.
import pynecone as pc
class User(pc.Model, table=True):
"""A table of Users."""
username: str
password: str
userrole: str
class State(pc.State):
"""The base state for the app."""
print("──────────────────── [ State(pc.State) ] ────────────────────")
username: str
userrole: str
logged_in: bool = False
def logout(self):
"""Log out a user."""
self.reset()
return pc.redirect("/login")
import pynecone as pc
#table=True 인수는 Pynecone에게 이 클래스에 대해 데이터베이스에 테이블을 생성하도록 지시
class User(pc.Model, table=True):
"""A table of Users."""
username: str # 사용자명
password: str # 패스워드
userrole: str # 사용자 권한
class State(pc.State):
"""The base state for the app."""
print("──────────────────── [ State(pc.State) ] ────────────────────")
username: str
userrole: str
logged_in: bool = False # 로그인/로그아웃시 상태를 저장
demo_auth.py 수정
Log in / Sign up 을위해서 추가 변경되는 부분이 많습니다.
import pynecone as pc
from .demo_state import State , User # Base State, User Class import
from .demo_helpers import navbar
# 로그인 호면에 대한 스타일 시트 적용
styles = {
"login_page": {
"padding_top": "10em",
"text_align": "top",
"position": "relative",
"background_image": "bg.svg",
"background_size": "100% auto",
"width": "100%",
"height": "100vh",
},
"login_input": {
"shadow": "lg",
"padding": "1em",
"border_radius": "lg",
"background": "white",
},
}
class AuthState(State):
"""The Substate of base state for the login page."""
print("──────────────────── [ AuthState(State) ] ────────────────────")
app_name: str = "Log In"
password: str # 로그인에서 입력받은 Password
new_username: str # 신규 사용자 Name
new_userrole: str # 신규 사용자 Role
new_password: str # 신규 사용자 Password
confirm_password: str # 신규 사용자 Confirm Password
message: str # 사용자 등록시 발생하는 Error 등 메시지 표시
message_color: str = "red" # Message의 기본색은 red, Sign up 성공인경우 navy
def signup(self):
"""Sign up a user."""
print("signup values : [{}], [{}], [{}]".format(self.new_username, self.new_password, self.new_userrole))
with pc.session() as session:
#self.message_color = "red"
# 입력 값 Validation 체크
if self.new_userrole == "" or self.new_userrole == None:
self.message = "Select user role."
return
if self.new_username == "" or self.new_username == None:
self.message = "Input user name."
return
if self.new_password == "" or self.new_password == None:
self.message = "Input passwd"
return
if self.new_password != self.confirm_password:
self.message = "Passwords do not match."
return
if session.exec(User.select.where(User.username == self.new_username)).first():
self.message = "Username already exists."
return
user = User(username=self.new_username, password=self.new_password, userrole=self.new_userrole)
session.add(user)
session.commit()
self.logged_in = False
self.message_color = "navy"
self.message = "User siginup completed, please log in"
return pc.redirect("/login")
def login(self):
"""Log in a user."""
if self.username == "" or self.password=="":
self.message = "Input username and password."
return
with pc.session() as session:
user = session.exec(
User.select.where(User.username == self.username)
).first()
if user and user.password == self.password:
self.logged_in = True
self.userrole = user.userrole
return pc.redirect("/servers")
else:
self.message = "Invalid username or password."
return
# Signup 화면에서 선택한 Role 정보를 AuthState에 저장
def role_change(self,value):
print("role_change value : {}".format(value))
self.new_userrole = value
def clear_message(self):
self.message = ""
self.message_color = "red"
def signup():
return pc.box(
pc.vstack(
navbar(State, AuthState.app_name),
pc.center(
pc.vstack(
pc.heading("Sign Up", font_size="1.5em"),
pc.select(
["User", "Admin"],
placeholder="Select a Role.",
on_change=lambda value: AuthState.role_change(value),
border_style="solid",
border_width="1px",
border_color="#43464B",
),
pc.input(
on_blur=AuthState.set_new_username, placeholder="Username", width="100%",
on_click=AuthState.clear_message,
),
pc.input(
on_blur=AuthState.set_new_password,
placeholder="Password",
type_="password",
width="100%",
on_click=AuthState.clear_message,
),
pc.input(
on_blur=AuthState.set_confirm_password,
placeholder="Confirm Password",
type_="password",
width="100%",
on_click=AuthState.clear_message,
),
pc.button("Sign Up", on_click=AuthState.signup, width="100%"),
pc.text(AuthState.message, color=AuthState.message_color),
),
style=styles["login_input"],
),
),
style=styles["login_page"],
)
def login():
return pc.box(
pc.vstack(
navbar(State, AuthState.app_name),
pc.center(
pc.vstack(
pc.heading("Log In", font_size="1.5em"),
pc.input(
on_blur=State.set_username, placeholder="Username", width="100%",
on_click=AuthState.clear_message,
),
pc.input(
on_change=AuthState.set_password,
placeholder="Password",
type_="password",
width="100%",
on_click=AuthState.clear_message,
),
# Username, Password를 입력받으면 AuthState의 login 함수 호출하는 event 작동
pc.button("Login", on_click=AuthState.login, width="100%"),
# 사용자 등록하기위한 Signup
pc.link(
pc.button("Sign Up", width="100%"), href="/signup", width="100%"
),
pc.text(AuthState.message, color=AuthState.message_color),
),
style=styles["login_input"], # 로그인 Page에 맞는 추가 스타일
),
),
style=styles["login_page"],
)
import pynecone as pc
from .demo_state import State # Substate of Base State
from .demo_helpers import navbar
class ServerState(State):
"""The Substate of base state for the server page."""
print("──────────────────── [ ServerState(State) ] ────────────────────")
app_name: str = "Servers" #1 Navigation Bar에 표시될 Page 명
def servers():
return pc.box(
pc.vstack(
navbar(State, ServerState.app_name), #2 맨위 상단에 navbar를 추가
pc.cond(
State.logged_in,
pc.box(
pc.vstack(
pc.heading("서버 모니터링 페이지", font_size="2em"),
),
width="100%",
border_width="0px",
),
pc.link(
pc.button("먼저 로그인 하세요"),
href="/login",
color="rgb(107,99,246)",
button=True,
)
),
padding_top="5em",
width="100%",
),
)
로그인 없이 접근할경우
demo_board.py
"""Welcome to Pynecone! This file outlines the steps to create a basic app."""
from pcconfig import config
import pynecone as pc
from .demo_state import State
from .demo_auth import login, signup # Page 추가
from .demo_servers import servers
def index():
return pc.center(
pc.vstack(
pc.heading("Pynecone Demo Board!", font_size="2em"),
pc.link(
"로그인",
href="/login",
border="0.1em solid",
padding="0.5em",
border_radius="0.5em",
_hover={
"color": "rgb(107,99,246)",
},
),
spacing="1.5em",
font_size="2em",
),
padding_top="10%",
)
# Add state and page to the app.
app = pc.App(state=State)
app.add_page(index)
app.add_page(login)
app.add_page(signup) # Page 추가
app.add_page(servers) # Page 추가
app.compile()
서버 재시작 (ctrl + c & pc run)
서버 재구동후 DB Browser for SQLite 로 pynecone.db 파일을 열어보면 user 테이블이 생성된것을 확인
import pynecone as pc
class State(pc.State):
"""The base state for the app."""
print("──────────────────── [ State(pc.State) ] ────────────────────")
#4 demo_auth.py 생성
import pynecone as pc
from .demo_state import State # Substate of Base State
class AuthState(State):
"""The Substate of base state for the login page."""
print("──────────────────── [ AuthState(State) ] ────────────────────")
def login():
return pc.box(
pc.vstack(
pc.heading("로그인 페이지", font_size="2em"),
padding_top="5em",
width="100%",
),
)
#2 demo_board.py 수정
- State(pc.State) 클래스 제거,
- 앞에서 생성한 demo_state.py의 Base State import
- link를 추가한 demo_auth.py의 /login Page로 변경
"""Welcome to Pynecone! This file outlines the steps to create a basic app."""
from pcconfig import config
import pynecone as pc
from .demo_state import State #1 새로 생성한 demo_state.py page의 State import
from .demo_auth import login #2 새로 생성한 login page를 import
def index():
return pc.center(
pc.vstack(
pc.heading("Pynecone Demo Board!", font_size="2em"),
pc.link(
"로그인", #3 화면 내용 수정
href="/login",
border="0.1em solid",
padding="0.5em",
border_radius="0.5em",
_hover={
"color": "rgb(107,99,246)",
},
),
spacing="1.5em",
font_size="2em",
),
padding_top="10%",
)
# Add state and page to the app.
app = pc.App(state=State)
app.add_page(index)
app.add_page(login) #4 로그인 페이지를 App에 등록
app.compile()
실행 - pc run
#5 demo_helpers.py 추가
로그인 Page 및 추가될 Page들에서 공통으로 사용할 Navigation Bar를 화면 상단에 fix 크기로 생성합니다.
import pynecone as pc
from .demo_state import State # Base State의 Vars들을 사용하기위해서 import
def navbar(State, app_name): #1 Nav.Bar 상단에 각 Page의 이름을 표시하기위해서 파라미터로 받음.
"""The navbar."""
return pc.box(
pc.hstack(
pc.hstack(
pc.image(src="bada.png", width="48px"), #2 회사 로고가있어서 좌측상단에 표시
pc.heading("Demo Board") #3 로고 옆에 프로젝트 이름
pc.heading(" - "+app_name, size="lg", color="navy"), #4 Page 이름 표시
),
pc.spacer(),
justify="space-between",
border_bottom="0.15em solid #d3d3d3",
padding_x="2em",
padding_y="1em",
bg="rgba(255,255,255, 1)",
),
position="fixed", #5 Box의 위치 및 크기를 지정후 고정한다.
width="100%",
top="0px",
z_index="500",
)
생성한 Navigation Bar를 login Page에 붙여 넣기
- login page 의 이름을 지정하고
- navbar를 추가할때 page 이름을 파라미터로 전달
import pynecone as pc
from .demo_state import State # Substate of Base State
class AuthState(State):
"""The Substate of base state for the login page."""
print("──────────────────── [ AuthState(State) ] ────────────────────")
app_name: str = "Log In" #1 Navigation Bar에 표시될 Page 명
def login():
return pc.box(
pc.vstack(
navbar(State, AuthState.app_name), #2 맨위 상단에 navbar를 추가
pc.heading("로그인 페이지", font_size="2em"),
padding_top="5em",
width="100%",
),
)
% pc init
[11:05:35] Initializing the web directory. utils.py:410
Initializing the app directory. utils.py:399
Finished Initializing: demo_board pc.py:49
% pc run
───────────────────── Starting Pynecone App ──────────────────────
────────────────── Installing frontend packages ──────────────────
bun install v0.5.0 (2db04ef9)
────────────────────────── App Running ───────────────────────────
$ next dev
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 2.4s (757 modules)
wait - compiling /404 (client and server)...
event - compiled client and server successfully in 191 ms (761 modules)
warn - Fast Refresh had to perform a full reload.
Read more: https://nextjs.org/docs/basic-features/fast-refresh#how-it-works
wait - compiling / (client and server)...
event - compiled client and server successfully in 375 ms (831 modules)
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9e1be00f07e1 nikolaik/python-nodejs:1.0 "bash" 5 weeks ago Up 5 weeks 0.0.0.0:3000->3000/tcp, 0.0.0.0:8000->8000/tcp, 5006/tcp demo_board
$
vim 설치
apt-get update
apt-get install vim
리눅스 버전확인
root@9e1be00f07e1:/pynecone# cd /pynecone
root@9e1be00f07e1:/pynecone# uname -a
Linux 11d0133a3eb6 3.10.0-1160.42.2.el7.x86_64 #1 SMP Tue Sep 7 14:49:57 UTC 2021 x86_64 GNU/Linux
root@9e1be00f07e1:/pynecone# cat /etc/issue
Debian GNU/Linux 11 \n \l
root@9e1be00f07e1:/pynecone#