From b7a8a86e531c229130a8afbc79854ddd6d17206a Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Sun, 16 Nov 2025 18:00:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0API=E9=89=B4=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 6 +- __pycache__/admin_routes.cpython-312.pyc | Bin 28619 -> 30677 bytes __pycache__/auth.cpython-312.pyc | Bin 0 -> 4810 bytes __pycache__/config.cpython-312.pyc | Bin 1565 -> 1673 bytes __pycache__/database.cpython-312.pyc | Bin 3935 -> 3935 bytes __pycache__/main.cpython-312.pyc | Bin 4483 -> 5073 bytes __pycache__/models.cpython-312.pyc | Bin 328 -> 328 bytes __pycache__/mqtt_manager.cpython-312.pyc | Bin 10828 -> 10828 bytes __pycache__/schemas.cpython-312.pyc | Bin 6949 -> 6949 bytes admin_routes.py | 63 ++++++++++++ api/__init__.py | 8 +- api/__pycache__/__init__.cpython-312.pyc | Bin 550 -> 616 bytes api/__pycache__/contents.cpython-312.pyc | Bin 13120 -> 13366 bytes api/__pycache__/devices.cpython-312.pyc | Bin 8111 -> 7833 bytes api/__pycache__/todos.cpython-312.pyc | Bin 7407 -> 7658 bytes api/contents.py | 14 +-- api/devices.py | 43 ++++----- api/todos.py | 18 ++-- auth.py | 118 +++++++++++++++++++++++ config.py | 4 + main.py | 30 +++++- templates/admin/base.html | 17 +++- templates/admin/login.html | 74 ++++++++++++++ 23 files changed, 343 insertions(+), 52 deletions(-) create mode 100644 __pycache__/auth.cpython-312.pyc create mode 100644 auth.py create mode 100644 templates/admin/login.html diff --git a/.env b/.env index fdaeb34..c64ef1e 100644 --- a/.env +++ b/.env @@ -25,4 +25,8 @@ INK_HEIGHT=300 # 安全配置 SECRET_KEY=123tangledup-ai ALGORITHM=HS256 -ACCESS_TOKEN_EXPIRE_MINUTES=30 \ No newline at end of file +ACCESS_TOKEN_EXPIRE_MINUTES=30 + +# 管理员配置 +ADMIN_USERNAME=admin +ADMIN_PASSWORD=123456 \ No newline at end of file diff --git a/__pycache__/admin_routes.cpython-312.pyc b/__pycache__/admin_routes.cpython-312.pyc index 703d6796a742ee5aa0d9496cb614a46559ec4d5b..aeaab725ca8a40e79a67038891219b88219fe7a9 100644 GIT binary patch delta 6814 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 O5aaHBm*YN$K>r7&=ri2_ delta 5007 zcmb7HTTC3+8Q$5;a9!5;!o}Pc%x$?XHej$#DBu{&#ldlbDy(WY19N~)cXpODLu^Rh z?ZvSasjcIjheWAsxlJ!|?4(V%Nz^*2ob)lRT1V>3NKvXjwDMCQD)k``Rsa8--JMy2 zfgq zk?x2OSEQY^KQql9RP8$!AjNn0+;NKq*dvpwEYFgpmJ_$Smjsr)>b@(!9Ct^Kd;c!E z4>-6xv)qqW?8se7y10H>t#U}{&PoVWJ0*~I_BZc=u{_!~9v!qL<~$2$C7-HsN>=4f za@;&Jm86_>RYT}9HrCn_BfVKkV|7kRo8*WHqnGvMFqcxOHaMgl$w~dpHKv*Ci8^M@TY=LC>>oujTJP@{eDkG0yLtNQ_RoLB80^{B&& z6JW*Da>dXigo>iK9T;-34zJ6hW9&Z#@38j@U-DQ|n4K@GP5&L$qXE zG09hmk<=3g3HfOwEYK!|c7zZ@2SO)87eY5e4}i6ol0}j*qo%IvMs#(J&~@61t8EBj z0K*RubpCxl9G*4UC&kAr0*b1|5>Zt#Ru*)Hs`Mo6tx9)P)OhB4QNUq@A%rg>97h;N zKMPz6cw-+%D_$Eu==NJB zhN+lqMs!hE2^*{JNTKys;i8@}!8rqhVq4P(OBC5H-%^Z%&>|!ZAfTaE!Lx)KF+H)R zu|8oevWpP&FipXy;RE1N0>djoj$c?FRf(y@;`BT$Po|~1dmd?oeGu$USw2FkP9Fwg zA>hqPHK`~!Om}KYL8iN=bUQ1TtQY>x~O4N|sD#&Dpwy_u64yG{ebOj(3 zc%;Fb(nOy`o-OK$7n&t2$s|drp~6Szi*lvUpp@Pipiv0G^9LYm&!4RyQG9XO@I3DP zK6~@*+4RraS0(AmbY(}%o#*wByG}gLs{*B3{ao2Pf>~5O1 z+oZtZ3E#l8mQzG^w5$Lic5B-w;c z(GgbCKb68f-p=$V57$?LK;1}{pvcw|ix2TW6)F&g!_B&gTP`82BWxh_1AHFHujDYD z!=(s7sFHpS-+K}G#Na*Jk87Kz1?6Qrj|;q3c_|kgt8pd1xI(mbdKs21kEUD_9gQYg zvazVd6-pIkTR^B~M+RDY>@MccanW0u&x3rKJwMP}#Y?{gOFvt|S3%w%UFkm!%#}b$ z+s5kaP5}iXh4aBZ;=9FCe#w3ch_8;adn8O8RYt?;t4b zv+-cC3197S+2(tpy$SSl!^y4@tbDP))7y)=@3N!o2%=fiqTvybnh|w#s&6j@R zWKUsUOMNui!Y4YP5wrz1O~Ax#MA=1naEbopR`DsaQD)QE3%8YnXx`^0ePK0s0Rte9GyuEA2K zesa}l6{>3l&RH0se7HWx-klyz<(5UY-2_HUh64n)g}jzKriw!1-H3}DrI{Y&rtRE- zz_MLkd13h+Tc2rY=K*!J7aCG-zyRj=3g{J0QxYn@#qQ749N{&ysg81!TKA}`Co~>tz0|MW4R_a)jSfWag6iamWoRPPq3|) z7l5rNzn(4sMFlU;@H2wI4S>E3G7S`#ozwUfv$`mdi#)v#J4(|(S@HXtHboX~v}6@- z6fmrA!3TYhJ; z_){W`6!YW{uy{KCVS-4~W2_^2Z8ix6%dHzUiW+$83ZZe81;rqAA1=xo{M?8ebU$w3 z`96Se{CuszHyk-ufq^#@#h{h&#VS_oQ&y??*+k)D2)_a5aqCHh(+DpioCmPHNgZb3 z5`N>|lfH;xAY4Vj!;;=Wz*$If_E2nI-Vl~|LD%CvO%z?b?fN&M3mvb3F**#Uuf@p- c{RD_Guw2G|_UUSf715*VD&x0a?*pLy58#O~vH$=8 diff --git a/__pycache__/auth.cpython-312.pyc b/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e23d6cc6870a3a1a68eb199e0aae122911285291 GIT binary patch literal 4810 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 D?EaNg literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc index ec3cb40c5f23ebd1cb791e16087bee1f872b3c49..2e2a64f6d9f5cdf95173bb03de5d0e1df7b154d9 100644 GIT binary patch delta 351 zcmbQs)5*(wnwOW00SMO4mCXDhvXWg>W%EkLwT$|=SQArnGxKiofvNb?;?$zN#N5N*!TBWHa^HWN5QtgWLfkrWcj03si12ZEd<6Q==yA0A_SQHth WI%>W!0I82|j0`*?-Byhd#{&Rk?NaXm delta 242 zcmeC=oy)^}nwOW00SL+_OJ>fW$UBXZVd9dv5=;#43@LIg3@P%fnLx4(3{m2h?3xOj zk20=hoV<{EkB&4@OaX|C*?~k0!wq4{PL3i)AiGEzNN7qF$$;3hAVO|(Crha^NVrG= zM1WN(fmm!Xf(6JZ-ac8Ibpy|L7DiU7PYgh!NOAICRtIkdkRrIATO2mI`6;D2sdh!$ sKnX^WMv#gR%#4hTcNw(qGDv@6k!6(XsQJPGq(0g)@Q8F=B zZ@=e%J=Jojz2i5p*M;C2zOr6i_9FCvDb)vb^La9E{jg)-r0hz#ID`=vBzMAXo}PpU zPP^oleF>lGb4WF^KjAlJr&KG~CTdODB?aWVM4c(SrFC*J5j16wR4+Fq8stzS#32hJ z?vb8q`PCDwJCJ^r4@`RVzMQ4(sK9rv!m1K!%yDHo(L`LhhHyg`>_5%tDu6^73URm= z2XGw;<8?TQ>v01P;q`dKWnSb&4hPdnR4RH<@pc^5LAV z9KsZUg&I~>qtwlO_LP#zkd$Uv6xDFZ$x(tus#2#u zXAg4$eZl^wZ4=-cZge^NIj&j%)fJwMKm*x5U|srn=HcxNOS98Um(CWi&MaQPS^V+h z^6XskoA(|}y<0qcrZ{`6_|f^|+?nDBUp>5i?$PwEr7uoH^gp)lcvrm3s8LBOOGRzs zXlj_qL^V9+*$$Ed2FvTV0OiGcj*{hNzlo_hSt`Q>Xr z6)(>`{Q2DCr?-~p^x~Os7k~JAX?nI?ltH7s!D|y^#IT4NgU=*oVz@C;Q&cqhi3)~I z6E%s@PLrJDgfiwSqreC#VtwMgz*K6X<_C5BQ>-;Q|9C+Q2B8(D%9` zJ82YRPx9L#5WK<|B}3x)Nnv}EU0K!x&I*5*E)*|bk6P$%FzIJq!`x>5u`3+k#!B;8 zNvVd75;(s@bRXEBzz@*~ybPWyibh`rk?6hdT#tFN&dS9SY{Ka);4Z^Gz^0@P4*=z< zQOdqPty#u1Vq`bufb+Ql`aBnr>v80PZ0%O`S|};G~uu zOsa&k)h+L+6}Y2}DZ^v3Hb9lEMpO!a_&%6E342d{3|*sdso6GZj=^q>23U+QiSfW{ zZ*&JU+GHi0(P)zi5Hl$$i%B^xd>m;~X4B-xaIJ}D(;N2eaWSRPjA5l&_@Xcw%%<0H z9E&iJ<7HXFS&8hU5~%F|U407gl9l7Q`>1sRZCyY;3#eEBw;Mq_JMGE!dLa3$CeDqJJt;tui+8bRZt1~2&)**lRb2{(aJnwD!tJ!b2 zOj%29`jkJ`dk+Qfpul`x%U#r35)j8%4Q+Q(`!j~t>!Ewy`|ot`pGSeasJ|rW-}^fq QSB6YArr-0=@N8}V1C`GOi2wiq delta 1306 zcmY*ZPi)&%9JU=haU7?KlZ7VBmZe=o6Sj3*DF>iznzTPm8l`ACP+uZ9?=^L5$Klx- zD+fTL0*Pts(7PZ6nl??MC=uIKPOU)V!iCci5|yQ?2S~$iLqj|4#CuNC6ia!(@4fH) z{eJI#&(6c~PY&mQ4TmL$JXdf^pA0d~pLEiH0zL5bH|~r4sd+UB7$6vuDv{_nLZ+2%AIdkcUMhVn)=6hl57cR8+;ok`XiGYMf;_1_rD7ev3gQt1sby_;@_jlABz2 zy{A5k*VqGDH3>x!hU@|dhF17yFH=pCf-ID)Q#AQP1Vmw~r8N02t)^kR`s$!bA3;Be zRag-BMuJ-VnnIUj8Itijtu>b*m3dNmx}PT9CJ#LM` zVQ_ef*pG7FW#-fzIc2VTiDn)f13B;_Iq^(?=Y!Z9?|aHJ%fhly$m6@dH}Er`y1vXm z@v%W%71Pu7U2+3ut5$<$$K@>B6-{jk0v*{%q?}|snxmIXWW|+$Zug`G4n^?K;*5|X zqQv^X;5qiA%LKM>ZTz)yBYR%6oino+vq!RTy;Cfb@Mf_%dz=oJ$z-NDo9%qH^7oIM zk3U=OY<%0kv$k{phxUWjoiBgt+}-G`Z+13s?RH_*`Tp=~b5v zt5QPny#}-$8=WN@4SyK9$L8@w^wOL}+Bop4W9ajUe&}w1$SZoSf|7LRC-j0|>E40xilebNuR@V{v$6#F9Go^&Ah68ORm}+QiqPqh20+Qw z0WjeE8iKB1T8)~6_T!S08m3No=W?Zi1f9jJN@|K)x5o={+O$B!fTvK25cFfTual9y zi7dO#j6GtGJYpvCwvxopmDyD64=MdnN^eEsmK@&-C;klz0q$eplPJCxJBFXd#(lpt M8Jvr+@zm}A0PAK;6aWAK diff --git a/__pycache__/models.cpython-312.pyc b/__pycache__/models.cpython-312.pyc index 3ed9b869791dd4d450973eb1b76e017f966ce4a8..12cfa21643911e52a5e95379248a2664565cc2ca 100644 GIT binary patch delta 19 ZcmX@Xbb^WNG%qg~0}!m8yOGO|5dbt;1kwNi delta 19 ZcmX@Xbb^WNG%qg~0}#~A*vMta2mmt~1e^c> diff --git a/__pycache__/mqtt_manager.cpython-312.pyc b/__pycache__/mqtt_manager.cpython-312.pyc index 0da7ebbef179ed87b7c4a4c67cc5e174182ced2c..6257174bdb3659e233a14b9328b68287d6e1a387 100644 GIT binary patch delta 19 ZcmX>Tawdf9G%qg~0}!m8yOGOD3jja;1*-r6 delta 19 ZcmX>Tawdf9G%qg~0}x!Bv60J33jjdc1LvhUHi;SAq8>B8UNM2#Exxv77ok8* diff --git a/api/__pycache__/contents.cpython-312.pyc b/api/__pycache__/contents.cpython-312.pyc index cc16a335563477e9226fe1e73b87560a0b5aad4b..29425778e788a1e8811425a28bc83b73882d5340 100644 GIT binary patch delta 2949 zcmbVNOKcNI7~WYwvUY6e;XLfvNgNWA*fEfVK+7wThBqVu5hEmg-TUjm7Wk)Z|%iVR6$p&D2E<6HPEV3Pwo7(abg}-p_cOP{Qu1V z{r~)r$-SwsXM8_+y>5a1e!Ue;jYV(z0_efbuCBZ;$r2J&K@~NZ?v~wrZPO}skL=-V zyXMt>vQMv)tGMjYs^w~S-l5gVHSFuuYIVQt=X(`eon9|D=#6rt9*_fiP!4jvq%|>p zNDs@+Ty|+KdW+n`);k2%t+nc{ax34c)FOJD+{V`)ZJpjOwJf|OJ9!6O2q9Y3uaxtV7K=9j=!`sh143{rPks`)sNj%PPOhm z$HEGc+|B%qgP-Xt?86N~rhkFAe!*rPXUS+#9nZ1bIeR0HsR8yE#8tQlShz2@+EnH% zUh*{(!oh#{@*KYveVB1u(KnYdZeWaOAo}AW-0(jbTX2B0N@{Bv>n6sk0IL#WnV(!q zRoUkSPmxlF{v-CbGB5q5Y;0SHgSXokM9aZT8Q|u2wWG{>GqdLXkzZe`GqZH4ood(Z z?lLZi>QJN4Xq;+inT3eg`oyYam{l;wD;UESE7|EO^E@!`h{hj7uu;i$4bd-bLvB+V z$H^Q?W#{P=n~th!!oIydipqpR%Da9gza4g-%Hq89tDryFJXUY15TC(=%+bjbv1;kxtM% zjijO}gmf{1bOS^IVgNk=ak|IVgzD+ru25wQC|Us`^t$UR+V!P-5J@5lfvUMMRQZXR zUkB@U2GMG=0oEGK8uC5vk}*nyTClL%Y&k!>Jz&rlFUVh~yRk)2F1JzC1hCRP$l4is+q8WHL&+^YK2 z=Cq=xNBryake;=)n68wrOyfk3XywTPK6jE}C5zRBl8pvl@6Ox@B-|7;hH0NwvNL7{ z!5UlGW=M=a^{>pZlz?LEL^Yq z&351r5J3$mSlfB=TvE%a?CNP&glLU*Lte7As&fC&mzLfMM0&RZ*Bby(U{Vk8CWB`p zKLy$dKr5{X#v6E@e7RYb<#n}E33@2_lyzGe&NUYXn{H5_>3Xe@lEB8vC`6dr~DT5X_TBJKIZ>;O5!AR2nT zuHcCp25TPBURd#|9EH^wz%c+mq{m@(0$?0qty>a&nyndz9o{Y#)1n7X$ zs9*s1Wh0N%8tB2E*jv2u)`)p~tu_^bw~q7!3;=8a;LTT-I{H=5 z*7~*hFr0}1Y@>ho#H;!2YzLWVJQ_yBkfAwpAP9j{C(1}0S?Rv190 z7^E5`@5=K%&XuGFOltxBP)gIO!?SK!uW23VHv;hFH1Gp#%%@V*q!9$Wl4FlTfY$bQ zfSr?kXEv852@n+!{Rbx|jwSkg2NI)WiQ%!)iG8CJ z1V)0W0GAkiCj8D&uv$pk$)d|?c#wR^bcp>Kk3~UPwjqRE4~290g>y^7IeNFZW8`Nc z{K!@Nz+Lx#_z#=t7Oy#%B|(&Kgsz8*-kxtpil@)qKb>AYoqi$+d9+_#7K8)h36VL= zqj8X~q6zU@h{@tfkyiJ`;WOA5tXVvZi&Gbu&XVF8<66@a%F=iGMmlq7Sqz}M=&uhABWFJ{h!-EU`hA~`Qml0 delta 2865 zcmb7GOKcle6!r5j{>0ADP8@$yr%jqn;x?a_0x2z|Re=KOH%-Jq3-QSWS17ugK|*FZmm%d z$ss)~hsD~XHOWnI-lH|k&G7BjTJ%=ARqQorZF)qG>g{s7-XVABF*zpmKCKh^pu1jDl98L!%YNHyOBg^3>C;IGg$lbL2^Z`x=BFAWy zx=%l9tLt&OdC6t3fxt;ymx#TZC29-pQCn%d+D84eUN!QnXKBq)?giQjq@AK68tnx2 za&-Mp2(VrC+(zaWWF53mjlrjrhG{=q3F?3vucMaMsN41=XspJ18=vi561Z&VQgsXi zz%UKYwT&le^ez~>Xoq0&shjFpGQjc-vK&o7;+|cLyzXl`y&m!Z_|{#=IEIA3;JG0I*z&s#@M$QP=aP z)c`(%XY(wATWtm{sM&H!RZI$=>AF%-B`-^&Z3=;O@qhd;lHJwk1AB>!uz2;G;JeOB z8uz*Zq-HjVat2`=g0S6=k_W+yFoZCI@GyYoD>9X`?94ek3b#s)oaHUf&KlHYd+~%z z(KJj1n(RS9tuSU?AdRs5aV>7{0LkzHq}Luc1`DfE%Nr(yB&x#>R9#ISg!EJs%^$eD zX*j^T`Q^6W3Z}7^KFgoil{uO%Gwt>y26Bu-lGLnXO0j#3S=@nzv7AN2a+efy9?PJC z;F@Rsyb#%3X}!gdTJhMEu(6&AL4@(!;fn%2fxJFo%ObDv`VQF$&e2*jj)RE|AL9)4 z+YQG*3y#%lQ}nv4@)%NMRVO84h3MIwR#qVy(8H$GV#h3sSgW(}=nloU0;1`&YFRH? zp1etQ!)gE;#WWe)iPzkRfZZS(k?jI0c1xU-xF>>5@mD%CWQ>2>86hY6_nm)2A112b z#dT8i_>dI5HwWRp)2K;>Gm_ixv|4^KwT2r2d&+L8ux+r>Qm0^-FD1Wp=A1m;9H~}P zzi)KbeD{{h_bVs)=!}{fp7>xyyb^cI)ZQ%q1Hx^igB

