mirror of
https://github.com/SOR2171/Springboot4-Vue3_Demo.git
synced 2026-10-23 11:31:10 +08:00
git init
This commit is contained in:
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+151
@@ -0,0 +1,151 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="LanguageInjectionConfiguration">
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>AsyncQueryRunner (org.apache.commons.dbutils)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("batch").withParameterCount(2).definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("insertBatch").withParameterCount(3).definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "insert").withParameters("java.lang.String", "org.apache.commons.dbutils.ResultSetHandler").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "insert").withParameters("java.lang.String", "org.apache.commons.dbutils.ResultSetHandler", "java.lang.Object...").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("update").withParameters("java.lang.String").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("update").withParameters("java.lang.String", "java.lang.Object").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("update").withParameters("java.lang.String", "java.lang.Object...").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("batch").withParameterCount(3).definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("insertBatch").withParameterCount(4).definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("query", "insert").withParameters("java.sql.Connection", "java.lang.String", "org.apache.commons.dbutils.ResultSetHandler").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("query", "insert").withParameters("java.sql.Connection", "java.lang.String", "org.apache.commons.dbutils.ResultSetHandler", "java.lang.Object...").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("update").withParameters("java.sql.Connection", "java.lang.String").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("update").withParameters("java.sql.Connection", "java.lang.String", "java.lang.Object").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("update").withParameters("java.sql.Connection", "java.lang.String", "java.lang.Object...").definedInClass("org.apache.commons.dbutils.AsyncQueryRunner"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>Jodd (jodd.db)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query").withParameterCount(1).definedInClass("jodd.db.DbQuery"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("DbQuery").withParameterCount(2).definedInClass("jodd.db.DbQuery"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("query").withParameterCount(2).definedInClass("jodd.db.DbQuery"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(2, psiMethod().withName("DbQuery").withParameterCount(3).definedInClass("jodd.db.DbQuery"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>MyBatis @Select/@Delete/@Insert/@Update</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiMethod().withName("value").withParameters().definedInClass("org.apache.ibatis.annotations.Delete")]]></place>
|
||||||
|
<place><![CDATA[psiMethod().withName("value").withParameters().definedInClass("org.apache.ibatis.annotations.Insert")]]></place>
|
||||||
|
<place><![CDATA[psiMethod().withName("value").withParameters().definedInClass("org.apache.ibatis.annotations.Select")]]></place>
|
||||||
|
<place><![CDATA[psiMethod().withName("value").withParameters().definedInClass("org.apache.ibatis.annotations.Update")]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>QueryRunner (org.apache.commons.dbutils)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("batch").withParameterCount(2).definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("insertBatch").withParameterCount(3).definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "insert").withParameters("java.lang.String", "org.apache.commons.dbutils.ResultSetHandler").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "insert", "execute").withParameters("java.lang.String", "org.apache.commons.dbutils.ResultSetHandler", "java.lang.Object...").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("update").withParameters("java.lang.String").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("update").withParameters("java.lang.String", "java.lang.Object").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("update", "execute").withParameters("java.lang.String", "java.lang.Object...").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("batch").withParameterCount(3).definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("insertBatch").withParameterCount(4).definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("query", "insert").withParameters("java.sql.Connection", "java.lang.String", "org.apache.commons.dbutils.ResultSetHandler", "java.lang.Object...").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("query", "insert", "execute").withParameters("java.sql.Connection", "java.lang.String", "org.apache.commons.dbutils.ResultSetHandler").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("update").withParameters("java.sql.Connection", "java.lang.String").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("update").withParameters("java.sql.Connection", "java.lang.String", "java.lang.Object").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(1, psiMethod().withName("update", "execute").withParameters("java.sql.Connection", "java.lang.String", "java.lang.Object...").definedInClass("org.apache.commons.dbutils.QueryRunner"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>R2DBC (io.r2dbc)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("add").definedInClass("io.r2dbc.spi.Batch"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("createStatement").definedInClass("io.r2dbc.spi.Connection"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="PostgreSQL" injector-id="java">
|
||||||
|
<display-name>Reactiverse Postgres Client (io.reactiverse)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch").definedInClass("io.reactiverse.pgclient.PgConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch").definedInClass("io.reactiverse.pgclient.PgTransaction"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPrepare", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.reactiverse.reactivex.pgclient.PgClient"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPrepare", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.reactiverse.reactivex.pgclient.PgConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPrepare", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.reactiverse.reactivex.pgclient.PgPool"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPrepare", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.reactiverse.reactivex.pgclient.PgTransaction"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "preparedQuery", "preparedBatch").definedInClass("io.reactiverse.axle.pgclient.PgClient"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "preparedQuery", "preparedBatch").definedInClass("io.reactiverse.pgclient.PgClient"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "preparedQuery", "preparedBatch").definedInClass("io.reactiverse.pgclient.PgPool"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>SmallRye Axle SqlClient (io.vertx.axle.sqlclient)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "preparedQuery", "preparedBatch").definedInClass("io.vertx.axle.sqlclient.Pool"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "preparedQuery", "preparedBatch").definedInClass("io.vertx.axle.sqlclient.SqlClient"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>SmallRye Mutiny SqlClient (io.vertx.mutiny.sqlclient)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "preparedQuery", "preparedBatch").definedInClass("io.vertx.mutiny.sqlclient.Pool"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "preparedQuery", "preparedBatch").definedInClass("io.vertx.mutiny.sqlclient.SqlClient"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>SmallRye Mutiny SqlConnection (io.vertx.mutiny.sqlclient)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("prepare", "prepareAndAwait").definedInClass("io.vertx.mutiny.db2client.DB2Connection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("prepare", "prepareAndAwait").definedInClass("io.vertx.mutiny.mssqlclient.MSSQLConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("prepare", "prepareAndAwait").definedInClass("io.vertx.mutiny.mysqlclient.MySQLConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("prepare", "prepareAndAwait").definedInClass("io.vertx.mutiny.pgclient.PgConnection"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>Vert.x SQL Extensions (io.vertx.ext.sql)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "queryWithParams", "queryStream", "queryStreamWithParams", "querySingle", "querySingleWithParams", "update", "updateWithParams", "call", "callWithParams").definedInClass("io.vertx.ext.sql.SQLClient"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "queryWithParams", "queryStream", "queryStreamWithParams", "querySingle", "querySingleWithParams", "update", "updateWithParams", "call", "callWithParams").definedInClass("io.vertx.ext.sql.SQLOperations"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "queryWithParams", "queryStream", "queryStreamWithParams", "querySingle", "querySingleWithParams", "update", "updateWithParams", "call", "callWithParams", "execute", "batchWithParams", "batchCallableWithParams").definedInClass("io.vertx.ext.sql.SQLConnection"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>Vert.x SQL Reactive Extensions (io.vertx.reactivex.ext.sql)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "queryWithParams", "queryStream", "queryStreamWithParams", "querySingle", "querySingleWithParams", "update", "updateWithParams", "call", "callWithParams").definedInClass("io.vertx.reactivex.ext.sql.SQLOperations"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "queryWithParams", "queryStream", "queryStreamWithParams", "querySingle", "querySingleWithParams", "update", "updateWithParams", "call", "callWithParams", "execute", "batchWithParams", "batchCallableWithParams", "rxQuerySingle", "rxQuerySingleWithParams", "rxQuery", "rxQueryWithParams", "rxQueryStream", "rxQueryStreamWithParams", "rxUpdate", "rxUpdateWithParams", "rxCall", "rxCallWithParams", "rxExecute", "rxBatchWithParams", "rxBatchCallableWithParams").definedInClass("io.vertx.reactivex.ext.sql.SQLClient"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "queryWithParams", "queryStream", "queryStreamWithParams", "querySingle", "querySingleWithParams", "update", "updateWithParams", "call", "callWithParams", "execute", "batchWithParams", "batchCallableWithParams", "rxQuerySingle", "rxQuerySingleWithParams", "rxQuery", "rxQueryWithParams", "rxQueryStream", "rxQueryStreamWithParams", "rxUpdate", "rxUpdateWithParams", "rxCall", "rxCallWithParams", "rxExecute", "rxBatchWithParams", "rxBatchCallableWithParams").definedInClass("io.vertx.reactivex.ext.sql.SQLConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("querySingle", "rxQuerySingle", "querySingleWithParams", "rxQuerySingleWithParams").definedInClass("io.vertx.reactivex.ext.asyncsql.AsyncSQLClient"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("querySingle", "rxQuerySingle", "querySingleWithParams", "rxQuerySingleWithParams").definedInClass("io.vertx.reactivex.ext.asyncsql.MySQLClient"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("querySingle", "rxQuerySingle", "querySingleWithParams", "rxQuerySingleWithParams").definedInClass("io.vertx.reactivex.ext.asyncsql.PostgreSQLClient"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>Vert.x SqlClient (io.vertx.sqlclient)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch").definedInClass("io.vertx.mssqlclient.MSSQLConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch").definedInClass("io.vertx.mysqlclient.MySQLConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch").definedInClass("io.vertx.pgclient.PgConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch").definedInClass("io.vertx.sqlclient.Pool"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch").definedInClass("io.vertx.sqlclient.SqlClient"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch").definedInClass("io.vertx.sqlclient.SqlConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch").definedInClass("io.vertx.sqlclient.Transaction"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>Vert.x SqlClient RxJava2 (io.vertx.reactivex.sqlclient)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPrepare", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.vertx.reactivex.mysqlclient.MySQLConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPrepare", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.vertx.reactivex.pgclient.PgConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPrepare", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.vertx.reactivex.sqlclient.SqlConnection"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPrepare", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.vertx.reactivex.sqlclient.Transaction"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.vertx.reactivex.mysqlclient.MySQLPool"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.vertx.reactivex.pgclient.PgPool"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.vertx.reactivex.sqlclient.Pool"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "prepare", "preparedQuery", "preparedBatch", "rxQuery", "rxPreparedQuery", "rxPreparedBatch").definedInClass("io.vertx.reactivex.sqlclient.SqlClient"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>jOOQ (org.jooq.DSLContext)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("batch").withParameters("java.lang.String", "java.lang.Object[]...").definedInClass("org.jooq.DSLContext"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "fetch", "fetchLazy", "fetchAsync", "fetchStream", "fetchMany", "fetchOne", "fetchSingle", "fetchOptional", "fetchValue", "fetchOptionalValue", "fetchValues", "execute", "resultQuery").withParameters("java.lang.String", "java.lang.Object...").definedInClass("org.jooq.DSLContext"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("query", "fetch", "fetchLazy", "fetchAsync", "fetchStream", "fetchMany", "fetchOne", "fetchSingle", "fetchOptional", "fetchValue", "fetchOptionalValue", "fetchValues", "execute", "resultQuery", "batch").withParameters("java.lang.String").definedInClass("org.jooq.DSLContext"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(psiMethod().withName("batch").withParameters("java.lang.String...").definedInClass("org.jooq.DSLContext"))]]></place>
|
||||||
|
</injection>
|
||||||
|
<injection language="SQL" injector-id="java">
|
||||||
|
<display-name>rxjava2-jdbc (org.davidmoten.rx.jdbc)</display-name>
|
||||||
|
<single-file value="true" />
|
||||||
|
<place><![CDATA[psiMethod().withName("value").definedInClass("org.davidmoten.rx.jdbc.annotations.Query")]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("call", "select", "update").definedInClass("org.davidmoten.rx.jdbc.Database"))]]></place>
|
||||||
|
<place><![CDATA[psiParameter().ofMethod(0, psiMethod().withName("call", "select", "update").definedInClass("org.davidmoten.rx.jdbc.TransactedBuilder"))]]></place>
|
||||||
|
</injection>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+20
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel>
|
||||||
|
<module name="careerpp" target="25" />
|
||||||
|
<module name="careerpp.main" target="25" />
|
||||||
|
<module name="careerpp.test" target="25" />
|
||||||
|
</bytecodeTargetLevel>
|
||||||
|
</component>
|
||||||
|
<component name="JavacSettings">
|
||||||
|
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||||
|
<module name="backend" options="-parameters" />
|
||||||
|
<module name="backend.main" options="-parameters" />
|
||||||
|
<module name="backend.test" options="-parameters" />
|
||||||
|
<module name="careerpp" options="-parameters" />
|
||||||
|
<module name="careerpp.main" options="-parameters" />
|
||||||
|
<module name="careerpp.test" options="-parameters" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/backend" />
|
||||||
|
<option name="gradleJvm" value="24" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/backend" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="externalSystemId" value="Gradle" />
|
||||||
|
<option name="version" value="2.3.20" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Springboot4-Vue3.iml" filepath="$PROJECT_DIR$/.idea/Springboot4-Vue3.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/backend/src/main/resources/springboot_vue3.sql" dialect="GenericSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 SOR2171
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
## Springboot4-Vue3
|
||||||
|
|
||||||
|
###### ~~*Long may the Student Pack*~~
|
||||||
|
|
||||||
|
English / [简体中文](./README_CN.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
|
||||||
|
- Kotlin with Gradle.kts
|
||||||
|
- Spring Security
|
||||||
|
- Spring Mail
|
||||||
|
- Mybatis Plus
|
||||||
|
- JWT
|
||||||
|
- MySQL
|
||||||
|
- Redis
|
||||||
|
- RabbitMQ
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
|
||||||
|
- Vue3 with Vite and TypeScript
|
||||||
|
- vue-router
|
||||||
|
- axios
|
||||||
|
- element-plus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Project Setup
|
||||||
|
|
||||||
|
##### General Requirements
|
||||||
|
|
||||||
|
- JDK 25+
|
||||||
|
- Kotlin 2.3.20+
|
||||||
|
- Node.js 16+
|
||||||
|
- MySQL 8+
|
||||||
|
- Redis 6+
|
||||||
|
- RabbitMQ 3.8+
|
||||||
|
|
||||||
|
##### Git clone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/SOR2171/Springboot4-Vue3_Demo.git
|
||||||
|
cd Springboot4-Vue3_Demo
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Backend Setup
|
||||||
|
|
||||||
|
1. Go to backend directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Configure application properties
|
||||||
|
|
||||||
|
Crate `src/main/resources/application.yml`
|
||||||
|
or edit `src/main/resources/application-example.yml`
|
||||||
|
to set your own configurations.
|
||||||
|
|
||||||
|
3. Load Database
|
||||||
|
|
||||||
|
Import `src/main/resources/springboot_vue3.sql`
|
||||||
|
> default database user:
|
||||||
|
>
|
||||||
|
> username: `test`
|
||||||
|
> password: `123456`
|
||||||
|
>
|
||||||
|
> (the password saved in SQL needs to be obtained through the test class)
|
||||||
|
|
||||||
|
4. Grade build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gradle build
|
||||||
|
```
|
||||||
|
|
||||||
|
jar directory: `build/libs/backend-(ver).jar`
|
||||||
|
|
||||||
|
5. Run the backend server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar build/libs/backend-(ver).jar
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Frontend Setup
|
||||||
|
|
||||||
|
1. Go to frontend directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. prepare some picture
|
||||||
|
|
||||||
|
you need a `frontend/src/assets/welcome-image.png`, which is the background of welcome page.
|
||||||
|
|
||||||
|
3. Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The build files will be generated in `dist` directory.
|
||||||
|
|
||||||
|
##### Nginx Setup (Optional)
|
||||||
|
|
||||||
|
If you want to serve the frontend with Nginx, you can follow these steps:
|
||||||
|
|
||||||
|
1. Install Nginx if you don't have it.
|
||||||
|
|
||||||
|
[download Nginx](https://nginx.org/en/download.html)
|
||||||
|
|
||||||
|
2. Copy the contents of the `dist` directory to your Nginx `html` directory.
|
||||||
|
3. Configure Nginx to serve the frontend and proxy API requests to the backend server.
|
||||||
|
4. Restart Nginx.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
## Springboot4-Vue3
|
||||||
|
|
||||||
|
###### ~~*赞美学生包*~~
|
||||||
|
|
||||||
|
[English](./README.md) / 简体中文
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
|
||||||
|
#### 后端
|
||||||
|
|
||||||
|
- Kotlin + Gradle.kts
|
||||||
|
- Spring Security
|
||||||
|
- Spring Mail
|
||||||
|
- Mybatis Plus
|
||||||
|
- JWT
|
||||||
|
- MySQL
|
||||||
|
- Redis
|
||||||
|
- RabbitMQ
|
||||||
|
|
||||||
|
#### 前端
|
||||||
|
|
||||||
|
- Vue3 + Vite + TypeScript
|
||||||
|
- vue-router
|
||||||
|
- axios
|
||||||
|
- element-plus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 项目搭建
|
||||||
|
|
||||||
|
#### 基础要求
|
||||||
|
|
||||||
|
- JDK 25+
|
||||||
|
- Kotlin 2.3.20+
|
||||||
|
- Node.js 16+
|
||||||
|
- MySQL 8+
|
||||||
|
- Redis 6+
|
||||||
|
- RabbitMQ 3.8+
|
||||||
|
|
||||||
|
#### Git 克隆项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/SOR2171/Springboot4-Vue3_Demo.git
|
||||||
|
cd Springboot4-Vue3_Demo
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 后端配置
|
||||||
|
|
||||||
|
1. 进入后端目录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 配置应用属性
|
||||||
|
|
||||||
|
创建 `src/main/resources/application.yml`
|
||||||
|
或编辑 `src/main/resources/application-example.yml`
|
||||||
|
以设置你自己的配置。
|
||||||
|
|
||||||
|
3. 导入数据库
|
||||||
|
|
||||||
|
导入 `src/main/resources/springboot_vue3.sql`
|
||||||
|
|
||||||
|
> 默认数据库用户:
|
||||||
|
>
|
||||||
|
> 用户名:`test`
|
||||||
|
> 密码:`123456`
|
||||||
|
>
|
||||||
|
> (SQL中保存的密码需要通过测试类获取)
|
||||||
|
|
||||||
|
4. Gradle 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gradle build
|
||||||
|
```
|
||||||
|
|
||||||
|
jar 文件目录:`build/libs/backend-(ver).jar`
|
||||||
|
|
||||||
|
5. 启动后端服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar build/libs/backend-(ver).jar
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 前端配置
|
||||||
|
|
||||||
|
1. 进入前端目录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 准备一些图片
|
||||||
|
|
||||||
|
你需要一张 `frontend/src/assets/welcome-image.png` 用作登陆界面的背景
|
||||||
|
|
||||||
|
3. 构建前端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建结果会生成在 `dist` 目录中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Nginx 部署(可选)
|
||||||
|
|
||||||
|
如果你想使用 Nginx 来部署前端,可以按以下步骤操作:
|
||||||
|
|
||||||
|
1. 若未安装 Nginx,请先安装。
|
||||||
|
|
||||||
|
[下载 Nginx](https://nginx.org/en/download.html)
|
||||||
|
|
||||||
|
2. 将 `dist` 目录中的内容复制到 Nginx 的 `html` 目录。
|
||||||
|
|
||||||
|
3. 配置 Nginx,使其能够提供前端页面,并将 API 请求代理到后端服务器。
|
||||||
|
|
||||||
|
4. 重启 Nginx。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/gradlew text eol=lf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.jar binary
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
HELP.md
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
src/main/resources/application.yaml
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Kotlin ###
|
||||||
|
.kotlin
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
## localhost:8080/api
|
||||||
|
|
||||||
|
所有返回的json对象总有如下格式
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"code": int,
|
||||||
|
"message": string,
|
||||||
|
"data": T
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
接下来将省略`/api`并只给出`T`的类声明
|
||||||
|
|
||||||
|
当`"code"`不是`200`时,`"data"`必定为`null`
|
||||||
|
|
||||||
|
部分`/api/test`的返回值不遵循上述格式
|
||||||
|
|
||||||
|
## /auth
|
||||||
|
|
||||||
|
### POST /login
|
||||||
|
|
||||||
|
Authorization: 无
|
||||||
|
|
||||||
|
key: username, password
|
||||||
|
|
||||||
|
data:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"username": string,
|
||||||
|
"role": string,
|
||||||
|
"token": sting,
|
||||||
|
"expire": time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /logout
|
||||||
|
|
||||||
|
Authorization: Bearer $Token
|
||||||
|
|
||||||
|
key: 无
|
||||||
|
|
||||||
|
data:
|
||||||
|
|
||||||
|
```
|
||||||
|
null
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /ask-code?email=******&type=register
|
||||||
|
|
||||||
|
Authorization: 无
|
||||||
|
|
||||||
|
key: 无
|
||||||
|
|
||||||
|
data:
|
||||||
|
|
||||||
|
```
|
||||||
|
null
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /ask-code?email=******&type=reset
|
||||||
|
|
||||||
|
Authorization: 无
|
||||||
|
|
||||||
|
key: 无
|
||||||
|
|
||||||
|
data:
|
||||||
|
|
||||||
|
```
|
||||||
|
null
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /register
|
||||||
|
|
||||||
|
Authorization: 无
|
||||||
|
|
||||||
|
key: 无
|
||||||
|
|
||||||
|
data:email, code, username, password
|
||||||
|
|
||||||
|
```
|
||||||
|
null
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /reset
|
||||||
|
|
||||||
|
Authorization: 无
|
||||||
|
|
||||||
|
key: email, code, password
|
||||||
|
|
||||||
|
data:
|
||||||
|
|
||||||
|
```
|
||||||
|
null
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /relogin
|
||||||
|
|
||||||
|
Authorization: Bearer $Token
|
||||||
|
|
||||||
|
key: 无
|
||||||
|
|
||||||
|
data:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"token": sting,
|
||||||
|
"expire": time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## /test
|
||||||
|
|
||||||
|
### GET /hello
|
||||||
|
|
||||||
|
Authorization: Bearer $Token
|
||||||
|
|
||||||
|
key: 无
|
||||||
|
|
||||||
|
回复:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hello Kotlin WebFlux!
|
||||||
|
```
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "2.3.20"
|
||||||
|
kotlin("plugin.spring") version "2.3.20"
|
||||||
|
kotlin("plugin.serialization") version "2.3.20"
|
||||||
|
id("org.springframework.boot") version "4.0.4"
|
||||||
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.github.sor2171"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("com.auth0:java-jwt:4.4.0")
|
||||||
|
implementation("com.baomidou:mybatis-plus-spring-boot4-starter:3.5.15")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-amqp")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-json")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-mail")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||||
|
implementation("org.springframework:spring-messaging")
|
||||||
|
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
|
||||||
|
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
|
||||||
|
implementation("tools.jackson.module:jackson-module-kotlin")
|
||||||
|
runtimeOnly("com.mysql:mysql-connector-j")
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||||
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test")
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property")
|
||||||
|
jvmToolchain(25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<org.springframework.boot.gradle.tasks.run.BootRun> {
|
||||||
|
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test> {
|
||||||
|
useJUnitPlatform()
|
||||||
|
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
|
}
|
||||||
|
rootProject.name = "backend"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.github.sor2171.backend
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.runApplication
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
class CareerppApplication
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runApplication<CareerppApplication>(*args)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.github.sor2171.backend.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class BeanConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun passwordEncoder(): PasswordEncoder {
|
||||||
|
return BCryptPasswordEncoder()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.github.sor2171.backend.config
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.serializer
|
||||||
|
import org.springframework.amqp.core.Message
|
||||||
|
import org.springframework.amqp.core.MessageProperties
|
||||||
|
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory
|
||||||
|
import org.springframework.amqp.rabbit.connection.ConnectionFactory
|
||||||
|
import org.springframework.amqp.rabbit.core.RabbitTemplate
|
||||||
|
import org.springframework.amqp.support.converter.AbstractMessageConverter
|
||||||
|
import org.springframework.amqp.support.converter.MessageConverter
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
|
import org.springframework.boot.amqp.autoconfigure.SimpleRabbitListenerContainerFactoryConfigurer
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class RabbitConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun messageConverter(@Qualifier("kotlinxSerializationJson") json: Json): MessageConverter =
|
||||||
|
object : AbstractMessageConverter() {
|
||||||
|
override fun createMessage(obj: Any, messageProperties: MessageProperties): Message {
|
||||||
|
val serializer = json.serializersModule.serializer(obj::class.java)
|
||||||
|
val jsonString = json.encodeToString(serializer, obj)
|
||||||
|
|
||||||
|
val bytes = jsonString.toByteArray(Charsets.UTF_8)
|
||||||
|
messageProperties.contentType = MessageProperties.CONTENT_TYPE_JSON
|
||||||
|
messageProperties.contentLength = bytes.size.toLong()
|
||||||
|
return Message(bytes, messageProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fromMessage(message: Message): Any {
|
||||||
|
val content = String(message.body, Charsets.UTF_8)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun rabbitTemplate(
|
||||||
|
connectionFactory: ConnectionFactory,
|
||||||
|
messageConverter: MessageConverter
|
||||||
|
): RabbitTemplate {
|
||||||
|
val rabbitTemplate = RabbitTemplate(connectionFactory)
|
||||||
|
rabbitTemplate.messageConverter = messageConverter
|
||||||
|
return rabbitTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun rabbitListenerContainerFactory(
|
||||||
|
configurer: SimpleRabbitListenerContainerFactoryConfigurer,
|
||||||
|
connectionFactory: ConnectionFactory,
|
||||||
|
messageConverter: MessageConverter
|
||||||
|
): SimpleRabbitListenerContainerFactory {
|
||||||
|
val factory = SimpleRabbitListenerContainerFactory()
|
||||||
|
configurer.configure(factory, connectionFactory)
|
||||||
|
factory.setMessageConverter(messageConverter)
|
||||||
|
return factory
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.github.sor2171.backend.config
|
||||||
|
|
||||||
|
import com.github.sor2171.backend.entity.RestBean
|
||||||
|
import com.github.sor2171.backend.entity.vo.response.AuthorizeVO
|
||||||
|
import com.github.sor2171.backend.filter.JwtAuthorizeFilter
|
||||||
|
import com.github.sor2171.backend.service.AccountService
|
||||||
|
import com.github.sor2171.backend.utils.DateUtils
|
||||||
|
import com.github.sor2171.backend.utils.JwtUtils
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.security.authentication.ReactiveAuthenticationManager
|
||||||
|
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||||
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||||
|
import org.springframework.security.config.web.server.SecurityWebFiltersOrder
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
|
import org.springframework.security.core.userdetails.User
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
|
import org.springframework.security.web.server.authentication.AuthenticationWebFilter
|
||||||
|
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
|
||||||
|
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository
|
||||||
|
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
class SecurityConfig(
|
||||||
|
private val jwtUtils: JwtUtils,
|
||||||
|
private val service: AccountService,
|
||||||
|
private val jwtAuthorizeFilter: JwtAuthorizeFilter,
|
||||||
|
private val passwordEncoder: PasswordEncoder
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun reactiveAuthenticationManager(): ReactiveAuthenticationManager {
|
||||||
|
val manager = UserDetailsRepositoryReactiveAuthenticationManager(service)
|
||||||
|
manager.setPasswordEncoder(passwordEncoder)
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
return http
|
||||||
|
.authorizeExchange {
|
||||||
|
it
|
||||||
|
.pathMatchers(
|
||||||
|
"/api/auth/relogin"
|
||||||
|
).authenticated()
|
||||||
|
.pathMatchers(
|
||||||
|
"/api/auth/**",
|
||||||
|
"/error"
|
||||||
|
).permitAll()
|
||||||
|
.anyExchange().authenticated()
|
||||||
|
}
|
||||||
|
.addFilterAt(loginAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
|
||||||
|
.addFilterAt(jwtAuthorizeFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||||
|
.exceptionHandling {
|
||||||
|
it.authenticationEntryPoint { exchange, ex ->
|
||||||
|
writeJsonResponse(
|
||||||
|
exchange,
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
RestBean.unauthenticated(ex.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.accessDeniedHandler { exchange, ex ->
|
||||||
|
writeJsonResponse(
|
||||||
|
exchange,
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
RestBean.forbidden(ex.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.csrf { it.disable() }
|
||||||
|
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun loginAuthenticationFilter(): AuthenticationWebFilter {
|
||||||
|
val filter = AuthenticationWebFilter(reactiveAuthenticationManager())
|
||||||
|
filter.setRequiresAuthenticationMatcher(
|
||||||
|
ServerWebExchangeMatchers.pathMatchers("/api/auth/login")
|
||||||
|
)
|
||||||
|
filter.setServerAuthenticationConverter(loginAuthenticationConverter())
|
||||||
|
filter.setAuthenticationSuccessHandler { webFilterExchange, authentication ->
|
||||||
|
val user = authentication.principal as User
|
||||||
|
service.findAccountByNameOrEmail(user.username)
|
||||||
|
.flatMap { account ->
|
||||||
|
val vo = account.toAnotherObject(
|
||||||
|
AuthorizeVO::class,
|
||||||
|
mapOf(
|
||||||
|
"token" to jwtUtils.createJwt(user, account.id!!, account.username),
|
||||||
|
"expire" to DateUtils.dateToLocalDateTime(jwtUtils.expiresTime())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
writeJsonResponse(
|
||||||
|
webFilterExchange.exchange,
|
||||||
|
HttpStatus.OK,
|
||||||
|
RestBean.success(vo)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter.setAuthenticationFailureHandler { webFilterExchange, exception ->
|
||||||
|
writeJsonResponse(
|
||||||
|
webFilterExchange.exchange,
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
RestBean.unauthenticated(exception.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loginAuthenticationConverter(): ServerAuthenticationConverter {
|
||||||
|
return ServerAuthenticationConverter { exchange ->
|
||||||
|
exchange.formData.flatMap { formData ->
|
||||||
|
val username = formData.getFirst("username")
|
||||||
|
val password = formData.getFirst("password")
|
||||||
|
if (username != null && password != null) {
|
||||||
|
Mono.just(
|
||||||
|
UsernamePasswordAuthenticationToken(
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Mono.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <reified T> writeJsonResponse(
|
||||||
|
exchange: ServerWebExchange,
|
||||||
|
status: HttpStatus,
|
||||||
|
body: RestBean<T>
|
||||||
|
): Mono<Void> {
|
||||||
|
val response = exchange.response
|
||||||
|
response.statusCode = status
|
||||||
|
response.headers.contentType = MediaType.APPLICATION_JSON
|
||||||
|
|
||||||
|
val jsonString = Json.encodeToJsonElement(body).toString()
|
||||||
|
val buffer = response.bufferFactory().wrap(jsonString.toByteArray())
|
||||||
|
return response.writeWith(Mono.just(buffer))
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.github.sor2171.backend.config
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class SerializationConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun kotlinxSerializationJson(): Json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
isLenient = true
|
||||||
|
encodeDefaults = false
|
||||||
|
prettyPrint = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.github.sor2171.backend.controller
|
||||||
|
|
||||||
|
import com.github.sor2171.backend.entity.RestBean
|
||||||
|
import com.github.sor2171.backend.entity.vo.request.EmailRegisterVO
|
||||||
|
import com.github.sor2171.backend.entity.vo.request.PasswordResetVO
|
||||||
|
import com.github.sor2171.backend.service.AccountService
|
||||||
|
import jakarta.validation.Valid
|
||||||
|
import jakarta.validation.constraints.Email
|
||||||
|
import jakarta.validation.constraints.NotBlank
|
||||||
|
import jakarta.validation.constraints.Pattern
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.validation.annotation.Validated
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@Validated
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
class AuthorizeController(
|
||||||
|
private val service: AccountService
|
||||||
|
) {
|
||||||
|
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
@GetMapping("/ask-code")
|
||||||
|
fun askVerifyCode(
|
||||||
|
@RequestParam @NotBlank @Email email: String,
|
||||||
|
@RequestParam @Pattern(regexp = "(register|reset)") type: String,
|
||||||
|
exchange: ServerWebExchange
|
||||||
|
): Mono<RestBean<Any?>> {
|
||||||
|
logger.info("Trying to ask code")
|
||||||
|
val ip = exchange.request.remoteAddress?.address?.hostAddress ?: "unknown"
|
||||||
|
return service.askEmailVerifyCode(type, email, ip)
|
||||||
|
.map { messageHandler(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
fun emailRegister(@RequestBody @Valid vo: EmailRegisterVO): Mono<RestBean<Any?>> {
|
||||||
|
logger.info("Trying to register")
|
||||||
|
return service.registerEmailAccount(vo)
|
||||||
|
.map { messageHandler(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/reset")
|
||||||
|
fun emailResetPassword(@RequestBody @Valid vo: PasswordResetVO): Mono<RestBean<Any?>> {
|
||||||
|
logger.info("Trying to reset password")
|
||||||
|
return service.resetEmailAccountPassword(vo)
|
||||||
|
.map { messageHandler(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/logout")
|
||||||
|
fun logout(exchange: ServerWebExchange): Mono<RestBean<Any?>> {
|
||||||
|
logger.info("Trying to logout")
|
||||||
|
val authorization = exchange.request.headers.getFirst("Authorization")
|
||||||
|
return Mono.fromCallable { service.invalidateJwt(authorization) }
|
||||||
|
.map { success ->
|
||||||
|
if (success) RestBean.success()
|
||||||
|
else RestBean.logoutFailed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/relogin")
|
||||||
|
fun relogin(exchange: ServerWebExchange): Mono<RestBean<Any?>> {
|
||||||
|
logger.info("Trying to relogin")
|
||||||
|
val authorization = exchange.request.headers.getFirst("Authorization")
|
||||||
|
return service
|
||||||
|
.jwtTokenRelogin(authorization)
|
||||||
|
?.map { RestBean.success(it) }
|
||||||
|
?: Mono.just(messageHandler("Please login with password."))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun messageHandler(wrongMessage: String): RestBean<Any?> {
|
||||||
|
return if (wrongMessage.isBlank()) {
|
||||||
|
RestBean.success()
|
||||||
|
} else {
|
||||||
|
RestBean.failure(400, null, wrongMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.github.sor2171.backend.controller
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/test")
|
||||||
|
class TestController {
|
||||||
|
|
||||||
|
@GetMapping("/hello")
|
||||||
|
fun test(): Mono<String> = Mono.just("Hello Kotlin WebFlux!")
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.github.sor2171.backend.entity
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
import kotlin.reflect.full.createInstance
|
||||||
|
import kotlin.reflect.full.declaredMemberProperties
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
import kotlin.reflect.jvm.javaField
|
||||||
|
|
||||||
|
interface DataCopy {
|
||||||
|
|
||||||
|
fun <T : Any> toAnotherObject(
|
||||||
|
toClass: KClass<T>,
|
||||||
|
otherProperties: Map<String, Any?>
|
||||||
|
): T {
|
||||||
|
try {
|
||||||
|
val voProperties = toClass.declaredMemberProperties
|
||||||
|
val vo = toClass.createInstance()
|
||||||
|
for (voProperty in voProperties)
|
||||||
|
copyFieldData(voProperty, otherProperties, vo)
|
||||||
|
return vo
|
||||||
|
} catch (e: ReflectiveOperationException) {
|
||||||
|
throw RuntimeException(
|
||||||
|
"${toClass.simpleName} has no fields.",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyFieldData(
|
||||||
|
toProperty: KProperty<*>,
|
||||||
|
otherProperties: Map<String, Any?>,
|
||||||
|
another: Any
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val thisProperty = this::class.memberProperties
|
||||||
|
.firstOrNull { it.name == toProperty.name }
|
||||||
|
|
||||||
|
val value =
|
||||||
|
if (thisProperty != null) {
|
||||||
|
thisProperty.getter.call(this)
|
||||||
|
} else if (otherProperties.containsKey(toProperty.name))
|
||||||
|
otherProperties[toProperty.name]
|
||||||
|
else
|
||||||
|
throw RuntimeException(
|
||||||
|
"Property ${toProperty.name} not found."
|
||||||
|
)
|
||||||
|
|
||||||
|
val field = toProperty.javaField!!
|
||||||
|
field.isAccessible = true
|
||||||
|
field.set(another, value)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.github.sor2171.backend.entity
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RestBean<T>(
|
||||||
|
val code: Int,
|
||||||
|
val data: T,
|
||||||
|
val message: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun <T> success(data: T, message: String = "Success"): RestBean<T> {
|
||||||
|
return RestBean(200, data, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 针对无 data 返回的情况,统一使用 Any?
|
||||||
|
fun success(message: String = "Success"): RestBean<Any?> {
|
||||||
|
return RestBean(200, null, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> failure(code: Int = 401, data: T, message: String?): RestBean<T> {
|
||||||
|
return RestBean(code, data, message ?: "Failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unauthenticated(message: String?) =
|
||||||
|
failure(401, null, message)
|
||||||
|
|
||||||
|
fun forbidden(message: String?) =
|
||||||
|
failure(403, null, message)
|
||||||
|
|
||||||
|
fun logoutFailed(message: String = ""): RestBean<Any?> {
|
||||||
|
val formatMessage = if (message.isBlank()) "" else ": $message"
|
||||||
|
return failure(400, null, "Logout Failed$formatMessage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> RestBean<T>.toJsonString(): String {
|
||||||
|
return Json.encodeToString(this)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.github.sor2171.backend.entity.dto
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName
|
||||||
|
import com.github.sor2171.backend.entity.DataCopy
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@TableName("account")
|
||||||
|
data class Account(
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
val id: Int? = null,
|
||||||
|
val username: String = "",
|
||||||
|
val password: String = "",
|
||||||
|
val email: String = "",
|
||||||
|
val role: String = "",
|
||||||
|
val registerTime: LocalDateTime? = null
|
||||||
|
) : DataCopy
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.github.sor2171.backend.entity.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EmailVerificationMsg(
|
||||||
|
val email: String,
|
||||||
|
val type: String,
|
||||||
|
val code: String
|
||||||
|
)
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package com.github.sor2171.backend.entity.vo.request
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email
|
||||||
|
import jakarta.validation.constraints.Pattern
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.hibernate.validator.constraints.Length
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EmailRegisterVO(
|
||||||
|
@param:Email
|
||||||
|
val email: String = "",
|
||||||
|
@param:Length(min = 6, max = 6)
|
||||||
|
val code: String? = null,
|
||||||
|
@param:Pattern(regexp = "^[a-zA-Z0-9\\u4e00-\\u9fa5]+$")
|
||||||
|
@param:Length(min = 4, max = 16)
|
||||||
|
val username: String = "",
|
||||||
|
@param:Length(min = 6, max = 20)
|
||||||
|
val password: String = "",
|
||||||
|
)
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package com.github.sor2171.backend.entity.vo.request
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.hibernate.validator.constraints.Length
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PasswordResetVO(
|
||||||
|
@param:Email
|
||||||
|
val email: String = "",
|
||||||
|
@param:Length(min = 6, max = 6)
|
||||||
|
val code: String? = null,
|
||||||
|
@param:Length(min = 6, max = 20)
|
||||||
|
val password: String = "",
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.github.sor2171.backend.entity.vo.response
|
||||||
|
|
||||||
|
import com.github.sor2171.backend.utils.DateUtils.getCurrentDateTime
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AuthorizeVO(
|
||||||
|
val username: String = "",
|
||||||
|
val role: String = "",
|
||||||
|
val token: String = "",
|
||||||
|
val expire: LocalDateTime = getCurrentDateTime(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.github.sor2171.backend.entity.vo.response
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReloginVO(
|
||||||
|
val token: String = "",
|
||||||
|
val expire: String = "",
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.github.sor2171.backend.exception
|
||||||
|
|
||||||
|
import com.github.sor2171.backend.entity.RestBean
|
||||||
|
import jakarta.validation.ValidationException
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
class ValidationController {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(ValidationController::class.java)
|
||||||
|
|
||||||
|
@ExceptionHandler(ValidationException::class)
|
||||||
|
fun validationException(e: Exception): RestBean<out String?> {
|
||||||
|
log.warn("Resolve [${e.javaClass.name}: ${e.message}]")
|
||||||
|
return RestBean.failure(
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
"parameter validation error"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.github.sor2171.backend.filter
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.web.cors.CorsConfiguration
|
||||||
|
import org.springframework.web.cors.reactive.CorsWebFilter
|
||||||
|
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class CorsFilter {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun corsWebFilter(): CorsWebFilter {
|
||||||
|
val config = CorsConfiguration().apply {
|
||||||
|
allowedOriginPatterns = listOf("*")
|
||||||
|
allowedMethods = listOf(
|
||||||
|
HttpMethod.GET.name(),
|
||||||
|
HttpMethod.POST.name(),
|
||||||
|
HttpMethod.PUT.name(),
|
||||||
|
HttpMethod.DELETE.name(),
|
||||||
|
HttpMethod.OPTIONS.name()
|
||||||
|
)
|
||||||
|
allowedHeaders = listOf(
|
||||||
|
HttpHeaders.AUTHORIZATION,
|
||||||
|
HttpHeaders.CONTENT_TYPE
|
||||||
|
)
|
||||||
|
allowCredentials = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val source = UrlBasedCorsConfigurationSource().apply {
|
||||||
|
registerCorsConfiguration("/**", config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CorsWebFilter(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.github.sor2171.backend.filter
|
||||||
|
|
||||||
|
import com.github.sor2171.backend.entity.RestBean
|
||||||
|
import com.github.sor2171.backend.entity.toJsonString
|
||||||
|
import com.github.sor2171.backend.utils.Const
|
||||||
|
import org.springframework.core.annotation.Order
|
||||||
|
import org.springframework.data.redis.core.ReactiveStringRedisTemplate
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import org.springframework.web.server.WebFilter
|
||||||
|
import org.springframework.web.server.WebFilterChain
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Order(Const.FLOW_LIMIT_ORDER)
|
||||||
|
class FlowLimitFilter(
|
||||||
|
private val template: ReactiveStringRedisTemplate
|
||||||
|
) : WebFilter {
|
||||||
|
|
||||||
|
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
|
||||||
|
val address = exchange.request.remoteAddress?.address?.hostAddress ?: "unknown"
|
||||||
|
|
||||||
|
return tryCount(address).flatMap { allowed ->
|
||||||
|
if (allowed) {
|
||||||
|
chain.filter(exchange)
|
||||||
|
} else {
|
||||||
|
writeBlockMessage(exchange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeBlockMessage(exchange: ServerWebExchange): Mono<Void> {
|
||||||
|
val response = exchange.response
|
||||||
|
response.statusCode = HttpStatus.FORBIDDEN
|
||||||
|
response.headers.contentType = MediaType.APPLICATION_JSON
|
||||||
|
val buffer = response.bufferFactory()
|
||||||
|
.wrap(RestBean.forbidden("Too many requests").toJsonString().toByteArray())
|
||||||
|
return response.writeWith(Mono.just(buffer))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryCount(ip: String): Mono<Boolean> {
|
||||||
|
val blockKey = Const.FLOW_LIMIT_BLOCK + ip
|
||||||
|
val counterKey = Const.FLOW_LIMIT_COUNTER + ip
|
||||||
|
|
||||||
|
return template.hasKey(blockKey).flatMap { isBlocked ->
|
||||||
|
if (isBlocked) {
|
||||||
|
Mono.just(false)
|
||||||
|
} else {
|
||||||
|
template.hasKey(counterKey).flatMap { hasCounter ->
|
||||||
|
if (hasCounter) {
|
||||||
|
template.opsForValue().increment(counterKey).flatMap { count ->
|
||||||
|
if (count > 10) {
|
||||||
|
template.opsForValue()
|
||||||
|
.set(blockKey, "1", Duration.ofMinutes(1))
|
||||||
|
.thenReturn(false)
|
||||||
|
} else {
|
||||||
|
Mono.just(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
template.opsForValue()
|
||||||
|
.set(counterKey, "1", Duration.ofSeconds(3))
|
||||||
|
.thenReturn(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.github.sor2171.backend.filter
|
||||||
|
|
||||||
|
import com.github.sor2171.backend.utils.JwtUtils
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||||
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import org.springframework.web.server.WebFilter
|
||||||
|
import org.springframework.web.server.WebFilterChain
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class JwtAuthorizeFilter(
|
||||||
|
private val utils: JwtUtils
|
||||||
|
) : WebFilter {
|
||||||
|
|
||||||
|
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
|
||||||
|
val authorization = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
|
||||||
|
val jwt = utils.resolveJwt(authorization)
|
||||||
|
|
||||||
|
return if (jwt != null) {
|
||||||
|
val user = utils.toUser(jwt)
|
||||||
|
val authentication = UsernamePasswordAuthenticationToken(
|
||||||
|
user,
|
||||||
|
null,
|
||||||
|
user.authorities
|
||||||
|
)
|
||||||
|
exchange.attributes["id"] = utils.toId(jwt)
|
||||||
|
|
||||||
|
chain.filter(exchange)
|
||||||
|
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication))
|
||||||
|
} else {
|
||||||
|
chain.filter(exchange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.github.sor2171.backend.listener
|
||||||
|
|
||||||
|
import com.github.sor2171.backend.entity.dto.EmailVerificationMsg
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitHandler
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.mail.MailSender
|
||||||
|
import org.springframework.mail.SimpleMailMessage
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RabbitListener(queues = ["mail"])
|
||||||
|
class MailQueueListener(
|
||||||
|
@param:Value($$"${spring.mail.username}")
|
||||||
|
private val username: String,
|
||||||
|
|
||||||
|
private val json: Json,
|
||||||
|
private val sender: MailSender
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(this::class.java)
|
||||||
|
|
||||||
|
@RabbitHandler
|
||||||
|
fun senderMailMessage(content: String) {
|
||||||
|
val data = json.decodeFromString<EmailVerificationMsg>(content)
|
||||||
|
val (email, type, code) = data
|
||||||
|
|
||||||
|
val message = when (type) {
|
||||||
|
"register" -> createMailMessage(
|
||||||
|
"Welcome to Our Service",
|
||||||
|
"Thank you for registering! Your verification code is: $code"
|
||||||
|
+ "\nThis code is valid for 3 minutes.",
|
||||||
|
email
|
||||||
|
)
|
||||||
|
|
||||||
|
"reset" -> createMailMessage(
|
||||||
|
"Password Reset Request",
|
||||||
|
"You requested a password reset. Your verification code is: $code"
|
||||||
|
+ "\nThis code is valid for 3 minutes.",
|
||||||
|
email
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
sender.send(message)
|
||||||
|
logger.info("Email sent to $email.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createMailMessage(title: String, content: String, to: String): SimpleMailMessage {
|
||||||
|
val message = SimpleMailMessage()
|
||||||
|
message.subject = title
|
||||||
|
message.text = content
|
||||||
|
message.from = username
|
||||||
|
message.setTo(to)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.github.sor2171.backend.mapper
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper
|
||||||
|
import com.github.sor2171.backend.entity.dto.Account
|
||||||
|
import org.apache.ibatis.annotations.Mapper
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
interface AccountMapper : BaseMapper<Account>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.github.sor2171.backend.service
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService
|
||||||
|
import com.github.sor2171.backend.entity.dto.Account
|
||||||
|
import com.github.sor2171.backend.entity.vo.request.EmailRegisterVO
|
||||||
|
import com.github.sor2171.backend.entity.vo.request.PasswordResetVO
|
||||||
|
import com.github.sor2171.backend.entity.vo.response.ReloginVO
|
||||||
|
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
interface AccountService : IService<Account>, ReactiveUserDetailsService {
|
||||||
|
fun findAccountByNameOrEmail(text: String): Mono<Account>
|
||||||
|
fun askEmailVerifyCode(type: String, email: String, ip: String): Mono<String>
|
||||||
|
fun registerEmailAccount(vo: EmailRegisterVO): Mono<String>
|
||||||
|
fun resetEmailAccountPassword(vo: PasswordResetVO): Mono<String>
|
||||||
|
fun invalidateJwt(headerToken: String?): Boolean
|
||||||
|
fun jwtTokenRelogin(headerToken: String?): Mono<ReloginVO>?
|
||||||
|
}
|
||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
package com.github.sor2171.backend.service.impl
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
|
||||||
|
import com.github.sor2171.backend.entity.dto.Account
|
||||||
|
import com.github.sor2171.backend.entity.dto.EmailVerificationMsg
|
||||||
|
import com.github.sor2171.backend.entity.vo.request.EmailRegisterVO
|
||||||
|
import com.github.sor2171.backend.entity.vo.request.PasswordResetVO
|
||||||
|
import com.github.sor2171.backend.entity.vo.response.ReloginVO
|
||||||
|
import com.github.sor2171.backend.mapper.AccountMapper
|
||||||
|
import com.github.sor2171.backend.service.AccountService
|
||||||
|
import com.github.sor2171.backend.utils.Const
|
||||||
|
import com.github.sor2171.backend.utils.DateUtils.getCurrentDateTime
|
||||||
|
import com.github.sor2171.backend.utils.FlowUtils
|
||||||
|
import com.github.sor2171.backend.utils.JwtUtils
|
||||||
|
import org.springframework.amqp.core.AmqpTemplate
|
||||||
|
import org.springframework.data.redis.core.ReactiveStringRedisTemplate
|
||||||
|
import org.springframework.security.core.userdetails.User
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import reactor.core.scheduler.Schedulers
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AccountServiceImpl(
|
||||||
|
private val utils: FlowUtils,
|
||||||
|
private val jwtUtils: JwtUtils,
|
||||||
|
private val amqpTemplate: AmqpTemplate,
|
||||||
|
private val stringRedisTemplate: ReactiveStringRedisTemplate,
|
||||||
|
private val encoder: PasswordEncoder
|
||||||
|
|
||||||
|
) : ServiceImpl<AccountMapper, Account>(), AccountService {
|
||||||
|
|
||||||
|
override fun findByUsername(username: String): Mono<UserDetails> {
|
||||||
|
return findAccountByNameOrEmail(username)
|
||||||
|
.switchIfEmpty(Mono.error(UsernameNotFoundException("Account with name $username not found")))
|
||||||
|
.map { account ->
|
||||||
|
User.withUsername(username)
|
||||||
|
.password(account.password)
|
||||||
|
.roles(account.role)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findAccountByNameOrEmail(text: String): Mono<Account> {
|
||||||
|
return Mono.fromCallable {
|
||||||
|
this.query()
|
||||||
|
.eq("username", text).or()
|
||||||
|
.eq("email", text)
|
||||||
|
.one()
|
||||||
|
}.flatMap { account ->
|
||||||
|
Mono.just(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun askEmailVerifyCode(type: String, email: String, ip: String): Mono<String> {
|
||||||
|
return verifyLimit(ip).flatMap { allowed ->
|
||||||
|
if (allowed) {
|
||||||
|
val code = (100000..999999).random().toString()
|
||||||
|
val data = EmailVerificationMsg(email, type, code)
|
||||||
|
|
||||||
|
Mono.fromRunnable<Void> {
|
||||||
|
amqpTemplate.convertAndSend("mail", data)
|
||||||
|
}.then(
|
||||||
|
stringRedisTemplate.opsForValue()
|
||||||
|
.set(
|
||||||
|
Const.VERIFY_EMAIL_DATA + email,
|
||||||
|
code,
|
||||||
|
Duration.ofMinutes(3)
|
||||||
|
)
|
||||||
|
).thenReturn("").subscribeOn(Schedulers.boundedElastic())
|
||||||
|
} else {
|
||||||
|
Mono.just("Request limit exceeded. Please try again later.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerEmailAccount(vo: EmailRegisterVO): Mono<String> {
|
||||||
|
val (email, code, username, password) = vo
|
||||||
|
|
||||||
|
return verifyCode(email, code).flatMap { error ->
|
||||||
|
if (!error.isBlank()) {
|
||||||
|
return@flatMap Mono.just(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
existAccountByEmail(email).flatMap { emailExists ->
|
||||||
|
if (emailExists) {
|
||||||
|
return@flatMap Mono.just("account with the same email already exists.")
|
||||||
|
}
|
||||||
|
|
||||||
|
existAccountByUsername(username).flatMap { usernameExists ->
|
||||||
|
if (usernameExists) {
|
||||||
|
return@flatMap Mono.just("username already exists.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val encodedPassword = encoder.encode(password)
|
||||||
|
val account = Account(
|
||||||
|
null,
|
||||||
|
username,
|
||||||
|
encodedPassword!!,
|
||||||
|
email,
|
||||||
|
"user",
|
||||||
|
getCurrentDateTime()
|
||||||
|
)
|
||||||
|
|
||||||
|
Mono.fromCallable { this.save(account) }
|
||||||
|
.flatMap { success ->
|
||||||
|
if (success) {
|
||||||
|
stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email)
|
||||||
|
.thenReturn("")
|
||||||
|
} else {
|
||||||
|
Mono.just("something went wrong. Please contact the administrator.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetEmailAccountPassword(vo: PasswordResetVO): Mono<String> {
|
||||||
|
val (email, code, password) = vo
|
||||||
|
|
||||||
|
return verifyCode(email, code).flatMap { error ->
|
||||||
|
if (!error.isBlank()) {
|
||||||
|
return@flatMap Mono.just(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
existAccountByEmail(email).flatMap { exists ->
|
||||||
|
if (!exists) {
|
||||||
|
return@flatMap Mono.just("account with the email does not exist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val encodedPassword = encoder.encode(password)
|
||||||
|
|
||||||
|
Mono.fromCallable {
|
||||||
|
this.update()
|
||||||
|
.eq("email", email)
|
||||||
|
.set("password", encodedPassword)
|
||||||
|
.update()
|
||||||
|
}.flatMap { success ->
|
||||||
|
if (success) {
|
||||||
|
stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email)
|
||||||
|
.thenReturn("")
|
||||||
|
} else {
|
||||||
|
Mono.just("something went wrong. Please contact the administrator.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun jwtTokenRelogin(headerToken: String?): Mono<ReloginVO>? {
|
||||||
|
val user = jwtUtils.resolveJwt(headerToken?.substring("Bearer ".length))
|
||||||
|
?.let { jwtUtils.toUser(it) }
|
||||||
|
?: return null
|
||||||
|
val accountMono = findAccountByNameOrEmail(user.username)
|
||||||
|
val voMono = accountMono.map {
|
||||||
|
ReloginVO(
|
||||||
|
jwtUtils.createJwt(user, it.id!!, it.username),
|
||||||
|
jwtUtils.expiresTime().toInstant().toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return voMono
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidateJwt(headerToken: String?): Boolean {
|
||||||
|
return jwtUtils.invalidateJwt(headerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun existAccountByEmail(email: String): Mono<Boolean> {
|
||||||
|
return Mono.fromCallable {
|
||||||
|
this.baseMapper.exists(
|
||||||
|
Wrappers.query<Account>()
|
||||||
|
.eq("email", email)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun existAccountByUsername(username: String): Mono<Boolean> {
|
||||||
|
return Mono.fromCallable {
|
||||||
|
this.baseMapper.exists(
|
||||||
|
Wrappers.query<Account>()
|
||||||
|
.eq("username", username)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyLimit(ip: String): Mono<Boolean> {
|
||||||
|
val key = Const.VERIFY_EMAIL_LIMIT + ip
|
||||||
|
return utils.limitOnceCheck(key, 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyCode(email: String, receivedCode: String?): Mono<String> {
|
||||||
|
return stringRedisTemplate.opsForValue()
|
||||||
|
.get(Const.VERIFY_EMAIL_DATA + email)
|
||||||
|
.flatMap { code ->
|
||||||
|
when {
|
||||||
|
receivedCode == null || code != receivedCode ->
|
||||||
|
Mono.just("verify code is wrong.")
|
||||||
|
|
||||||
|
else -> Mono.just("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.switchIfEmpty(Mono.just("verify code has not been sent"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.github.sor2171.backend.utils
|
||||||
|
|
||||||
|
object Const {
|
||||||
|
const val FLOW_LIMIT_ORDER = -101
|
||||||
|
|
||||||
|
const val JWT_BLACK_LIST = "jwt:blacklist:"
|
||||||
|
const val VERIFY_EMAIL_LIMIT = "verify:email:limit:"
|
||||||
|
const val VERIFY_EMAIL_DATA = "verify:email:data:"
|
||||||
|
|
||||||
|
const val FLOW_LIMIT_COUNTER = "flow:counter:"
|
||||||
|
const val FLOW_LIMIT_BLOCK = "flow:block:"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.github.sor2171.backend.utils
|
||||||
|
|
||||||
|
import kotlinx.datetime.*
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object DateUtils {
|
||||||
|
fun getCurrentDateTime() =
|
||||||
|
Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
|
||||||
|
fun dateToLocalDateTime(date: Date): LocalDateTime {
|
||||||
|
return date
|
||||||
|
.toInstant()
|
||||||
|
.toKotlinInstant()
|
||||||
|
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.github.sor2171.backend.utils
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource
|
||||||
|
import org.springframework.data.redis.core.ReactiveStringRedisTemplate
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class FlowUtils(
|
||||||
|
@Resource
|
||||||
|
val template: ReactiveStringRedisTemplate
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun limitOnceCheck(key: String, blockTime: Int): Mono<Boolean> {
|
||||||
|
return template.hasKey(key).flatMap { exists ->
|
||||||
|
if (exists) {
|
||||||
|
Mono.just(false)
|
||||||
|
} else {
|
||||||
|
template.opsForValue()
|
||||||
|
.set(key, "", Duration.ofSeconds(blockTime.toLong()))
|
||||||
|
.thenReturn(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.github.sor2171.backend.utils
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.auth0.jwt.JWTVerifier
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import com.auth0.jwt.interfaces.DecodedJWT
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate
|
||||||
|
import org.springframework.security.core.userdetails.User
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class JwtUtils(
|
||||||
|
@param:Value($$"${spring.security.jwt.key}")
|
||||||
|
private val key: String,
|
||||||
|
|
||||||
|
@param:Value($$"${spring.security.jwt.expire-hours}")
|
||||||
|
private val expireHours: Int,
|
||||||
|
|
||||||
|
private val template: StringRedisTemplate,
|
||||||
|
private val algorithm: Algorithm = Algorithm.HMAC256(key),
|
||||||
|
private val jwtVerifier: JWTVerifier = JWT.require(algorithm).build()
|
||||||
|
) {
|
||||||
|
fun invalidateJwt(headerToken: String?): Boolean {
|
||||||
|
val token = this.convertToToken(headerToken) ?: return false
|
||||||
|
try {
|
||||||
|
val jwt = jwtVerifier.verify(token)
|
||||||
|
val id = jwt.id
|
||||||
|
return deleteToken(id, jwt.expiresAt)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println(e.message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteToken(uuid: String, time: Date): Boolean {
|
||||||
|
val now = Date()
|
||||||
|
val expire = (time.time - now.time).coerceAtLeast(0)
|
||||||
|
template.opsForValue().set(
|
||||||
|
Const.JWT_BLACK_LIST + uuid,
|
||||||
|
"deleted",
|
||||||
|
expire,
|
||||||
|
TimeUnit.MILLISECONDS
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isInvalidToken(uuid: String): Boolean =
|
||||||
|
template.hasKey(Const.JWT_BLACK_LIST + uuid) ?: false
|
||||||
|
|
||||||
|
fun resolveJwt(headerToken: String?): DecodedJWT? {
|
||||||
|
val token = convertToToken(headerToken) ?: return null
|
||||||
|
try {
|
||||||
|
val decodedJWT = jwtVerifier.verify(token)
|
||||||
|
if (this.isInvalidToken(decodedJWT.id)) return null
|
||||||
|
val expiresAt = decodedJWT.expiresAt
|
||||||
|
|
||||||
|
return if (Date().after(expiresAt)) null
|
||||||
|
else decodedJWT
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println(e.message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createJwt(
|
||||||
|
details: UserDetails,
|
||||||
|
id: Int,
|
||||||
|
username: String
|
||||||
|
): String {
|
||||||
|
return JWT.create()
|
||||||
|
.withJWTId(UUID.randomUUID().toString())
|
||||||
|
.withClaim("id", id)
|
||||||
|
.withClaim("username", username)
|
||||||
|
.withClaim(
|
||||||
|
"authorities",
|
||||||
|
details.authorities.map { it.authority }.toList()
|
||||||
|
)
|
||||||
|
.withExpiresAt(expiresTime())
|
||||||
|
.withIssuedAt(Date())
|
||||||
|
.sign(algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toUser(jwt: DecodedJWT): UserDetails {
|
||||||
|
val claims = jwt.claims
|
||||||
|
return User
|
||||||
|
.withUsername(claims["username"]!!.asString())
|
||||||
|
.password("********")
|
||||||
|
.authorities(*(claims["authorities"]!!.asArray(String::class.java)))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toId(jwt: DecodedJWT) = jwt.claims["id"]?.asInt() ?: -1
|
||||||
|
|
||||||
|
fun expiresTime(): Date {
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
cal.add(Calendar.HOUR, expireHours)
|
||||||
|
return cal.time
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertToToken(headerToken: String?): String? =
|
||||||
|
headerToken?.replace("Bearer ", "")
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
spring:
|
||||||
|
webflux:
|
||||||
|
base-path: /
|
||||||
|
|
||||||
|
mail:
|
||||||
|
#host: smtp.example.com
|
||||||
|
#username: 123456@example.com
|
||||||
|
#password: password
|
||||||
|
#port: 123456
|
||||||
|
protocol: smtp
|
||||||
|
properties:
|
||||||
|
mail.smtp.auth: true
|
||||||
|
mail.smtp.ssl.enable: true
|
||||||
|
mail.smtp.timeout: 5000
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
host: localhost
|
||||||
|
port: 5672
|
||||||
|
virtual-host: /
|
||||||
|
#username: admin
|
||||||
|
#password: 123456
|
||||||
|
|
||||||
|
application:
|
||||||
|
name: backend
|
||||||
|
|
||||||
|
security:
|
||||||
|
jwt:
|
||||||
|
#key: 123456
|
||||||
|
expire-hours: 168
|
||||||
|
ip-ban-limit: 10
|
||||||
|
ip-ban-time: 3
|
||||||
|
ip-ban-time-min: 1
|
||||||
|
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://localhost:3306/kotlin_springboot
|
||||||
|
#username: root
|
||||||
|
#password: 123456
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- MySQL dump 10.13 Distrib 8.0.43, for Linux (x86_64)
|
||||||
|
--
|
||||||
|
-- Host: 127.0.0.1 Database: kotlin_springboot
|
||||||
|
-- ------------------------------------------------------
|
||||||
|
-- Server version 8.0.43-0ubuntu0.24.04.2
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!50503 SET NAMES utf8mb4 */;
|
||||||
|
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||||
|
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||||
|
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||||
|
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||||
|
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||||
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `account`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `account`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!50503 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `account` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(255) DEFAULT NULL,
|
||||||
|
`password` varchar(255) DEFAULT NULL,
|
||||||
|
`email` varchar(255) DEFAULT NULL,
|
||||||
|
`role` varchar(255) DEFAULT NULL,
|
||||||
|
`register_time` datetime DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `unique_email` (`email`),
|
||||||
|
UNIQUE KEY `unique_name` (`username`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Dumping data for table `account`
|
||||||
|
--
|
||||||
|
|
||||||
|
LOCK TABLES `account` WRITE;
|
||||||
|
/*!40000 ALTER TABLE `account` DISABLE KEYS */;
|
||||||
|
INSERT INTO `account` VALUES (1,'test','$2a$10$Xqe2W.5zs/I8FAs3cK331e6tyuo4XWTHNhW1oV6bOHUzwMZ2kAPTa','1234567890@gmail.com','user','2025-11-06 15:56:37');
|
||||||
|
/*!40000 ALTER TABLE `account` ENABLE KEYS */;
|
||||||
|
UNLOCK TABLES;
|
||||||
|
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||||
|
|
||||||
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
|
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
|
||||||
|
-- Dump completed on 2025-11-24 17:53:21
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.github.sor2171.backend
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class BackendApplicationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun contextLoads() {
|
||||||
|
println(BCryptPasswordEncoder().encode("123456"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
src/assets
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
|
||||||
|
}
|
||||||
Vendored
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// oxlint-disable
|
||||||
|
// ------
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
ChatPage: typeof import('./src/components/home/ChatPage.vue')['default']
|
||||||
|
ElAside: typeof import('element-plus/es')['ElAside']
|
||||||
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
|
ElFooter: typeof import('element-plus/es')['ElFooter']
|
||||||
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
|
ElImage: typeof import('element-plus/es')['ElImage']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
|
ElMain: typeof import('element-plus/es')['ElMain']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
|
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
|
||||||
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
|
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||||
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
|
HeaderBar: typeof import('./src/components/home/HeaderBar.vue')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
|
export interface GlobalDirectives {
|
||||||
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="index" level="application" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vue.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css"/>
|
||||||
|
<title>frontend</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+3437
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
"@vueuse/core": "^14.0.0",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"dompurify": "^3.3.0",
|
||||||
|
"element-plus": "^2.11.7",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"marked": "^17.0.1",
|
||||||
|
"unplugin-auto-import": "^20.3.0",
|
||||||
|
"uuid": "^13.0.0",
|
||||||
|
"vue": "^3.5.24",
|
||||||
|
"vue-router": "^4.6.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"sass-embedded": "^1.93.3",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"unplugin-vue-components": "^30.0.0",
|
||||||
|
"vite": "^7.2.4",
|
||||||
|
"vue-tsc": "^3.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07"
|
||||||
|
height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198">
|
||||||
|
<path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"/>
|
||||||
|
<path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"/>
|
||||||
|
<path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 456 B |
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {useDark, useToggle} from "@vueuse/core";
|
||||||
|
|
||||||
|
useDark({
|
||||||
|
selector: 'html',
|
||||||
|
attribute: 'class',
|
||||||
|
valueDark: 'dark',
|
||||||
|
valueLight: 'light',
|
||||||
|
})
|
||||||
|
|
||||||
|
useDark({
|
||||||
|
onChanged(dark) {
|
||||||
|
useToggle(dark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<router-view/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import type {
|
||||||
|
ApiResponse,
|
||||||
|
LoginResponse,
|
||||||
|
FailureCallback,
|
||||||
|
SuccessCallback
|
||||||
|
} from "../interfaces/ApiTypes.ts";
|
||||||
|
import { deleteAccessToken, storeAccessToken, takeAccessToken } from "../utils/JwtUtils.ts";
|
||||||
|
import router from "../router";
|
||||||
|
|
||||||
|
// --- Constants & Defaults ---
|
||||||
|
|
||||||
|
export const chatStreamURL = `/api/chat/stream`;
|
||||||
|
|
||||||
|
const defaultFailure: FailureCallback = (message, code, url) => {
|
||||||
|
console.warn(`Request to ${url} failed ${code}: ${message}`);
|
||||||
|
ElMessage.warning('Something went wrong: ' + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultError: ErrorCallback = (err) => {
|
||||||
|
console.error(err);
|
||||||
|
ElMessage.error('Something went wrong, please contact the administrator.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessHeader = (): Record<string, string> => {
|
||||||
|
const token = takeAccessToken();
|
||||||
|
return token ? {
|
||||||
|
'Authorization': 'Bearer ' + token
|
||||||
|
} : {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Internal Request Helpers ---
|
||||||
|
|
||||||
|
function internalPost<T>(
|
||||||
|
url: string,
|
||||||
|
data: any,
|
||||||
|
header: Record<string, string>,
|
||||||
|
success: SuccessCallback<T>,
|
||||||
|
failure: FailureCallback = defaultFailure,
|
||||||
|
error: ErrorCallback = defaultError
|
||||||
|
): void {
|
||||||
|
axios.post<ApiResponse<T>>(url, data, { headers: header })
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
success(data.data);
|
||||||
|
} else {
|
||||||
|
failure(data.message, data.code, url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function internalGet<T>(
|
||||||
|
url: string,
|
||||||
|
header: Record<string, string>,
|
||||||
|
success: SuccessCallback<T>,
|
||||||
|
failure: FailureCallback = defaultFailure,
|
||||||
|
error: ErrorCallback = defaultError
|
||||||
|
): void {
|
||||||
|
axios.get<ApiResponse<T>>(url, { headers: header })
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
success(data.data);
|
||||||
|
} else {
|
||||||
|
failure(data.message, data.code, url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Exported Functions ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic GET request
|
||||||
|
* @param url API Endpoint
|
||||||
|
* @param success Callback on success (code 200)
|
||||||
|
* @param failure Callback on business failure (code != 200)
|
||||||
|
* @param error Callback on network/system error
|
||||||
|
*/
|
||||||
|
export function get<T = any>(
|
||||||
|
url: string,
|
||||||
|
success: SuccessCallback<T>,
|
||||||
|
failure: FailureCallback = defaultFailure,
|
||||||
|
error: ErrorCallback = defaultError
|
||||||
|
): void {
|
||||||
|
internalGet<T>(url, accessHeader(), success, failure, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic POST request
|
||||||
|
* @param url API Endpoint
|
||||||
|
* @param data Request body
|
||||||
|
* @param success Callback on success (code 200)
|
||||||
|
* @param failure Callback on business failure (code != 200)
|
||||||
|
* @param error Callback on network/system error
|
||||||
|
*/
|
||||||
|
export function post<T = any>(
|
||||||
|
url: string,
|
||||||
|
data: any,
|
||||||
|
success: SuccessCallback<T>,
|
||||||
|
failure: FailureCallback = defaultFailure,
|
||||||
|
error: ErrorCallback = defaultError
|
||||||
|
): void {
|
||||||
|
internalPost<T>(url, data, accessHeader(), success, failure, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
remember: boolean,
|
||||||
|
success: SuccessCallback<LoginResponse>
|
||||||
|
): void {
|
||||||
|
internalPost<LoginResponse>(
|
||||||
|
'/api/auth/login',
|
||||||
|
{
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
storeAccessToken(data.token, remember, data.expire);
|
||||||
|
ElMessage.success(`Login successfully! Welcome back, ${data.username}`);
|
||||||
|
success(data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout(success: () => void): void {
|
||||||
|
deleteAccessToken();
|
||||||
|
ElMessage.success('Logout successfully!');
|
||||||
|
router.push('/');
|
||||||
|
success();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface JwtAuth {
|
||||||
|
token: string;
|
||||||
|
expire: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
expire: number;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SuccessCallback<T = any> = (data: T) => void;
|
||||||
|
export type FailureCallback = (message: string, code: number, url: string) => void;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {createApp} from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from "./router/index.ts";
|
||||||
|
// import axios from "axios";
|
||||||
|
import 'element-plus/dist/index.css';
|
||||||
|
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||||
|
|
||||||
|
// axios.defaults.baseURL = 'http://localhost:8080'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteMeta {
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'welcome',
|
||||||
|
component: () => import('../views/WelcomeView.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'welcome-login',
|
||||||
|
component: () => import('../views/welcome/LoginPage.vue'),
|
||||||
|
meta: { title: 'Welcome - login' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'register',
|
||||||
|
name: 'welcome-register',
|
||||||
|
component: () => import('../views/welcome/RegisterPage.vue'),
|
||||||
|
meta: { title: 'Welcome - register' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reset',
|
||||||
|
name: 'welcome-reset',
|
||||||
|
component: () => import('../views/welcome/ResetPage.vue'),
|
||||||
|
meta: { title: 'Welcome - reset' },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/home',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('../views/HomeView.vue'),
|
||||||
|
meta: { title: 'home' },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
const nearestTitle = to.meta.title;
|
||||||
|
|
||||||
|
if (nearestTitle) {
|
||||||
|
document.title = nearestTitle;
|
||||||
|
} else {
|
||||||
|
document.title = 'frontend - page';
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router
|
||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.css' {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.scss' {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { JwtAuth } from "../interfaces/ApiTypes.ts";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import router from "../router";
|
||||||
|
|
||||||
|
const authItemName = 'access_token';
|
||||||
|
|
||||||
|
function takeAccessToken(): string | null {
|
||||||
|
const str = localStorage.getItem(authItemName) || sessionStorage.getItem(authItemName);
|
||||||
|
if (!str) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authObject: JwtAuth = JSON.parse(str);
|
||||||
|
if (authObject.expire < Date.now()) {
|
||||||
|
deleteAccessToken();
|
||||||
|
ElMessage.warning('Your session has expired, please log in again.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return authObject.token;
|
||||||
|
} catch (e) {
|
||||||
|
deleteAccessToken();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeAccessToken(token: string, remember: boolean, expire: number): void {
|
||||||
|
const authObject: JwtAuth = {
|
||||||
|
token: token,
|
||||||
|
expire: expire
|
||||||
|
};
|
||||||
|
const authString = JSON.stringify(authObject);
|
||||||
|
if (remember) {
|
||||||
|
localStorage.setItem(authItemName, authString);
|
||||||
|
} else {
|
||||||
|
sessionStorage.setItem(authItemName, authString);
|
||||||
|
localStorage.removeItem(authItemName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAccessToken(): void {
|
||||||
|
localStorage.removeItem(authItemName);
|
||||||
|
sessionStorage.removeItem(authItemName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loginPageTokenInit(): void {
|
||||||
|
const storedToken = takeAccessToken();
|
||||||
|
if (storedToken) {
|
||||||
|
router.push('/home')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function homePageTokenInit(): void {
|
||||||
|
const storedToken = takeAccessToken();
|
||||||
|
if (!storedToken) {
|
||||||
|
router.push('/').then(() => {
|
||||||
|
ElMessage.warning('Have not logged in yet, please log in.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
takeAccessToken,
|
||||||
|
storeAccessToken,
|
||||||
|
deleteAccessToken,
|
||||||
|
loginPageTokenInit,
|
||||||
|
homePageTokenInit
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { logout } from "../api";
|
||||||
|
import { homePageTokenInit } from "../utils/JwtUtils";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
homePageTokenInit();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout(() => {
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center-container">
|
||||||
|
<el-button @click="handleLogout" type="primary">logout</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.center-container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { loginPageTokenInit } from "../utils/JwtUtils.ts";
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loginPageTokenInit()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="width: 100vw;height: 100vh;overflow: hidden;display: flex">
|
||||||
|
<div id="left-card">
|
||||||
|
<el-image style="width: 100%;height: 100%" fit="cover"
|
||||||
|
src="src/assets/welcome-image.png"/>
|
||||||
|
</div>
|
||||||
|
<div id="welcome-title">
|
||||||
|
<div>welcome here.</div>
|
||||||
|
</div>
|
||||||
|
<div id="right-card">
|
||||||
|
<router-view v-slot="{Component}">
|
||||||
|
<transition name="el-fade-in-linear" mode="out-in">
|
||||||
|
<component :is="Component"/>
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#left-card {
|
||||||
|
flex: 1;
|
||||||
|
background-color: black
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
#left-card {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#right-card {
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 513px) {
|
||||||
|
#right-card {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#welcome-title {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 120px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 0 10px black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {reactive, ref} from "vue";
|
||||||
|
import {Lock, User} from "@element-plus/icons-vue";
|
||||||
|
import {login} from "../../api";
|
||||||
|
import router from "../../router/index.ts";
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
remember: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRule = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: 'please input username or email', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: 'please input password', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function userLogin() {
|
||||||
|
formRef.value.validate((valid:boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
login(form.username, form.password, form.remember, () => router.push('/home'))
|
||||||
|
} else {
|
||||||
|
console.log('error submit!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="text-align: center;margin: 32px">
|
||||||
|
<div style="margin-top: 169px">
|
||||||
|
<div style="font-size: 28px;font-weight: bold">Login</div>
|
||||||
|
<div style="font-size: 14px;color: slategray">Please enter your username (or your email) and your password</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 32px">
|
||||||
|
<el-form model="form" :rules="formRule" ref="formRef">
|
||||||
|
<el-form-item>
|
||||||
|
<el-input v-model="form.username" maxlength="63" type="text" placeholder="username or email">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<User/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-input v-model="form.password" maxlength="20" type="password" placeholder="password">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Lock/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12" style="text-align: left">
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="form.remember" label="remember me"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12" style="text-align: right">
|
||||||
|
<el-link @click="router.push('/reset')">
|
||||||
|
forget password?
|
||||||
|
</el-link>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 24px">
|
||||||
|
<el-button @click="userLogin" style="width: 280px" type="primary">
|
||||||
|
Sign in
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-divider>
|
||||||
|
<span style="font-size: 10px;color: slategray">have no account?</span>
|
||||||
|
</el-divider>
|
||||||
|
<div style="margin-top: 16px">
|
||||||
|
<el-button @click="router.push('/register')" style="width: 280px" type="default">
|
||||||
|
Sign up
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {reactive, ref} from "vue";
|
||||||
|
import {Bell, Lock, Message, User} from "@element-plus/icons-vue";
|
||||||
|
import router from "../../router/index.js";
|
||||||
|
import {get, post} from "../../api";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
|
||||||
|
const coldTime = ref(0);
|
||||||
|
const formRef = ref()
|
||||||
|
const isCounting = ref(false)
|
||||||
|
let timer: number | null = null;
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
passwordRepeat: '',
|
||||||
|
email: '',
|
||||||
|
code: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const startColdTime = (): void => {
|
||||||
|
isCounting.value = true;
|
||||||
|
coldTime.value = 60;
|
||||||
|
|
||||||
|
timer = setInterval(() => {
|
||||||
|
coldTime.value--;
|
||||||
|
if (coldTime.value <= 0) {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
timer = null;
|
||||||
|
isCounting.value = false;
|
||||||
|
coldTime.value = 0;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateUsername = (_rule: any, value: string, callback: (err?: Error) => void): void => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error("Please input username"));
|
||||||
|
} else if (!/^[a-zA-Z0-9\u4e00-\u9fa5]+$/.test(value)) {
|
||||||
|
callback(new Error("Only letters, numbers, or Chinese characters"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validatePasswordRepeat = (_rule: any, value: string, callback: (err?: Error) => void): void => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error("Please repeat password"));
|
||||||
|
} else if (value !== form.password) {
|
||||||
|
callback(new Error("Passwords do not match"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const rule = {
|
||||||
|
username: [
|
||||||
|
{validator: validateUsername, trigger: ['blur', 'change']},
|
||||||
|
{min: 4, max: 16, message: 'Length should be 4 to 16', trigger: 'blur'}
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{required: true, message: 'Please input password', trigger: 'blur'},
|
||||||
|
{min: 6, max: 20, message: 'Length should be 6 to 20', trigger: ['blur', 'change']}
|
||||||
|
],
|
||||||
|
passwordRepeat: [
|
||||||
|
{validator: validatePasswordRepeat, trigger: ['blur', 'change']},
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{required: true, message: 'Please input email', trigger: 'blur'},
|
||||||
|
{type: 'email', message: 'Please input a valid email', trigger: ['blur', 'change']}
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{required: true, message: 'Please input verification code', trigger: 'blur'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function askCode(): void {
|
||||||
|
get(
|
||||||
|
`/api/auth/ask-code?email=${form.email}&type=register`,
|
||||||
|
(response: unknown) => {
|
||||||
|
coldTime.value = 60;
|
||||||
|
startColdTime();
|
||||||
|
ElMessage.success(`Verification code sent to ${form.email}`);
|
||||||
|
console.log(response);
|
||||||
|
},
|
||||||
|
(err: unknown) => {
|
||||||
|
coldTime.value = 0;
|
||||||
|
ElMessage.error("Email address is invalid or already registered");
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function register(): void {
|
||||||
|
formRef.value?.validate((valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
post(
|
||||||
|
"/api/auth/register",
|
||||||
|
{...form},
|
||||||
|
() => {
|
||||||
|
ElMessage.success("Registration successfully!");
|
||||||
|
router.push("/");
|
||||||
|
},
|
||||||
|
(err: unknown) => {
|
||||||
|
ElMessage.error("Please check your information and try again.");
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div style="text-align: center;margin: 32px">
|
||||||
|
<div style="margin-top: 169px; margin-bottom: 50px">
|
||||||
|
<div style="font-size: 28px;font-weight: bold">Register</div>
|
||||||
|
<div style="font-size: 14px;color: slategray">welcome to join us!</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin: 32px;">
|
||||||
|
<el-form :model="form" :rules="rule" ref="formRef">
|
||||||
|
<el-form-item prop="username">
|
||||||
|
<el-input v-model="form.username" maxlength="16" type="text" placeholder="Username">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<User/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="form.password" maxlength="20" type="password" placeholder="Password">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Lock/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="passwordRepeat">
|
||||||
|
<el-input v-model="form.passwordRepeat" maxlength="20" type="password" placeholder="Repeat password">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Lock/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="email">
|
||||||
|
<el-input v-model="form.email" type="text" placeholder="Email">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Message/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="code">
|
||||||
|
<el-row justify="space-between">
|
||||||
|
<el-col :span="11">
|
||||||
|
<el-input v-model="form.code" maxlength="6" type="text" placeholder="Verify code">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Bell/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="10">
|
||||||
|
<el-button @click="askCode" :disabled="coldTime" type="success" plain style="width: 100%">
|
||||||
|
{{ coldTime > 0 ? `Wait for ${coldTime}s` : 'Get Code' }}
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 24px">
|
||||||
|
<el-button @click="register" style="width: 280px" type="primary">Register</el-button>
|
||||||
|
</div>
|
||||||
|
<el-divider>
|
||||||
|
<span style="font-size: 10px;color: slategray">already have a account?</span>
|
||||||
|
</el-divider>
|
||||||
|
<div style="margin-top: 16px">
|
||||||
|
<el-button @click="router.push('/')" style="width: 280px" type="default">
|
||||||
|
Sign in
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {reactive, ref} from "vue";
|
||||||
|
import {Bell, Lock, Message} from "@element-plus/icons-vue";
|
||||||
|
import {get, post} from "../../api";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import router from "../../router/index.js";
|
||||||
|
|
||||||
|
const activeStep = ref(0);
|
||||||
|
const coldTime = ref(0);
|
||||||
|
const formRef = ref()
|
||||||
|
const isCounting = ref(false)
|
||||||
|
let timer: number | null = null;
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
password: '',
|
||||||
|
passwordRepeat: '',
|
||||||
|
email: '',
|
||||||
|
code: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const startColdTime = () => {
|
||||||
|
isCounting.value = true
|
||||||
|
coldTime.value = 60
|
||||||
|
|
||||||
|
timer = setInterval(() => {
|
||||||
|
coldTime.value--
|
||||||
|
if (coldTime.value <= 0) {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
timer = null
|
||||||
|
isCounting.value = false
|
||||||
|
coldTime.value = 0
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatePasswordRepeat = (_rule: any, value: string, callback: (err?: Error) => void): void => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error("Please repeat password"));
|
||||||
|
} else if (value !== form.password) {
|
||||||
|
callback(new Error("Passwords do not match"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rule = {
|
||||||
|
password: [
|
||||||
|
{required: true, message: 'Please input password', trigger: 'blur'},
|
||||||
|
{min: 6, max: 20, message: 'Length should be 6 to 20', trigger: ['blur', 'change']}
|
||||||
|
],
|
||||||
|
passwordRepeat: [
|
||||||
|
{validator: validatePasswordRepeat, trigger: ['blur', 'change']},
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{required: true, message: 'Please input email', trigger: 'blur'},
|
||||||
|
{type: 'email', message: 'Please input a valid email', trigger: ['blur', 'change']}
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{required: true, message: 'Please input verification code', trigger: 'blur'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function askCode() {
|
||||||
|
get(
|
||||||
|
`/api/auth/ask-code?email=${form.email}&type=reset`,
|
||||||
|
(response) => {
|
||||||
|
coldTime.value = 60;
|
||||||
|
startColdTime();
|
||||||
|
ElMessage.success(`Verification code sent to ${form.email}`);
|
||||||
|
console.log(response);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
coldTime.value = 0;
|
||||||
|
ElMessage.error('Email address is invalid or already registered');
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPassword() {
|
||||||
|
formRef.value.validate((valid: boolean) => {
|
||||||
|
if (valid) {
|
||||||
|
post(
|
||||||
|
'/api/auth/reset',
|
||||||
|
{...form},
|
||||||
|
() => {
|
||||||
|
ElMessage.success('Reset successfully!')
|
||||||
|
router.push('/')
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
ElMessage.error('Please check your information and try again.')
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="text-align: center;margin: 32px">
|
||||||
|
<div class="title">
|
||||||
|
Reset Password
|
||||||
|
</div>
|
||||||
|
<div style="margin: 32px">
|
||||||
|
<el-steps :active="activeStep" finish-status="success" align-center>
|
||||||
|
<el-step title="Verify Email"/>
|
||||||
|
<el-step title="Reset Password"/>
|
||||||
|
</el-steps>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 32px" v-if="activeStep === 0">
|
||||||
|
<div class="description">
|
||||||
|
Please enter your email address.
|
||||||
|
</div>
|
||||||
|
<el-form :model="form" :rules="rule" ref="formRef">
|
||||||
|
<el-form-item prop="email">
|
||||||
|
<el-input v-model="form.email" type="text" placeholder="Email">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Message/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="code">
|
||||||
|
<el-row justify="space-between">
|
||||||
|
<el-col :span="11">
|
||||||
|
<el-input v-model="form.code" maxlength="6" type="text" placeholder="Verify code">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Bell/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="10">
|
||||||
|
<el-button @click="askCode" :disabled="coldTime" type="success" plain style="width: 100%">
|
||||||
|
{{ coldTime > 0 ? `Wait for ${coldTime}s` : 'Get Code' }}
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 32px" v-if="activeStep === 1">
|
||||||
|
<div class="description">
|
||||||
|
Please enter new password.
|
||||||
|
</div>
|
||||||
|
<el-form :model="form" :rules="rule" ref="formRef">
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="form.password" maxlength="20" type="password" placeholder="Password">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Lock/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="passwordRepeat">
|
||||||
|
<el-input v-model="form.passwordRepeat" maxlength="20" type="password" placeholder="Repeat password">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon>
|
||||||
|
<Lock/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 24px" v-if="activeStep === 0">
|
||||||
|
<el-button @click="activeStep++" :disabled="!(form.code.toString().length === 6)" style="width: 280px"
|
||||||
|
type="primary">
|
||||||
|
Go Next Step
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 24px" v-if="activeStep === 1">
|
||||||
|
<el-button @click="resetPassword" style="width: 280px" type="primary">
|
||||||
|
Reset
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider>
|
||||||
|
<span style="font-size: 10px;color: slategray">Wanna go back?</span>
|
||||||
|
</el-divider>
|
||||||
|
|
||||||
|
<div style="margin-top: 24px" v-if="activeStep === 0">
|
||||||
|
<el-button @click="router.push('/')" style="width: 280px" type="default">
|
||||||
|
Go Back
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 24px" v-if="activeStep === 1">
|
||||||
|
<el-button @click="activeStep--" style="width: 280px" type="default">
|
||||||
|
Go Last Step
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<
|
||||||
|
<style scoped>
|
||||||
|
.title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 169px
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: slategray;
|
||||||
|
margin-bottom: 32px
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: "./",
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
allowedHosts: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user