commit 21d82c8b7c83fd7e70caa8f05873d456a6e7e4b6
parent 97188539648fba10f120be9ef12ae3cb182aa1e2
Author: seestem <seestem@noreply.codeberg.org>
Date: Wed, 31 Aug 2022 16:55:31 +0200
Merge pull request 'seting up client (cyrto)' (#3) from dev into root
Reviewed-on: https://codeberg.org/seestem/cyrtophora/pulls/3
Diffstat:
22 files changed, 608 insertions(+), 36 deletions(-)
diff --git a/cyrto/.gitignore b/cyrto/.gitignore
@@ -0,0 +1,10 @@
+# Files and directories created by pub.
+.dart_tool/
+.packages
+
+# Conventional directory for build outputs.
+build/
+
+# Omit committing pubspec.lock for library packages; see
+# https://dart.dev/guides/libraries/private-files#pubspeclock.
+pubspec.lock
diff --git a/cyrto/CHANGELOG.md b/cyrto/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+- Initial version.
diff --git a/cyrto/README.md b/cyrto/README.md
@@ -0,0 +1 @@
+# cyrtophora client
diff --git a/cyrto/analysis_options.yaml b/cyrto/analysis_options.yaml
@@ -0,0 +1,30 @@
+# This file configures the static analysis results for your project (errors,
+# warnings, and lints).
+#
+# This enables the 'recommended' set of lints from `package:lints`.
+# This set helps identify many issues that may lead to problems when running
+# or consuming Dart code, and enforces writing Dart using a single, idiomatic
+# style and format.
+#
+# If you want a smaller set of lints you can change this to specify
+# 'package:lints/core.yaml'. These are just the most critical lints
+# (the recommended set includes the core lints).
+# The core lints are also what is used by pub.dev for scoring packages.
+
+include: package:lints/recommended.yaml
+
+# Uncomment the following section to specify additional rules.
+
+# linter:
+# rules:
+# - camel_case_types
+
+# analyzer:
+# exclude:
+# - path/to/excluded/files/**
+
+# For more information about the core and recommended set of lints, see
+# https://dart.dev/go/core-lints
+
+# For additional information about configuring this file, see
+# https://dart.dev/guides/language/analysis-options
diff --git a/cyrto/example/cyrtophora_example.dart b/cyrto/example/cyrtophora_example.dart
@@ -0,0 +1,6 @@
+import 'package:cyrtophora/cyrtophora.dart';
+
+void main() {
+ var awesome = Awesome();
+ print('awesome: ${awesome.isAwesome}');
+}
diff --git a/cyrto/lib/cyrtophora.dart b/cyrto/lib/cyrtophora.dart
@@ -0,0 +1,9 @@
+/// cyrtophora client
+
+library cyrtophora;
+
+export 'src/utils.dart';
+export 'src/validate.dart';
+export 'src/data.dart';
+export 'src/api.dart';
+export 'src/account.dart';
diff --git a/cyrto/lib/src/account.dart b/cyrto/lib/src/account.dart
@@ -0,0 +1,10 @@
+/// Account Public Data
+class PublicAccount {
+ String username;
+ PublicAccount(this.username);
+
+ /// Generate input from JSON
+ factory PublicAccount.fromJson(dynamic json) {
+ return PublicAccount(json['username'] as String);
+ }
+}
diff --git a/cyrto/lib/src/api.dart b/cyrto/lib/src/api.dart
@@ -0,0 +1,30 @@
+import 'package:http/http.dart' as http;
+import 'package:cyrtophora/src/data.dart';
+import 'dart:convert';
+import 'package:cyrtophora/src/account.dart';
+
+/// Accounts API
+class CyrtophoraAPI {
+ CyrtophoraAPI(this.api);
+ final String api;
+ final Map<String, String> jsonHeaders = {
+ 'Content-Type': 'application/json; charset=UTF-8',
+ };
+
+ /// Create new account
+ Future<PublicAccount> create(AccountRegistration input) async {
+ final response = await http.post(
+ Uri.parse("$api/accounts"),
+ headers: jsonHeaders,
+ body: jsonEncode(input.toJson()),
+ );
+
+ if (response.statusCode == 400) {
+ throw Exception('Invalid Input: ${response.body}');
+ } else if (response.statusCode == 201) {
+ return PublicAccount.fromJson(jsonDecode(response.body));
+ } else {
+ throw Exception('Server side error: ${response.body}');
+ }
+ }
+}
diff --git a/cyrto/lib/src/data.dart b/cyrto/lib/src/data.dart
@@ -0,0 +1,64 @@
+import "package:cyrtophora/cyrtophora.dart";
+
+/// Account registration input
+class AccountRegistration {
+ String username;
+ String email;
+ String password;
+ String retyped_password;
+
+ AccountRegistration(
+ this.username, this.email, this.password, this.retyped_password);
+
+ /// Generate input from JSON
+ factory AccountRegistration.fromJson(dynamic json) {
+ return AccountRegistration(
+ json['username'] as String,
+ json['email'] as String,
+ json['password'] as String,
+ json['retyped_password'] as String);
+ }
+
+ /// Serialize object to JSON
+ Map<String, String> toJson() {
+ return {
+ 'username': username,
+ 'email': email,
+ 'password': password,
+ 'retyped_password': retyped_password
+ };
+ }
+
+ /// Validate input and create
+ bool isValid() {
+ // Check if password and retyped password match
+ if (password != retyped_password) {
+ throw Exception('InvalidPasswordException\n\n- passwords do not match');
+ }
+ // Validate username
+ if (!Validate.username(username)) {
+ throw Exception(
+ 'InvalidUsernameException\n\n- Username cannot be empty\n- Username cannot be more than 15 characters long\n- Username cannot contain symbols expcept one _');
+ }
+
+ // Validate fullname
+ // if (!Validate.fullname(fullname)) {
+ // //throw InvalidNameException;
+ // throw Exception(
+ // 'InvalidNameException\n\n- Name cannot be empty\n- Name cannot contain more than 70 characters');
+ // }
+
+ // Validate password
+ if (!Validate.password(password)) {
+ throw Exception(
+ 'InvalidPasswordException\n\n- Password length should be 10 chartcers or more');
+ }
+
+ // Validate email
+ if (!Validate.email(email)) {
+ throw Exception('InvalidEmailException\n\n- Invalid email');
+ }
+
+ return true;
+ }
+}
diff --git a/cyrto/lib/src/utils.dart b/cyrto/lib/src/utils.dart
@@ -0,0 +1,75 @@
+/// utilities -- cyrtophora utility functions
+
+/// Check if a character is alphanumeric
+bool isAlphanumeric(String char) {
+ final alphanumeric = [
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "f",
+ "g",
+ "h",
+ "i",
+ "j",
+ "k",
+ "l",
+ "m",
+ "n",
+ "o",
+ "p",
+ "q",
+ "r",
+ "s",
+ "t",
+ "u",
+ "v",
+ "w",
+ "x",
+ "y",
+ "z",
+ "A",
+ "B",
+ "C",
+ "D",
+ "E",
+ "F",
+ "G",
+ "H",
+ "I",
+ "J",
+ "K",
+ "L",
+ "M",
+ "N",
+ "O",
+ "P",
+ "Q",
+ "R",
+ "S",
+ "T",
+ "U",
+ "V",
+ "W",
+ "X",
+ "Y",
+ "Z",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ "0"
+ ];
+
+ if (alphanumeric.contains(char)) {
+ return true;
+ } else {
+ return false;
+ }
+}
diff --git a/cyrto/lib/src/validate.dart b/cyrto/lib/src/validate.dart
@@ -0,0 +1,80 @@
+import "package:cyrtophora/cyrtophora.dart";
+
+class Validate {
+ /// Validate username
+ static bool username(String username) {
+ // Username cannot be empty
+ if (username.isEmpty) {
+ return false;
+ }
+
+ // Username length cannot be greater than 15 characters
+ if (username.length > 15) {
+ return false;
+ }
+
+ final chars = username.split("");
+ var underscoreCount = 0;
+
+ for (var i = 0; i < chars.length; i++) {
+ // Username cannot start with a underscore (_)
+ if (i == 0 && chars[i] == '_') {
+ return false;
+ }
+
+ // Username can only contain letters, numbers, one dot and one underscore
+ if (chars[i] != '_') {
+ if (!isAlphanumeric(chars[i])) {
+ return false;
+ }
+ } else {
+ // Count underscores because name can have only one [ _ ] underscore
+ underscoreCount += 1;
+ }
+ }
+ // Username can have only one [ _ ] underscore
+ if (underscoreCount > 1) {
+ return false;
+ }
+ return true;
+ }
+
+ /// Validate fullname if it is provided
+ static bool fullname(String fullname) {
+ // Full Name cannot be empty
+ if (fullname.isEmpty) {
+ return false;
+ }
+
+ // Full Name cannot be more than 70 characters
+ if (fullname.length > 70) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// Validate password
+ static bool password(String password) {
+ // Password should be at least 10 characters long
+ if (password.length < 10) {
+ return false;
+ }
+ return true;
+ }
+
+ // TODO: better email verification
+ /// Validate email address
+ static bool email(String email) {
+ // Email address cannot be empty
+ if (email.isEmpty) {
+ return false;
+ }
+
+ // Email address should contain @ and . symbols
+ if (!email.contains('@') || !email.contains('.')) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/cyrto/pubspec.yaml b/cyrto/pubspec.yaml
@@ -0,0 +1,14 @@
+name: cyrtophora
+description: A starting point for Dart libraries or applications.
+version: 1.0.0
+# homepage: https://www.example.com
+
+environment:
+ sdk: '>=2.17.0-227.0.dev <3.0.0'
+
+dependencies:
+ http: ^0.13.5
+
+dev_dependencies:
+ lints: ^2.0.0
+ test: ^1.16.0
diff --git a/cyrto/test/cyrtophora_test.dart b/cyrto/test/cyrtophora_test.dart
@@ -0,0 +1,5 @@
+import 'package:test/test.dart';
+
+void main() {
+ group('Tests for cyrtophora utilities', () {});
+}
diff --git a/cyrto/test/utils_test.dart b/cyrto/test/utils_test.dart
@@ -0,0 +1,100 @@
+import 'package:cyrtophora/cyrtophora.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Tests for cyrtophora utilities', () {
+ test('Test isAlphaNumeric function', () {
+ expect(isAlphanumeric("a"), isTrue);
+ expect(isAlphanumeric("b"), isTrue);
+ expect(isAlphanumeric("c"), isTrue);
+ expect(isAlphanumeric("d"), isTrue);
+ expect(isAlphanumeric("e"), isTrue);
+ expect(isAlphanumeric("f"), isTrue);
+ expect(isAlphanumeric("g"), isTrue);
+ expect(isAlphanumeric("h"), isTrue);
+ expect(isAlphanumeric("i"), isTrue);
+ expect(isAlphanumeric("j"), isTrue);
+ expect(isAlphanumeric("k"), isTrue);
+ expect(isAlphanumeric("l"), isTrue);
+ expect(isAlphanumeric("m"), isTrue);
+ expect(isAlphanumeric("n"), isTrue);
+ expect(isAlphanumeric("o"), isTrue);
+ expect(isAlphanumeric("p"), isTrue);
+ expect(isAlphanumeric("q"), isTrue);
+ expect(isAlphanumeric("r"), isTrue);
+ expect(isAlphanumeric("s"), isTrue);
+ expect(isAlphanumeric("t"), isTrue);
+ expect(isAlphanumeric("u"), isTrue);
+ expect(isAlphanumeric("v"), isTrue);
+ expect(isAlphanumeric("w"), isTrue);
+ expect(isAlphanumeric("x"), isTrue);
+ expect(isAlphanumeric("y"), isTrue);
+ expect(isAlphanumeric("z"), isTrue);
+ expect(isAlphanumeric("A"), isTrue);
+ expect(isAlphanumeric("B"), isTrue);
+ expect(isAlphanumeric("C"), isTrue);
+ expect(isAlphanumeric("D"), isTrue);
+ expect(isAlphanumeric("E"), isTrue);
+ expect(isAlphanumeric("F"), isTrue);
+ expect(isAlphanumeric("G"), isTrue);
+ expect(isAlphanumeric("H"), isTrue);
+ expect(isAlphanumeric("I"), isTrue);
+ expect(isAlphanumeric("J"), isTrue);
+ expect(isAlphanumeric("K"), isTrue);
+ expect(isAlphanumeric("L"), isTrue);
+ expect(isAlphanumeric("M"), isTrue);
+ expect(isAlphanumeric("N"), isTrue);
+ expect(isAlphanumeric("O"), isTrue);
+ expect(isAlphanumeric("P"), isTrue);
+ expect(isAlphanumeric("Q"), isTrue);
+ expect(isAlphanumeric("R"), isTrue);
+ expect(isAlphanumeric("S"), isTrue);
+ expect(isAlphanumeric("T"), isTrue);
+ expect(isAlphanumeric("U"), isTrue);
+ expect(isAlphanumeric("V"), isTrue);
+ expect(isAlphanumeric("W"), isTrue);
+ expect(isAlphanumeric("X"), isTrue);
+ expect(isAlphanumeric("Y"), isTrue);
+ expect(isAlphanumeric("Z"), isTrue);
+ expect(isAlphanumeric("1"), isTrue);
+ expect(isAlphanumeric("2"), isTrue);
+ expect(isAlphanumeric("3"), isTrue);
+ expect(isAlphanumeric("4"), isTrue);
+ expect(isAlphanumeric("5"), isTrue);
+ expect(isAlphanumeric("6"), isTrue);
+ expect(isAlphanumeric("7"), isTrue);
+ expect(isAlphanumeric("8"), isTrue);
+ expect(isAlphanumeric("9"), isTrue);
+ expect(isAlphanumeric("0"), isTrue);
+
+ expect(isAlphanumeric("!"), isFalse);
+ expect(isAlphanumeric("@"), isFalse);
+ expect(isAlphanumeric("#"), isFalse);
+ expect(isAlphanumeric("\$"), isFalse);
+ expect(isAlphanumeric("%"), isFalse);
+ expect(isAlphanumeric("^"), isFalse);
+ expect(isAlphanumeric("&"), isFalse);
+ expect(isAlphanumeric("*"), isFalse);
+ expect(isAlphanumeric("("), isFalse);
+ expect(isAlphanumeric(")"), isFalse);
+ expect(isAlphanumeric("-"), isFalse);
+ expect(isAlphanumeric("="), isFalse);
+ expect(isAlphanumeric("+"), isFalse);
+ expect(isAlphanumeric("\\"), isFalse);
+ expect(isAlphanumeric("]"), isFalse);
+ expect(isAlphanumeric("}"), isFalse);
+ expect(isAlphanumeric("["), isFalse);
+ expect(isAlphanumeric("{"), isFalse);
+ expect(isAlphanumeric(":"), isFalse);
+ expect(isAlphanumeric(";"), isFalse);
+ expect(isAlphanumeric("'"), isFalse);
+ expect(isAlphanumeric("\""), isFalse);
+ expect(isAlphanumeric("<"), isFalse);
+ expect(isAlphanumeric(">"), isFalse);
+ expect(isAlphanumeric("/"), isFalse);
+ expect(isAlphanumeric("?"), isFalse);
+ expect(isAlphanumeric("."), isFalse);
+ expect(isAlphanumeric(","), isFalse);
+ });
+ });
+}
diff --git a/cyrto/test/validate_test.dart b/cyrto/test/validate_test.dart
@@ -0,0 +1,66 @@
+import 'package:test/test.dart';
+import 'package:cyrtophora/cyrtophora.dart';
+
+void main() {
+ group('Test cyrtophora validator', () {
+ test('Test fullname validator', () {
+ final validName1 = "John Doe";
+ final validName2 = "Peter Bob Bunny Wailer";
+ final invalidName1 =
+ "kdfjafdjkdf dfjfdkdfjdf fdkjfdkjf dkfdfjdkfj dfdfjdfdkjf dfkjfdjdf fdkjfdjdkf fdnfdkjfd dffd dffdkfd fkdjfdj fdfdfdfdffdfdf dfkjf";
+ final invalidName2 = "";
+
+ expect(Validate.fullname(validName1), true);
+ expect(Validate.fullname(validName2), true);
+
+ // Name cannot be more than 70 characters long
+ expect(Validate.fullname(invalidName1), false);
+
+ // Name cannot be empty
+ expect(Validate.fullname(invalidName2), false);
+ });
+
+ test('Test username validator', () {
+ final validUsername1 = "cy6erlion";
+ final validUsername2 = "cy6er_lion";
+ final validUsername3 = "cy6er1ion2022";
+ final validUsername4 = "fishcanyon10";
+
+ final invalidUsername1 = "_cy6erlion";
+ final invalidUsername2 = "cy6er.lion";
+ final invalidUsername3 = "cy6er__lion";
+ final invalidUsername4 =
+ "cy6erlion1111111111111111111111111eweewewewewew";
+ final invalidUsername5 = "";
+
+ expect(Validate.username(validUsername1), true);
+ expect(Validate.username(validUsername2), true);
+ expect(Validate.username(validUsername3), true);
+ expect(Validate.username(validUsername4), true);
+
+ expect(Validate.username(invalidUsername1), false);
+ expect(Validate.username(invalidUsername2), false);
+ expect(Validate.username(invalidUsername3), false);
+ expect(Validate.username(invalidUsername4), false);
+ expect(Validate.username(invalidUsername5), false);
+ });
+
+ test('Test password validator', () {
+ final validPassword = "thisIs_a_P@ssword";
+ final invalidPassword = "short";
+
+ expect(Validate.password(validPassword), true);
+ expect(Validate.password(invalidPassword), false);
+ });
+
+ test('Test email validator', () {
+ final validEmail = "info@example.com";
+ final invalidEmail1 = "wrong";
+ final invalidEmail2 = "";
+
+ expect(Validate.email(validEmail), true);
+ expect(Validate.email(invalidEmail1), false);
+ expect(Validate.email(invalidEmail2), false);
+ });
+ });
+}
diff --git a/phora/Cargo.lock b/phora/Cargo.lock
@@ -128,6 +128,7 @@ dependencies = [
"chacha20poly1305",
"ring",
"scrypt",
+ "serde",
]
[[package]]
@@ -312,6 +313,26 @@ dependencies = [
]
[[package]]
+name = "serde"
+version = "1.0.144"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.144"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "sha2"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/phora/Cargo.toml b/phora/Cargo.toml
@@ -8,4 +8,5 @@ anyhow = "1.0.57"
ring = "0.16.20"
chacha20poly1305 = "0.9.0"
base64 = "0.13.0"
-scrypt = "0.10.0"
-\ No newline at end of file
+scrypt = "0.10.0"
+serde = { version = "1.0.144", features = ["derive"] }
+\ No newline at end of file
diff --git a/phora/src/account.rs b/phora/src/account.rs
@@ -1,6 +1,9 @@
-use crate::validate::{Validate, ValidationError};
+use crate::data::AccountRegistration;
+use crate::validate::ValidationError;
+use serde::{Deserialize, Serialize};
/// An account
+#[derive(Deserialize, Serialize)]
pub struct Account {
/// The username of the user, also used as an unique identifier
pub username: String,
@@ -11,37 +14,34 @@ pub struct Account {
}
impl Account {
- /// Register new user
- pub fn register(
- username: &str,
- password: &str,
- email: Option<&str>,
- ) -> Result<Self, ValidationError> {
- let mut validated_email: Option<String> = None;
+ /// Create new account
+ pub fn create(payload: AccountRegistration) -> Result<PublicAccount, ValidationError> {
+ payload.is_valid()?;
- if !Validate::username(username) {
- return Err(ValidationError::Username);
- }
-
- if !Validate::password(password) {
- return Err(ValidationError::Email);
- }
+ // TODO: email verification
+ // TODO: database registration
- if let Some(email) = email {
- if !Validate::email(email) {
- return Err(ValidationError::Email);
- }
- validated_email = Some(email.to_string());
+ let account = Account {
+ username: payload.username,
+ email: payload.email,
+ password: payload.password,
+ };
- // TODO: email verification code
- }
+ Ok(account.into())
+ }
+}
- // TODO: database registration code
+/// Account Public Data
+#[derive(Deserialize, Serialize)]
+pub struct PublicAccount {
+ /// The username of the user, also used as an unique identifier
+ pub username: String,
+}
- Ok(Account {
- username: username.to_string(),
- email: validated_email,
- password: password.to_string(),
- })
+impl From<Account> for PublicAccount {
+ fn from(account: Account) -> Self {
+ PublicAccount {
+ username: account.username,
+ }
}
}
diff --git a/phora/src/api.rs b/phora/src/api.rs
@@ -0,0 +1,13 @@
+pub struct API {
+ pub version: String,
+ pub accounts: String,
+}
+
+impl Default for API {
+ fn default() -> Self {
+ API {
+ version: "/v0".to_string(),
+ accounts: "/v0/accounts".to_string(),
+ }
+ }
+}
diff --git a/phora/src/crypto.rs b/phora/src/crypto.rs
@@ -13,11 +13,8 @@ use scrypt::{
Scrypt,
};
-const CREDENTIAL_LEN: usize = digest::SHA256_OUTPUT_LEN;
-
/// Nonce used when encrypting the Crypto's keypair (Crypto::encrypt_keypair_document())
const AED_NONCE: &str = "people the information ";
-pub type Credential = [u8; CREDENTIAL_LEN];
pub struct Credentials {
/// Base64 encoded key pair serialized as a PKCS#8 document
diff --git a/phora/src/data.rs b/phora/src/data.rs
@@ -0,0 +1,35 @@
+use crate::validate::{Validate, ValidationError};
+use serde::{Deserialize, Serialize};
+
+/// User registration Data
+#[derive(Deserialize, Serialize)]
+pub struct AccountRegistration {
+ pub username: String,
+ pub password: String,
+ pub retyped_password: String,
+ pub email: Option<String>,
+}
+
+impl AccountRegistration {
+ pub fn is_valid(&self) -> Result<(), ValidationError> {
+ if self.password != self.retyped_password {
+ return Err(ValidationError::Password);
+ }
+
+ if !Validate::username(&self.username) {
+ return Err(ValidationError::Username);
+ }
+
+ if !Validate::password(&self.password) {
+ return Err(ValidationError::Password);
+ }
+
+ if let Some(email) = &self.email {
+ if !Validate::email(email) {
+ return Err(ValidationError::Email);
+ }
+ }
+
+ Ok(())
+ }
+}
diff --git a/phora/src/lib.rs b/phora/src/lib.rs
@@ -1,3 +1,5 @@
-mod account;
-mod crypto;
-mod validate;
+pub mod account;
+pub mod api;
+pub mod crypto;
+pub mod data;
+pub mod validate;