mO;KZ&iAeNf| zR^I#ek2e-q&cC+u$=B&^=}E~`mr>!o7*Qg$$9ZaS=pgp32(XNUXwClOtAD(8e&yn4 zH$H!VEG>24Ro?hJgJUGhuM7_C6U`}-Cax5HhU}ud7*0|)j4+C@13`3QU0(S^nO#va zbp%Zg&)C`Gi;UfIJh5D*vdK>3GRyxR?tJ7FiZci%z=w{%03CK-d=J6(WsUA- zuY(R|ki&Qre*RWngpgkyQ$IVVt~#dp)JV_v-~6q=1Y3RyL|#w+?QsR13*Oa$!|7X0 zyp*^S8vJVF%Jj*frwdo73k!*Bq{uIiWYQ(F>g*@&tCR5W(6<W*ugl)_K diff --git a/api/__pycache__/devices.cpython-312.pyc b/api/__pycache__/devices.cpython-312.pyc index 32f8433de35497079b74c5cd8d431a44732a449b..f6f19fdce9aced26365ca271ad649fe27dae0c70 100644 GIT binary patch delta 2853 zcmb7GTWlOx8J;sUd%s`o?9J=-E!Ng{E;Lw_I!RMEaZ)MkBz9stO|x3>oFr?{>~7EO zHa2xx2YN{*XlRdUT7)1E6$)}lTN$ZEDJXfO2t?OfAtx&;;K3q!DVxg!hzB_Tj2$~M zL}j%5&pH43{_nq>{~XQxJKx>z{+-KZ=h(CGlW^{f;G6DH?WJ0jUD`wRqS@f}>SXG;3Q|*#npNon^bx00w81x}-Pni4i2*~qRx%qNM_2I7)-xv>>-vg&W#u@Mz1T1Nu;sWY`(HNA zEt5#CI5x41zS$HgxUmiU!c4!8Gcd;r2oFm;D4S%{1KcLL3Af43?7soqaXWCxAvWsJ zN8r;ghu8S*T=BUtf_?wn+XG&Z9L4R+8N}AGbuqU6kiq`{pREN4bVjorU&GkV7!LyD zvk_MHLwf4>iFGlzu2kqDF(=k|c*NM(eQh|nj_Y2vAHvnXhO1|#nyHR;T%B{g5%@#6 zy4G;@PMeY&Z-PnEM0W{2_PTWhXUim4nx=|SL@xTd5bsK&y5j^cr8C7`dJ<39Ei!%~ zm&F>>i>1s7t&TP)1==mfT}~MnabCvxYz}MlPl@Mv6rz7JA4Cbd$?adKTM471Dpf?^e?M?iFzAY0HZOeBnl zY+>WNQ^{$i85v_ok@KN z8577|gX6|L8E))Q>g6Krn18~h@J~JtNFW_$C3oSetWuV7x}0ZOCB6UM4Oo~!m_ZEC zFFXn4pdN21)(eV0CRiB~Fy0Bm2!;-Ndz$oQUFD*jDdCkso~GaN{@%)y5MaaehkP39 zzZX?1>HJbbfUTeY%ir6r$FQBv?@!$&V+F4|P0>`~GXD;upE-U@pBI~vm;T++fv`GgWQi~h`TGrSH8RD_Gd%9(vhoDoO3p9`l@QY=Fc z*kPA%T>ACWd#^2h@`I)KKS}f@ZoKi)(%H8S<;_>V_qShNT6*idORv7Nbm^BmoZQPk zDHvshhO1k%g?tI;OWFe^sj$=&3_!X;tl1baat<0s`wJ|8qanWrFi13MGVF}~0qp@W zu@bBm3#0#S(-l;+ZO zF_%9xjC@T0_zAEwV#jE|yE=X)xap z)4<-*(F5tPY}uAh4W|c(Q=_|6qa+2(&_R0cKjZGOA!poE)&UkwS;0HVB-6ncjCPFe zYC}K>ng7fU{gE4@7h2+DpL4Ol+Wgn-fiuy&0&nMM%?&HZTVIQwj-DAgAN#m}@$o|o z>^GjPPLyloFH|$9W}`JUb$wv)%AU)^x40N;I`3@2>x1sw@VX;#{518&L!PVMpZ3(A zJW|~?HXEs-Z_rKgLmkhc1|L9y#v|Y)ayoJ*bz$J@sZU?54Lw`ka};pT(RbqUj$;gG zNB&0WA-F%pdwT#EVz_O9YdPI==35u~7khRuu;1aM)#pyu4(F<)6SFNfG)Y6P@ruIm z9^`GbFDF@bbyfR^7RHWM(-XC^$?6dW(o?TL_QaLu%dNM#W)wW1ZNTfosoU^^^zw{^ z4I5jQgR45*KHTtew6=A3b;};eVd(nTcU{?ddGjqUfXwH$2E0D7+~xxG$E{}PUUZAq Kz)U9CuKWj|XQ)X4 delta 2952 zcma)8Yit|G5x(Uek0-t#BE^?T>J^KUBEfR)R1XDr8m>3C(HqkzY798^?NSR?$ zAt^Mzmw0&J38&deJPJcVL-CzZ4S#^l6alq&WD9LCPfz4q{lv(6$&&wHSS2 z$-RZX0~Lu8DPGMmy3M$1HkQ_S^<()nCgQgus}Nt$%Nh^5ioMV-?!W2%mNk|y$7F1Gj~cf*7lh+;X3V&q** zCyJBLEKxo|!EqCD2w~(Q?Lu=P2nsk^y-qxvuE63r`GIf-^^muOav0$NoxLZ538y}v zE9z!Br{px;wD{*bjHa3apan=PkR~8|DAD=C=%^}bxEBOABg;t(-VL*$*s?Q%V*|xY zCsYe1iORs;l-KQYPSvWJsrb881t{u8O71dWlDBLx(G`B~2`y&rRuqj;byBF{ZDkds zTHHpiIa-qj3k$}ubQSIsjaCU|9g}L2@WA4S4%zg0A6NqD=_Jw}(#5PiAxi0qJY8AR z{qHLJ2%@;1{)`_B+(IhdQ(QpUYbW%ch$4G4u6qWqQn!D3) z?17`=Q9GX@67u|&MYv({bB~JlZRR+fw12CVAcTk9@OAAn!tbZ@S2w~TO0uL$wRlgF zd%m~PAIX=TM3$UM@(b@7a>rHYGNp}vOWm1CG~PfqtYL(HW)7ktdC}QM=>c<$I&gLk z_SzWZVR}{TozJ&3Ozl>VH=@Jnu7NQoxe?}f>;;sxOu*q- zp;+(!{!i<_zPf(>_WF-+-Tmnw)?d1@{>~IxzxI<)ug!k)>cT(Yef^V}+lD3HJgw80 z9gnHGOwY)1SPLVDb@{(=?E6Y^n zR&#~C&WVzm#V)`Z266(xT-l0ix@Kj~VyRl#^x9@2RJGKT=1O%}3LBiAGPR@zRSaFg zl3Fa#d!@qNfjzBWN_V9xrOy@$nyTSU5vQoZ41Lr9A|E?WO+BzP;pW%kuf&%_9n(iX z-~y{0fAz_kC$AhU^GqOk^Vyf5eN|recdqz*Oa9)!3H2*NtR%$dk|m*YOKL3%kK8x2 z0scQcV{y%#dvS8b)lzb`thm}suJ&bD$1>k}`|ulw=Z}^xjM?|V%D9_Wy^*!9ll|9Y zE1vfEJ?(c~YrO}r53kgBzhB?IbmW-_7S7F24{g{PA+XxAd!?nX)Y3OAd>|xN6WuF` zzEZ-F8b7xArceA=g?{m)!{NhG@=-%z@qZ0JM^l@{Qfm(hYO}3*J`LhCK%Sr^>3mpC zfTd9q3E(!HHU26Nu62ZFIEYGb^lbIUw5nya2^Bv_F@8l3hJS-P$fx1PDcz~QAZO(4 zIZ3(HO^XymFV}hPQZbhw!vaLC17eipE|?Q&AIzN1s1mk;Xi>p8qhdQq_9u+5-H4&X z4)AmWfhyE_1?p9;e0kiUK56t^5BX)Jm43S3iG=*{t-u3d%@!v#JOtt~@>wJ_2!9Di z85wJMgSk&-rm{|9fFe)G()V$J>fi@UbZ!vlr29_|6Yy!m+Vc;sF@UEFx@P|I)Ht diff --git a/api/__pycache__/todos.cpython-312.pyc b/api/__pycache__/todos.cpython-312.pyc index dc425f47a11a215f6ae9daee21c70c2b08a7bd29..23c30f53875978d2d5daf4b90df674b84e9891de 100644 GIT binary patch delta 2473 zcmbVOOKcNY6n$^}AII@8PW;V}(8l}%6j8pRBDARlI1w=kOe-13^PCLK#O|9h5UG-b zREeyrqFJ=NW*L-LMe>GKRd%3m99b1*qzHA<1#B9iYPWrNCZtYMTFS_|bKm`(bMJlc z+4=48)1$3F`g}ZsXCWr-Z@bkRtzNew^d%CJh-IW;0V|xqDK^2T*af@d5F8kMoa|Iw zg3HuxvRm;89)%Zpb8VNsidXQOxoH;z<2BoJfPEr`J-HYRx3FhXKj3~xeq zh;5`(jIh6U!jmq*5IdOGZEC>Ci_s>=9$+*9H;AejBY{_wZz9EFoDo{>B$~i`8Tjx9 zTyyan*gKnN1>i{%G6|ews)?YF5uDcda2*q8w$f}Vzw_nn{-yk`CiJ(h{T}Xq!%_4! zVf9;BqrGp4wXF#&JsVEr5Tmvl#)nf!>ej}t)k`%Y-O~Fq)cW+ zGEMl_G6oCY4Z{U=I#~O*33sh@pGBmEBh;DP<_n;vZ#EbEvkzJ>OKs2bup{g~{y&MzfUvtimtN zY^>uPLV76nb#;f~D(2bMDoYfO5VeE&`>?^uHCokA==hj81K~EX_<& z4WF93tjvNJhmT4*bz_8F0fMP&NQ`8@*r>tzDX9tc;3G25FJm;0IO}ID}Sh7~}EBX!s{Iyn=ck zqn;I%`V}P}qrqh~xPtmt(2f<9{KXUcnGep#ez$QvH|Jb+BhGy@aU(H5wh+JD{_ybV zGW%Sbs!ywpOIkfwnM*YARed1QiLdFIMAU!Jb>G`~FIzuxelFg?wZCZ5CVg$;2617PZE z^wj{mH@RyU#C5Z{Q|x~X_s!Ie)cmK5?GMvK%j`2I)ITaV#-{4$rsq-(Ea~IP&h3|3 zLJLbsu60@W_1-!1V4{BhQsZpCer6K7E9jpmJ4Zwo8Df$B>ydqTga_B^6O)aRLOmlw jBzbi3_}yZy^bE1cg?CorwK)77UeHAmKLZF#*p~bSMvES} delta 2108 zcmb7FU1%It6rMY?JG+_v&nEl7o1exeo78TVB$BjA6Kx8`CZ=iZ!yv>><|b~P$!_m% z#0ZiU1p6YQ_d!9>^g*;FLSbKglD>)vhCEmXLR3&_UtG}_1;KM>vgtMz8wd8AbIxi zp9Zx6l@}da@Ga-&evlf6oCBD1&P&}i5C{EMs9q7;Y}UeB_)spV)?Cz2pDj)Yg!(3o=?eG1Pvmye~_*WdT~ii`5b+%ut48Xa`{prS1D@6vgx#5S<#eBj4l>lHyxGyQrRTAtjL!f znJRZgMbgSQ=~4;I@m@TF9%ex)4P|bot>Ql^*GwE z@a@P<^<&>hqC5m^7UyS!k?9z(z zhMAbcKMnSzj-%KO0K>6Zj-bM(%I^jH!qx~pDQyTfNlsH(5I39Q@HUw#@Lw0I-$wG{0}|(_{x1uwHvZ|`R36JBP*{GL$24rg z7x7O>H=j(T*Gzx;nx5AeUZKY7$s#it9%fUntdy}>4q)M&*#gOd2s0} zV_3e-@D`iOEL_{mmWKH-#E)9pIz(mro&`|dZmC&u1vb&-eTD1L@Q|BQX{Ew0;PQDsmW;3A)-oI^gMNft!ta2>{`EKmHCJ>x$`(L}ACj^H zpQ4&X2-y=xc7%~#p>t2@*cH-03&}lUbXyqR6}onX-XEKq?|PzlmC%j&A5KXT*X5cb zi0-$O*ONCcZzR5qd_Oe3Jv6G)J*K@niwXbT5sLs;^zr8_Vb&mOl+VgDkNBoJ!1SRb%G4=U^?zxPw$X(d^nv+ zOh7R&c&GL`edp`EJq%>XD1S4ZiJz^B&0y_0uy);6(K^QOq%+=e@*A4jJj~-CbE3b9 diff --git a/api/contents.py b/api/contents.py index 1c6d97a..7b7af55 100644 --- a/api/contents.py +++ b/api/contents.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Query, File, UploadFile +from fastapi import APIRouter, Depends, HTTPException, status, Query, File, UploadFile, Security from sqlalchemy.orm import Session from sqlalchemy import func from typing import List, Optional @@ -11,13 +11,13 @@ from models import Content as ContentModel, Device as DeviceModel from mqtt_manager import mqtt_manager from image_processor import image_processor from config import settings +from auth import get_api_key router = APIRouter( - prefix="/api", tags=["contents"] ) -@router.post("/devices/{device_id}/content", response_model=ContentSchema, status_code=status.HTTP_201_CREATED) +@router.post("/devices/{device_id}/content", response_model=ContentSchema, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_api_key)]) async def create_content( device_id: str, content: ContentCreate, @@ -60,7 +60,7 @@ async def create_content( return db_content -@router.get("/devices/{device_id}/content", response_model=List[ContentSchema]) +@router.get("/devices/{device_id}/content", response_model=List[ContentSchema], dependencies=[Depends(get_api_key)]) async def list_content( device_id: str, skip: int = 0, @@ -87,7 +87,7 @@ async def list_content( contents = query.order_by(ContentModel.version.desc()).offset(skip).limit(limit).all() return contents -@router.get("/devices/{device_id}/content/{version}", response_model=ContentResponse) +@router.get("/devices/{device_id}/content/{version}", response_model=ContentResponse, dependencies=[Depends(get_api_key)]) async def get_content( device_id: str, version: int, @@ -141,7 +141,7 @@ async def get_content( created_at=content.created_at ) -@router.put("/devices/{device_id}/content/{version}", response_model=ContentSchema) +@router.put("/devices/{device_id}/content/{version}", response_model=ContentSchema, dependencies=[Depends(get_api_key)]) async def update_content( device_id: str, version: int, @@ -176,7 +176,7 @@ async def update_content( return content -@router.delete("/devices/{device_id}/content/{version}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/devices/{device_id}/content/{version}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_api_key)]) async def delete_content( device_id: str, version: int, diff --git a/api/devices.py b/api/devices.py index c05985a..adc339c 100644 --- a/api/devices.py +++ b/api/devices.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Security from sqlalchemy.orm import Session from typing import List, Optional from datetime import datetime @@ -9,13 +9,13 @@ from schemas import Device as DeviceSchema, DeviceCreate, DeviceUpdate, Bootstra from models import Device as DeviceModel from database import Content as ContentModel from mqtt_manager import mqtt_manager +from auth import get_api_key router = APIRouter( - prefix="/api/devices", tags=["devices"] ) -@router.post("/", response_model=DeviceSchema, status_code=status.HTTP_201_CREATED) +@router.post("/", response_model=DeviceSchema, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_api_key)]) async def create_device(device: DeviceCreate, db: Session = Depends(get_db)): """ 注册新设备 @@ -46,7 +46,7 @@ async def create_device(device: DeviceCreate, db: Session = Depends(get_db)): return db_device -@router.get("/", response_model=List[DeviceSchema]) +@router.get("/", response_model=List[DeviceSchema], dependencies=[Depends(get_api_key)]) async def list_devices( skip: int = 0, limit: int = 100, @@ -68,7 +68,7 @@ async def list_devices( devices = query.offset(skip).limit(limit).all() return devices -@router.get("/{device_id}", response_model=DeviceSchema) +@router.get("/{device_id}", response_model=DeviceSchema, dependencies=[Depends(get_api_key)]) async def get_device(device_id: str, db: Session = Depends(get_db)): """ 获取设备详情 @@ -81,7 +81,7 @@ async def get_device(device_id: str, db: Session = Depends(get_db)): ) return device -@router.put("/{device_id}", response_model=DeviceSchema) +@router.put("/{device_id}", response_model=DeviceSchema, dependencies=[Depends(get_api_key)]) async def update_device( device_id: str, device_update: DeviceUpdate, @@ -108,7 +108,7 @@ async def update_device( return device -@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_api_key)]) async def delete_device(device_id: str, db: Session = Depends(get_db)): """ 删除设备 @@ -126,11 +126,12 @@ async def delete_device(device_id: str, db: Session = Depends(get_db)): db.delete(device) db.commit() -@router.get("/{device_id}/bootstrap", response_model=BootstrapResponse) -async def device_bootstrap(device_id: str, db: Session = Depends(get_db)): +@router.post("/{device_id}/bootstrap", response_model=BootstrapResponse, dependencies=[Depends(get_api_key)]) +async def bootstrap_device(device_id: str, db: Session = Depends(get_db)): """ - 设备启动获取当前版本信息 + 设备引导 - 获取设备配置和内容 """ + # 验证设备是否存在 device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first() if not device: raise HTTPException( @@ -138,29 +139,19 @@ async def device_bootstrap(device_id: str, db: Session = Depends(get_db)): detail="设备不存在" ) - # 更新设备最后在线时间 - device.last_online = datetime.utcnow() - db.commit() - - # 获取最新的活跃内容 - latest_content = db.query(ContentModel).filter( - ContentModel.device_id == device_id, - ContentModel.is_active == True - ).order_by(ContentModel.version.desc()).first() + # 获取设备场景的内容 + contents = db.query(ContentModel).filter(ContentModel.scene == device.scene).all() + # 构建响应 response = BootstrapResponse( device_id=device_id, - timezone=latest_content.timezone if latest_content else "Asia/Shanghai", - time_format=latest_content.time_format if latest_content else "%Y-%m-%d %H:%M" + scene=device.scene, + contents=contents ) - if latest_content: - response.content_version = latest_content.version - response.last_updated = latest_content.created_at - return response -@router.get("/{device_id}/status") +@router.get("/{device_id}/status", dependencies=[Depends(get_api_key)]) async def get_device_status(device_id: str, db: Session = Depends(get_db)): """ 获取设备状态 diff --git a/api/todos.py b/api/todos.py index fbeb654..f198a7e 100644 --- a/api/todos.py +++ b/api/todos.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Security from sqlalchemy.orm import Session from typing import List, Optional from datetime import datetime @@ -7,13 +7,13 @@ from database import get_db from schemas import Todo as TodoSchema, TodoCreate, TodoUpdate from models import Todo as TodoModel from database import Device as DeviceModel +from auth import get_api_key router = APIRouter( - prefix="/api/todos", tags=["todos"] ) -@router.post("/", response_model=TodoSchema, status_code=status.HTTP_201_CREATED) +@router.post("/", response_model=TodoSchema, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_api_key)]) async def create_todo(todo: TodoCreate, db: Session = Depends(get_db)): """ 创建新的待办事项 @@ -40,7 +40,7 @@ async def create_todo(todo: TodoCreate, db: Session = Depends(get_db)): return db_todo -@router.get("/", response_model=List[TodoSchema]) +@router.get("/", response_model=List[TodoSchema], dependencies=[Depends(get_api_key)]) async def list_todos( skip: int = 0, limit: int = 100, @@ -62,7 +62,7 @@ async def list_todos( todos = query.order_by(TodoModel.created_at.desc()).offset(skip).limit(limit).all() return todos -@router.get("/{todo_id}", response_model=TodoSchema) +@router.get("/{todo_id}", response_model=TodoSchema, dependencies=[Depends(get_api_key)]) async def get_todo(todo_id: int, db: Session = Depends(get_db)): """ 获取待办事项详情 @@ -75,7 +75,7 @@ async def get_todo(todo_id: int, db: Session = Depends(get_db)): ) return todo -@router.put("/{todo_id}", response_model=TodoSchema) +@router.put("/{todo_id}", response_model=TodoSchema, dependencies=[Depends(get_api_key)]) async def update_todo( todo_id: int, todo_update: TodoUpdate, @@ -110,7 +110,7 @@ async def update_todo( return todo -@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_api_key)]) async def delete_todo(todo_id: int, db: Session = Depends(get_db)): """ 删除待办事项 @@ -125,7 +125,7 @@ async def delete_todo(todo_id: int, db: Session = Depends(get_db)): db.delete(todo) db.commit() -@router.post("/{todo_id}/complete", response_model=TodoSchema) +@router.post("/{todo_id}/complete", response_model=TodoSchema, dependencies=[Depends(get_api_key)]) async def complete_todo(todo_id: int, db: Session = Depends(get_db)): """ 标记待办事项为完成 @@ -146,7 +146,7 @@ async def complete_todo(todo_id: int, db: Session = Depends(get_db)): return todo -@router.post("/{todo_id}/incomplete", response_model=TodoSchema) +@router.post("/{todo_id}/incomplete", response_model=TodoSchema, dependencies=[Depends(get_api_key)]) async def incomplete_todo(todo_id: int, db: Session = Depends(get_db)): """ 标记待办事项为未完成 diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..fe8bf4d --- /dev/null +++ b/auth.py @@ -0,0 +1,118 @@ +from fastapi import HTTPException, status, Request, Security, Depends +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.responses import Response +from config import settings +import logging + +logger = logging.getLogger(__name__) + +# 创建API Key安全方案 +api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) + +async def get_api_key(api_key: str = Security(api_key_header)): + """ + API Key依赖项,用于路由级别的鉴权 + """ + if not api_key: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="缺少API Key", + headers={"WWW-Authenticate": "ApiKey"}, + ) + + if api_key != settings.secret_key: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无效的API Key", + headers={"WWW-Authenticate": "ApiKey"}, + ) + + return api_key + +class APIKeyMiddleware(BaseHTTPMiddleware): + """ + API Key鉴权中间件 + 验证请求中的Secret Key + """ + + async def dispatch(self, request: Request, call_next): + # 跳过不需要鉴权的路径 + if self._should_skip_auth(request.url.path): + return await call_next(request) + + # 检查API Key + api_key = request.headers.get("X-API-Key") + if not api_key: + logger.warning(f"缺少API Key: {request.method} {request.url.path}") + return Response( + content='{"detail": "缺少API Key"}', + status_code=status.HTTP_401_UNAUTHORIZED, + media_type="application/json" + ) + + # 验证API Key + if api_key != settings.secret_key: + logger.warning(f"无效的API Key: {request.method} {request.url.path}") + return Response( + content='{"detail": "无效的API Key"}', + status_code=status.HTTP_401_UNAUTHORIZED, + media_type="application/json" + ) + + return await call_next(request) + + def _should_skip_auth(self, path: str) -> bool: + """ + 判断是否跳过鉴权的路径 + """ + # 所有API路径都需要鉴权,不跳过 + # 如果路径以/api开头,则不跳过(需要鉴权) + if path.startswith("/api"): + return False + + skip_paths = [ + "/", + "/health", + "/docs", + "/redoc", + "/openapi.json", + "/admin", + "/admin/login", + "/static", + ] + + # 检查是否以跳过路径开头 + for skip_path in skip_paths: + if path.startswith(skip_path): + return True + + return False + + +class AdminAuthMiddleware(BaseHTTPMiddleware): + """ + Admin页面认证中间件 + 验证用户是否已登录 + """ + + async def dispatch(self, request: Request, call_next): + # 只对admin路径进行认证 + if not request.url.path.startswith("/admin") or request.url.path == "/admin/login": + return await call_next(request) + + # 检查会话 + if not self._is_authenticated(request): + # 重定向到登录页面 + from fastapi.responses import RedirectResponse + return RedirectResponse(url="/admin/login?next=" + request.url.path, status_code=303) + + return await call_next(request) + + def _is_authenticated(self, request: Request) -> bool: + """ + 检查用户是否已认证 + """ + # 从session中获取认证信息 + session = request.session + return session.get("authenticated", False) \ No newline at end of file diff --git a/config.py b/config.py index b69c719..6241588 100644 --- a/config.py +++ b/config.py @@ -29,6 +29,10 @@ class Settings(BaseSettings): algorithm: str = "HS256" access_token_expire_minutes: int = 30 + # 管理员配置 + admin_username: str = "admin" + admin_password: str = "123456" + class Config: env_file = ".env" diff --git a/main.py b/main.py index ebfbc88..5689b40 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware from contextlib import asynccontextmanager import logging import os @@ -10,6 +11,7 @@ from database import init_db from mqtt_manager import mqtt_manager from api import api_router from admin_routes import admin_router +from auth import APIKeyMiddleware, AdminAuthMiddleware # 配置日志 logging.basicConfig( @@ -51,10 +53,21 @@ async def lifespan(app: FastAPI): # 创建FastAPI应用 app = FastAPI( - title=settings.app_name, - description="基于 FastAPI + MQTT + HTTP/HTTPS + NTP 的轻量级墨水屏显示系统服务端", + title="墨水屏桌面屏幕系统 API", + description="用于管理墨水屏设备、内容和待办事项的API", version="1.0.0", - lifespan=lifespan + lifespan=lifespan, + openapi_components={ + "securitySchemes": { + "APIKeyHeader": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key", + "description": "API Key鉴权,请在下方输入正确的API Key" + } + } + }, + security=[{"APIKeyHeader": []}] ) # 添加CORS中间件 @@ -66,11 +79,20 @@ app.add_middleware( allow_headers=["*"], ) +# 添加API Key鉴权中间件 +app.add_middleware(APIKeyMiddleware) + +# 添加Admin认证中间件 +app.add_middleware(AdminAuthMiddleware) + +# 添加Session中间件 +app.add_middleware(SessionMiddleware, secret_key=settings.secret_key) + # 挂载静态文件 app.mount("/static", StaticFiles(directory=settings.static_dir), name="static") # 注册API路由 -app.include_router(api_router) +app.include_router(api_router, prefix="/api") # 包含管理后台路由 app.include_router(admin_router, prefix="/admin", tags=["管理后台"]) diff --git a/templates/admin/base.html b/templates/admin/base.html index 06a0394..5b8e4f5 100644 --- a/templates/admin/base.html +++ b/templates/admin/base.html @@ -50,7 +50,22 @@ - +

+ diff --git a/templates/admin/login.html b/templates/admin/login.html new file mode 100644 index 0000000..174ab04 --- /dev/null +++ b/templates/admin/login.html @@ -0,0 +1,74 @@ +{% extends "admin/base.html" %} + +{% block title %}管理员登录{% endblock %} + +{% block content %} + + + +{% endblock %} \ No newline at end of file