email/pass login with sqlite db
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ target
|
|||||||
/tournamentfiles
|
/tournamentfiles
|
||||||
*.iml
|
*.iml
|
||||||
*~
|
*~
|
||||||
|
pairgoth.db
|
||||||
|
9
create-user.sh
Executable file
9
create-user.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
read -rp 'email: ' EMAIL
|
||||||
|
read -rp 'password: ' PASSWORD
|
||||||
|
|
||||||
|
ENCPASS=$(echo -n $PASSWORD | sha256sum)
|
||||||
|
ENCPASS=${ENCPASS% -}
|
||||||
|
|
||||||
|
sqlite3 pairgoth.db "INSERT INTO cred (email, password) VALUES ('$EMAIL', '$ENCPASS')"
|
@@ -286,6 +286,12 @@
|
|||||||
<artifactId>lucene-queryparser</artifactId>
|
<artifactId>lucene-queryparser</artifactId>
|
||||||
<version>${lucene.version}</version>
|
<version>${lucene.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- sqlite -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xerial</groupId>
|
||||||
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
<version>3.45.1.0</version>
|
||||||
|
</dependency>
|
||||||
<!-- tests -->
|
<!-- tests -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
@@ -0,0 +1,35 @@
|
|||||||
|
package org.jeudego.pairgoth.util
|
||||||
|
|
||||||
|
import com.republicate.kson.Json
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.sql.DriverManager
|
||||||
|
|
||||||
|
object CredentialsChecker {
|
||||||
|
private const val CREDENTIALS_DB = "pairgoth.db"
|
||||||
|
private val hasher = MessageDigest.getInstance("SHA-256")
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
fun check(email: String, password: String): String? {
|
||||||
|
initDatabase()
|
||||||
|
val sha256 = hasher.digest(password.toByteArray(StandardCharsets.UTF_8)).toHexString()
|
||||||
|
DriverManager.getConnection("jdbc:sqlite:$CREDENTIALS_DB").use { conn ->
|
||||||
|
val rs =
|
||||||
|
conn.prepareStatement("SELECT 1 FROM cred WHERE email = ? AND password = ?").apply {
|
||||||
|
setString(1, email)
|
||||||
|
setString(2, password)
|
||||||
|
}.executeQuery()
|
||||||
|
return if (rs.next()) email else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun initDatabase() {
|
||||||
|
if (!File(CREDENTIALS_DB).exists()) {
|
||||||
|
DriverManager.getConnection("jdbc:sqlite:$CREDENTIALS_DB").use { conn ->
|
||||||
|
conn.createStatement().executeUpdate("CREATE TABLE cred (email VARCHAR(200) UNIQUE NOT NULL, password VARCHAR(200) NOT NULL)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
package org.jeudego.pairgoth.web
|
package org.jeudego.pairgoth.web
|
||||||
|
|
||||||
import com.republicate.kson.Json
|
import com.republicate.kson.Json
|
||||||
|
import org.jeudego.pairgoth.util.CredentialsChecker
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import javax.servlet.http.HttpServlet
|
import javax.servlet.http.HttpServlet
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
@@ -17,7 +18,7 @@ class LoginServlet: HttpServlet() {
|
|||||||
val payload = Json.Companion.parse(req.reader.readText())?.asObject() ?: throw Error("null json")
|
val payload = Json.Companion.parse(req.reader.readText())?.asObject() ?: throw Error("null json")
|
||||||
val user = when (WebappManager.getProperty("auth")) {
|
val user = when (WebappManager.getProperty("auth")) {
|
||||||
"sesame" -> checkSesame(payload)
|
"sesame" -> checkSesame(payload)
|
||||||
else -> null
|
else -> checkLoginPass(payload)
|
||||||
} ?: throw Error("authentication failed")
|
} ?: throw Error("authentication failed")
|
||||||
req.session.setAttribute("logged", user)
|
req.session.setAttribute("logged", user)
|
||||||
val ret = Json.Object("status" to "ok")
|
val ret = Json.Object("status" to "ok")
|
||||||
@@ -25,7 +26,9 @@ class LoginServlet: HttpServlet() {
|
|||||||
resp.writer.println(ret.toString())
|
resp.writer.println(ret.toString())
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logger.error("exception while logging in", t)
|
logger.error("exception while logging in", t)
|
||||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST)
|
resp.contentType = "application/json"
|
||||||
|
resp.status = HttpServletResponse.SC_BAD_REQUEST
|
||||||
|
resp.writer.println(errorJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,8 +37,15 @@ class LoginServlet: HttpServlet() {
|
|||||||
return if (payload.getString("sesame")?.equals(expected) == true) true else null
|
return if (payload.getString("sesame")?.equals(expected) == true) true else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkLoginPass(payload: Json.Object): String? {
|
||||||
|
return CredentialsChecker.check(
|
||||||
|
payload.getString("email") ?: throw Error("Missing login field"),
|
||||||
|
payload.getString("password") ?: throw Error("missing password field"))
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
|
fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
|
||||||
val logger = LoggerFactory.getLogger("login")
|
val logger = LoggerFactory.getLogger("login")
|
||||||
|
private val errorJson = "{ \"status\": \"error\", \"error\": \"authentication failed\"}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -215,6 +215,10 @@
|
|||||||
width: initial;
|
width: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.form .centered.inline.fields {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.ui.accordion .content {
|
.ui.accordion .content {
|
||||||
display: block;
|
display: block;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
|
@@ -162,3 +162,11 @@ white blanc
|
|||||||
White Blanc
|
White Blanc
|
||||||
white vs. black blanc vs. Noir
|
white vs. black blanc vs. Noir
|
||||||
confirmed. confirmé(s).
|
confirmed. confirmé(s).
|
||||||
|
Note that login to this instance is reserved to French federation actors plus several external people at our discretion. Send us La connexion à cette instance est réservée aux acteurs de la FFG et à quelques personnes extérieures, à notre discrétion. Envoyez-nous
|
||||||
|
an email un email
|
||||||
|
to request an access. pour demander un accès.
|
||||||
|
(not yet available) (pas encore disponible)
|
||||||
|
Log in using Se connecter avec
|
||||||
|
(reserved to FFG actors) (réservé aux acteurs FFG)
|
||||||
|
Log in using an email Se connecter avec un email
|
||||||
|
password mot de passe
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<p><b>Stay in the browser</b>: If you prefer convenience, you can simply use the <span class="logo">pairgoth</span> instance graciously hosted by the French Go Federation.</p>
|
<p><b>Stay in the browser</b>: If you prefer convenience, you can simply use the <span class="logo">pairgoth</span> instance graciously hosted by the French Go Federation.</p>
|
||||||
|
<p>Note that login to this instance is reserved to French federation actors plus several external people at our discretion. Send us <a href="mailto:pairgothjeudego.org">an email</a> to request an access.</p>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<a class="nobreak" href="/login">Launch <span class="logo">pairgoth</span></a>
|
<a class="nobreak" href="/login">Launch <span class="logo">pairgoth</span></a>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
@@ -114,7 +114,7 @@ HTMLFormElement.prototype.val = function(name, value) {
|
|||||||
let tag = ctl.tagName;
|
let tag = ctl.tagName;
|
||||||
let type = tag === 'INPUT' ? ctl.attr('type') : undefined;
|
let type = tag === 'INPUT' ? ctl.attr('type') : undefined;
|
||||||
if (
|
if (
|
||||||
(tag === 'INPUT' && ['text', 'number', 'hidden'].includes(ctl.attr('type'))) ||
|
(tag === 'INPUT' && ['text', 'number', 'hidden', 'password'].includes(ctl.attr('type'))) ||
|
||||||
tag === 'SELECT'
|
tag === 'SELECT'
|
||||||
) {
|
) {
|
||||||
if (hasValue) {
|
if (hasValue) {
|
||||||
|
@@ -30,30 +30,71 @@
|
|||||||
#elseif($auth == 'oauth')
|
#elseif($auth == 'oauth')
|
||||||
|
|
||||||
<div id="login" class="section">
|
<div id="login" class="section">
|
||||||
<div>Log in using</div>
|
<div id="oauth-buttons" class="roundbox">
|
||||||
<div id="oauth-buttons">
|
|
||||||
#foreach($provider in $oauthProviders)
|
#foreach($provider in $oauthProviders)
|
||||||
<div>
|
<form>
|
||||||
<button id="login-$provider" class="ui floating basic button">$provider</button>
|
<label>Log in using</label>
|
||||||
</div>
|
<button id="login-$provider" type="button" class="ui green floating button">$provider</button>
|
||||||
|
#if($provider == 'ffg')
|
||||||
|
(reserved to FFG actors)
|
||||||
|
#end
|
||||||
|
</form>
|
||||||
#end
|
#end
|
||||||
</div>
|
</div>
|
||||||
|
<div class="roundbox">
|
||||||
|
Log in using an email
|
||||||
|
<form id="login-form" class="ui form">
|
||||||
|
<div class="centered inline fields">
|
||||||
|
<div class="field">
|
||||||
|
<input name="email" type="text" placeholder="email"/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<input name="password" type="password" placeholder="password"/>
|
||||||
|
</div>
|
||||||
|
<button id="login-email" type="button" class="ui green floating button">Log in</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
async function digestMessage(message) {
|
||||||
|
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||||
|
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
|
||||||
onLoad(()=> {
|
onLoad(()=> {
|
||||||
#foreach($provider in $oauthProviders)
|
#foreach($provider in $oauthProviders)
|
||||||
let buttonId = '#login-$provider';
|
let buttonId = '#login-$provider';
|
||||||
let loginURL= '$application.getAttribute("${provider}Provider").getLoginURL($session.id)';
|
let loginURL= '$application.getAttribute("${provider}Provider").getLoginURL($session.id)';
|
||||||
// #[[
|
// #[[
|
||||||
console.log(`buttonId = ${buttonId}`);
|
|
||||||
console.log(`loginURL = ${loginURL}`);
|
|
||||||
$(buttonId).on('click', e => {
|
$(buttonId).on('click', e => {
|
||||||
document.location.href = loginURL;
|
document.location.href = loginURL;
|
||||||
});
|
});
|
||||||
// ]]#
|
// ]]#
|
||||||
#end
|
#end
|
||||||
|
// #[[
|
||||||
|
$('#login-email').on('click', e => {
|
||||||
|
let form = $('#login-form')[0]
|
||||||
|
let password = form.val('password');
|
||||||
|
digestMessage(password).then(enc => {
|
||||||
|
let payload = {
|
||||||
|
'email': form.val('email'),
|
||||||
|
'password': enc
|
||||||
|
}
|
||||||
|
api.postJson('login', payload)
|
||||||
|
.then(resp => {
|
||||||
|
if (resp !== 'error' && resp.status === 'ok') {
|
||||||
|
document.location.href = '/index'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// ]]#
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user